view.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document, HTMLElement */
  6. /* bender-tags: ui */
  7. 'use strict';
  8. import testUtils from '/tests/ckeditor5/_utils/utils.js';
  9. import View from '/ckeditor5/ui/view.js';
  10. import Region from '/ckeditor5/ui/region.js';
  11. import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
  12. import Model from '/ckeditor5/ui/model.js';
  13. let TestView, view;
  14. testUtils.createSinonSandbox();
  15. describe( 'View', () => {
  16. describe( 'constructor', () => {
  17. beforeEach( () => {
  18. setTestViewClass();
  19. setTestViewInstance();
  20. } );
  21. it( 'accepts the model', () => {
  22. setTestViewInstance( { a: 'foo', b: 42 } );
  23. expect( view.model ).to.be.an.instanceof( Model );
  24. expect( view ).to.have.deep.property( 'model.a', 'foo' );
  25. expect( view ).to.have.deep.property( 'model.b', 42 );
  26. } );
  27. it( 'defines basic view properties', () => {
  28. const model = new Model();
  29. view = new View( model );
  30. expect( view.model ).to.equal( model );
  31. expect( view.t ).to.be.undefined;
  32. expect( view.regions.length ).to.equal( 0 );
  33. } );
  34. it( 'defines the locale property and the t function', () => {
  35. const model = new Model();
  36. const locale = { t() {} };
  37. view = new View( model, locale );
  38. expect( view.locale ).to.equal( locale );
  39. expect( view.t ).to.equal( locale.t );
  40. } );
  41. } );
  42. describe( 'init', () => {
  43. beforeEach( () => {
  44. setTestViewClass( () => ( {
  45. tag: 'p',
  46. children: [
  47. { tag: 'span' },
  48. { tag: 'strong' }
  49. ]
  50. } ) );
  51. } );
  52. it( 'calls child regions #init', () => {
  53. setTestViewInstance();
  54. const region1 = new Region( 'x' );
  55. const region2 = new Region( 'y' );
  56. view.register( region1, el => el );
  57. view.register( region2, el => el );
  58. const spy1 = testUtils.sinon.spy( region1, 'init' );
  59. const spy2 = testUtils.sinon.spy( region2, 'init' );
  60. view.init();
  61. sinon.assert.calledOnce( spy1 );
  62. sinon.assert.calledOnce( spy2 );
  63. } );
  64. it( 'initializes view regions with string selector', () => {
  65. setTestViewInstance();
  66. const region1 = new Region( 'x' );
  67. const region2 = new Region( 'y' );
  68. view.register( region1, 'span' );
  69. view.register( region2, 'strong' );
  70. view.init();
  71. expect( region1.element ).to.equal( view.element.firstChild );
  72. expect( region2.element ).to.equal( view.element.lastChild );
  73. } );
  74. it( 'initializes view regions with function selector', () => {
  75. setTestViewInstance();
  76. const region1 = new Region( 'x' );
  77. const region2 = new Region( 'y' );
  78. view.register( region1, el => el.firstChild );
  79. view.register( region2, el => el.lastChild );
  80. view.init();
  81. expect( region1.element ).to.equal( view.element.firstChild );
  82. expect( region2.element ).to.equal( view.element.lastChild );
  83. } );
  84. it( 'initializes view regions with boolean selector', () => {
  85. setTestViewInstance();
  86. const region1 = new Region( 'x' );
  87. const region2 = new Region( 'y' );
  88. view.register( region1, true );
  89. view.register( region2, true );
  90. view.init();
  91. expect( region1.element ).to.be.null;
  92. expect( region2.element ).to.be.null;
  93. } );
  94. } );
  95. describe( 'register', () => {
  96. beforeEach( () => {
  97. setTestViewClass();
  98. setTestViewInstance();
  99. } );
  100. it( 'should throw when first argument is neither Region instance nor string', () => {
  101. expect( () => {
  102. view.register( new Date() );
  103. } ).to.throw( CKEditorError, /ui-view-register-wrongtype/ );
  104. } );
  105. it( 'should throw when missing the selector argument', () => {
  106. expect( () => {
  107. view.register( 'x' );
  108. } ).to.throw( CKEditorError, /ui-view-register-badselector/ );
  109. } );
  110. it( 'should throw when selector argument is of a wrong type', () => {
  111. expect( () => {
  112. view.register( 'x', new Date() );
  113. } ).to.throw( CKEditorError, /ui-view-register-badselector/ );
  114. expect( () => {
  115. view.register( 'x', false );
  116. } ).to.throw( CKEditorError, /ui-view-register-badselector/ );
  117. } );
  118. it( 'should throw when overriding an existing region but without override flag set', () => {
  119. expect( () => {
  120. view.register( 'x', true );
  121. view.register( new Region( 'x' ), true );
  122. } ).to.throw( CKEditorError, /ui-view-register-override/ );
  123. } );
  124. it( 'should register a new region with region name as a first argument', () => {
  125. view.register( 'x', true );
  126. expect( view.regions.get( 'x' ) ).to.be.an.instanceof( Region );
  127. } );
  128. it( 'should register a new region with Region instance as a first argument', () => {
  129. view.register( new Region( 'y' ), true );
  130. expect( view.regions.get( 'y' ) ).to.be.an.instanceof( Region );
  131. } );
  132. it( 'should override an existing region with override flag', () => {
  133. view.template = {
  134. tag: 'div',
  135. children: [
  136. { tag: 'span' }
  137. ]
  138. };
  139. const region1 = new Region( 'x' );
  140. const region2 = new Region( 'x' );
  141. view.register( region1, true );
  142. view.register( region2, true, true );
  143. view.register( 'x', 'span', true );
  144. view.init();
  145. expect( view.regions.get( 'x' ) ).to.equal( region2 );
  146. expect( view.regions.get( 'x' ).element ).to.equal( view.element.firstChild );
  147. } );
  148. it( 'should not override an existing region with the same region with override flag', () => {
  149. const region = new Region( 'x' );
  150. const spy = testUtils.sinon.spy( view.regions, 'remove' );
  151. view.register( region, true );
  152. view.register( region, true, true );
  153. sinon.assert.notCalled( spy );
  154. } );
  155. } );
  156. describe( 'element', () => {
  157. beforeEach( createViewInstanceWithTemplate );
  158. it( 'invokes out of #template', () => {
  159. setTestViewInstance( { a: 1 } );
  160. expect( view.element ).to.be.an.instanceof( HTMLElement );
  161. expect( view.element.nodeName ).to.equal( 'A' );
  162. } );
  163. it( 'can be explicitly declared', () => {
  164. class CustomView extends View {
  165. constructor() {
  166. super();
  167. this.element = document.createElement( 'span' );
  168. }
  169. }
  170. view = new CustomView();
  171. expect( view.element ).to.be.an.instanceof( HTMLElement );
  172. } );
  173. it( 'is null when there is no template', () => {
  174. expect( new View().element ).to.be.null;
  175. } );
  176. } );
  177. describe( 'attributeBinder', () => {
  178. it( 'provides "to" and "if" interface', () => {
  179. const bind = view.attributeBinder;
  180. expect( bind ).to.have.keys( 'to', 'if' );
  181. expect( typeof bind.to ).to.equal( 'function' );
  182. expect( typeof bind.if ).to.equal( 'function' );
  183. } );
  184. describe( 'to', () => {
  185. it( 'returns an object which describes the binding', () => {
  186. setTestViewInstance( { a: 1 } );
  187. const spy = testUtils.sinon.spy();
  188. const bind = view.attributeBinder;
  189. const binding = bind.to( 'a', spy );
  190. expect( spy.called ).to.be.false;
  191. expect( binding ).to.have.keys( [ 'type', 'model', 'attribute', 'callback' ] );
  192. expect( binding.model ).to.equal( view.model );
  193. expect( binding.callback ).to.equal( spy );
  194. expect( binding.attribute ).to.equal( 'a' );
  195. } );
  196. it( 'allows binding attribute to the model – simple (HTMLElement attribute)', () => {
  197. setTestViewClass( function() {
  198. const bind = this.attributeBinder;
  199. return {
  200. tag: 'p',
  201. attributes: {
  202. 'class': bind.to( 'foo' )
  203. },
  204. children: [ 'abc' ]
  205. };
  206. } );
  207. setTestViewInstance( { foo: 'bar' } );
  208. expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  209. view.model.foo = 'baz';
  210. expect( view.element.outerHTML ).to.equal( '<p class="baz">abc</p>' );
  211. expect( view.element.attributes.getNamedItem( 'class' ).namespaceURI ).to.be.null;
  212. } );
  213. it( 'allows binding attribute to the model – simple (Text Node)', () => {
  214. setTestViewClass( function() {
  215. const bind = this.attributeBinder;
  216. return {
  217. tag: 'p',
  218. children: [
  219. {
  220. text: bind.to( 'foo' )
  221. }
  222. ]
  223. };
  224. } );
  225. setTestViewInstance( { foo: 'bar' } );
  226. expect( view.element.outerHTML ).to.equal( '<p>bar</p>' );
  227. view.model.foo = 'baz';
  228. expect( view.element.outerHTML ).to.equal( '<p>baz</p>' );
  229. } );
  230. it( 'allows binding attribute to the model – value processing', () => {
  231. setTestViewClass( function() {
  232. const bind = this.attributeBinder;
  233. const callback = value => value > 0 ? 'positive' : 'negative';
  234. return {
  235. tag: 'p',
  236. attributes: {
  237. 'class': bind.to( 'foo', callback )
  238. },
  239. children: [
  240. {
  241. text: bind.to( 'foo', callback )
  242. }
  243. ]
  244. };
  245. } );
  246. setTestViewInstance( { foo: 3 } );
  247. expect( view.element.outerHTML ).to.equal( '<p class="positive">positive</p>' );
  248. view.model.foo = -7;
  249. expect( view.element.outerHTML ).to.equal( '<p class="negative">negative</p>' );
  250. } );
  251. it( 'allows binding attribute to the model – value processing (use Node)', () => {
  252. setTestViewClass( function() {
  253. const bind = this.attributeBinder;
  254. const callback = ( value, node ) => {
  255. return ( !!node.tagName && value > 0 ) ? 'HTMLElement positive' : '';
  256. };
  257. return {
  258. tag: 'p',
  259. attributes: {
  260. 'class': bind.to( 'foo', callback )
  261. },
  262. children: [
  263. {
  264. text: bind.to( 'foo', callback )
  265. }
  266. ]
  267. };
  268. } );
  269. setTestViewInstance( { foo: 3 } );
  270. expect( view.element.outerHTML ).to.equal( '<p class="HTMLElement positive"></p>' );
  271. view.model.foo = -7;
  272. expect( view.element.outerHTML ).to.equal( '<p></p>' );
  273. } );
  274. it( 'allows binding attribute to the model – custom callback', () => {
  275. setTestViewClass( function() {
  276. const bind = this.attributeBinder;
  277. return {
  278. tag: 'p',
  279. attributes: {
  280. 'class': bind.to( 'foo', ( value, el ) => {
  281. el.innerHTML = value;
  282. if ( value == 'changed' ) {
  283. return value;
  284. }
  285. } )
  286. }
  287. };
  288. } );
  289. setTestViewInstance( { foo: 'moo' } );
  290. expect( view.element.outerHTML ).to.equal( '<p class="undefined">moo</p>' );
  291. view.model.foo = 'changed';
  292. expect( view.element.outerHTML ).to.equal( '<p class="changed">changed</p>' );
  293. } );
  294. it( 'allows binding attribute to the model – array of bindings (HTMLElement attribute)', () => {
  295. setTestViewClass( function() {
  296. const bind = this.attributeBinder;
  297. return {
  298. tag: 'p',
  299. attributes: {
  300. 'class': [
  301. 'ck-class',
  302. bind.to( 'foo' ),
  303. bind.to( 'bar' ),
  304. bind.to( 'foo', value => `foo-is-${value}` ),
  305. 'ck-end'
  306. ]
  307. },
  308. children: [ 'abc' ]
  309. };
  310. } );
  311. setTestViewInstance( { foo: 'a', bar: 'b' } );
  312. expect( view.element.outerHTML ).to.equal( '<p class="ck-class a b foo-is-a ck-end">abc</p>' );
  313. view.model.foo = 'c';
  314. view.model.bar = 'd';
  315. expect( view.element.outerHTML ).to.equal( '<p class="ck-class c d foo-is-c ck-end">abc</p>' );
  316. } );
  317. it( 'allows binding attribute to the model – array of bindings (Text Node)', () => {
  318. setTestViewClass( function() {
  319. const bind = this.attributeBinder;
  320. return {
  321. tag: 'p',
  322. attributes: {
  323. },
  324. children: [
  325. {
  326. text: [
  327. 'ck-class',
  328. bind.to( 'foo' ),
  329. bind.to( 'bar' ),
  330. bind.to( 'foo', value => `foo-is-${value}` ),
  331. 'ck-end'
  332. ]
  333. }
  334. ]
  335. };
  336. } );
  337. setTestViewInstance( { foo: 'a', bar: 'b' } );
  338. expect( view.element.outerHTML ).to.equal( '<p>ck-class a b foo-is-a ck-end</p>' );
  339. view.model.foo = 'c';
  340. view.model.bar = 'd';
  341. expect( view.element.outerHTML ).to.equal( '<p>ck-class c d foo-is-c ck-end</p>' );
  342. } );
  343. it( 'allows binding attribute to the model – falsy values', () => {
  344. setTestViewClass( function() {
  345. const bind = this.attributeBinder;
  346. return {
  347. tag: 'p',
  348. attributes: {
  349. 'class': bind.to( 'foo' )
  350. },
  351. children: [ 'abc' ]
  352. };
  353. } );
  354. setTestViewInstance( { foo: 'bar' } );
  355. expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  356. view.model.foo = false;
  357. expect( view.element.outerHTML ).to.equal( '<p class="false">abc</p>' );
  358. view.model.foo = null;
  359. expect( view.element.outerHTML ).to.equal( '<p class="null">abc</p>' );
  360. view.model.foo = undefined;
  361. expect( view.element.outerHTML ).to.equal( '<p class="undefined">abc</p>' );
  362. view.model.foo = 0;
  363. expect( view.element.outerHTML ).to.equal( '<p class="0">abc</p>' );
  364. view.model.foo = '';
  365. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  366. } );
  367. it( 'allows binding attribute to the model – a custom namespace', () => {
  368. setTestViewClass( function() {
  369. const bind = this.attributeBinder;
  370. return {
  371. tag: 'p',
  372. attributes: {
  373. 'class': {
  374. ns: 'foo',
  375. value: bind.to( 'foo' )
  376. }
  377. },
  378. children: [ 'abc' ]
  379. };
  380. } );
  381. setTestViewInstance( { foo: 'bar' } );
  382. expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  383. expect( view.element.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  384. view.model.foo = 'baz';
  385. expect( view.element.outerHTML ).to.equal( '<p class="baz">abc</p>' );
  386. expect( view.element.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  387. } );
  388. } );
  389. describe( 'if', () => {
  390. it( 'returns an object which describes the binding', () => {
  391. setTestViewInstance( { a: 1 } );
  392. const spy = testUtils.sinon.spy();
  393. const bind = view.attributeBinder;
  394. const binding = bind.if( 'a', 'foo', spy );
  395. expect( spy.called ).to.be.false;
  396. expect( binding ).to.have.keys( [ 'type', 'model', 'attribute', 'callback', 'valueIfTrue' ] );
  397. expect( binding.model ).to.equal( view.model );
  398. expect( binding.callback ).to.equal( spy );
  399. expect( binding.attribute ).to.equal( 'a' );
  400. expect( binding.valueIfTrue ).to.equal( 'foo' );
  401. } );
  402. it( 'allows binding attribute to the model – presence of an attribute (HTMLElement attribute)', () => {
  403. setTestViewClass( function() {
  404. const bind = this.attributeBinder;
  405. return {
  406. tag: 'p',
  407. attributes: {
  408. 'class': bind.if( 'foo' )
  409. },
  410. children: [ 'abc' ]
  411. };
  412. } );
  413. setTestViewInstance( { foo: true } );
  414. expect( view.element.outerHTML ).to.equal( '<p class="">abc</p>' );
  415. view.model.foo = false;
  416. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  417. view.model.foo = 'bar';
  418. expect( view.element.outerHTML ).to.equal( '<p class="">abc</p>' );
  419. } );
  420. // TODO: Is this alright? It makes sense but it's pretty useless. Text Node cannot be
  421. // removed just like an attribute of some HTMLElement.
  422. it( 'allows binding attribute to the model – presence of an attribute (Text Node)', () => {
  423. setTestViewClass( function() {
  424. const bind = this.attributeBinder;
  425. return {
  426. tag: 'p',
  427. children: [
  428. {
  429. text: bind.if( 'foo' )
  430. }
  431. ]
  432. };
  433. } );
  434. setTestViewInstance( { foo: true } );
  435. expect( view.element.outerHTML ).to.equal( '<p></p>' );
  436. view.model.foo = false;
  437. expect( view.element.outerHTML ).to.equal( '<p></p>' );
  438. view.model.foo = 'bar';
  439. expect( view.element.outerHTML ).to.equal( '<p></p>' );
  440. } );
  441. it( 'allows binding attribute to the model – value of an attribute (HTMLElement attribute)', () => {
  442. setTestViewClass( function() {
  443. const bind = this.attributeBinder;
  444. return {
  445. tag: 'p',
  446. attributes: {
  447. 'class': bind.if( 'foo', 'bar' )
  448. },
  449. children: [ 'abc' ]
  450. };
  451. } );
  452. setTestViewInstance( { foo: 'bar' } );
  453. expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  454. view.model.foo = false;
  455. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  456. view.model.foo = 64;
  457. expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
  458. } );
  459. it( 'allows binding attribute to the model – value of an attribute (Text Node)', () => {
  460. setTestViewClass( function() {
  461. const bind = this.attributeBinder;
  462. return {
  463. tag: 'p',
  464. children: [
  465. {
  466. text: bind.if( 'foo', 'bar' )
  467. }
  468. ]
  469. };
  470. } );
  471. setTestViewInstance( { foo: 'bar' } );
  472. expect( view.element.outerHTML ).to.equal( '<p>bar</p>' );
  473. view.model.foo = false;
  474. expect( view.element.outerHTML ).to.equal( '<p></p>' );
  475. view.model.foo = 64;
  476. expect( view.element.outerHTML ).to.equal( '<p>bar</p>' );
  477. } );
  478. it( 'allows binding attribute to the model – array of bindings (HTMLElement attribute)', () => {
  479. setTestViewClass( function() {
  480. const bind = this.attributeBinder;
  481. return {
  482. tag: 'p',
  483. attributes: {
  484. 'class': [
  485. 'ck-class',
  486. bind.if( 'foo', 'foo-set' ),
  487. bind.if( 'bar', 'bar-not-set', ( value ) => !value ),
  488. 'ck-end'
  489. ]
  490. },
  491. children: [ 'abc' ]
  492. };
  493. } );
  494. setTestViewInstance( { foo: true, bar: true } );
  495. expect( view.element.outerHTML ).to.equal( '<p class="ck-class foo-set ck-end">abc</p>' );
  496. view.model.foo = view.model.bar = false;
  497. expect( view.element.outerHTML ).to.equal( '<p class="ck-class bar-not-set ck-end">abc</p>' );
  498. } );
  499. it( 'allows binding attribute to the model – value of an attribute processed by a callback', () => {
  500. setTestViewClass( function() {
  501. const bind = this.attributeBinder;
  502. return {
  503. tag: 'p',
  504. attributes: {
  505. 'class': bind.if( 'foo', 'there–is–no–foo', value => !value )
  506. },
  507. children: [ 'abc' ]
  508. };
  509. } );
  510. setTestViewInstance( { foo: 'bar' } );
  511. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  512. view.model.foo = false;
  513. expect( view.element.outerHTML ).to.equal( '<p class="there–is–no–foo">abc</p>' );
  514. view.model.foo = 64;
  515. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  516. } );
  517. it( 'allows binding attribute to the model – value of an attribute processed by a callback (use Node)', () => {
  518. setTestViewClass( function() {
  519. const bind = this.attributeBinder;
  520. return {
  521. tag: 'p',
  522. attributes: {
  523. 'class': bind.if( 'foo', 'eqls-tag-name', ( value, el ) => el.tagName === value )
  524. },
  525. children: [ 'abc' ]
  526. };
  527. } );
  528. setTestViewInstance( { foo: 'bar' } );
  529. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  530. view.model.foo = 'P';
  531. expect( view.element.outerHTML ).to.equal( '<p class="eqls-tag-name">abc</p>' );
  532. view.model.foo = 64;
  533. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  534. } );
  535. it( 'allows binding attribute to the model – falsy values', () => {
  536. setTestViewClass( function() {
  537. const bind = this.attributeBinder;
  538. return {
  539. tag: 'p',
  540. attributes: {
  541. 'class': bind.if( 'foo', 'foo-is-set' )
  542. },
  543. children: [ 'abc' ]
  544. };
  545. } );
  546. setTestViewInstance( { foo: 'bar' } );
  547. expect( view.element.outerHTML ).to.equal( '<p class="foo-is-set">abc</p>' );
  548. view.model.foo = false;
  549. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  550. view.model.foo = null;
  551. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  552. view.model.foo = undefined;
  553. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  554. view.model.foo = '';
  555. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  556. view.model.foo = 0;
  557. expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
  558. } );
  559. } );
  560. } );
  561. describe( 'on', () => {
  562. it( 'accepts plain binding', () => {
  563. const spy = testUtils.sinon.spy();
  564. setTestViewClass( function() {
  565. return {
  566. tag: 'p',
  567. on: {
  568. x: 'a',
  569. }
  570. };
  571. } );
  572. setTestViewInstance();
  573. view.on( 'a', spy );
  574. dispatchEvent( view.element, 'x' );
  575. sinon.assert.calledWithExactly( spy,
  576. sinon.match.has( 'name', 'a' ),
  577. sinon.match.has( 'target', view.element )
  578. );
  579. } );
  580. it( 'accepts an array of event bindings', () => {
  581. const spy1 = testUtils.sinon.spy();
  582. const spy2 = testUtils.sinon.spy();
  583. setTestViewClass( function() {
  584. return {
  585. tag: 'p',
  586. on: {
  587. x: [ 'a', 'b' ]
  588. }
  589. };
  590. } );
  591. setTestViewInstance();
  592. view.on( 'a', spy1 );
  593. view.on( 'b', spy2 );
  594. dispatchEvent( view.element, 'x' );
  595. sinon.assert.calledWithExactly( spy1,
  596. sinon.match.has( 'name', 'a' ),
  597. sinon.match.has( 'target', view.element )
  598. );
  599. sinon.assert.calledWithExactly( spy2,
  600. sinon.match.has( 'name', 'b' ),
  601. sinon.match.has( 'target', view.element )
  602. );
  603. } );
  604. it( 'accepts DOM selectors', () => {
  605. const spy1 = testUtils.sinon.spy();
  606. const spy2 = testUtils.sinon.spy();
  607. const spy3 = testUtils.sinon.spy();
  608. setTestViewClass( function() {
  609. return {
  610. tag: 'p',
  611. children: [
  612. {
  613. tag: 'span',
  614. attributes: {
  615. 'class': 'y',
  616. },
  617. on: {
  618. 'test@p': 'c'
  619. }
  620. },
  621. {
  622. tag: 'div',
  623. children: [
  624. {
  625. tag: 'span',
  626. attributes: {
  627. 'class': 'y',
  628. }
  629. }
  630. ],
  631. }
  632. ],
  633. on: {
  634. 'test@.y': 'a',
  635. 'test@div': 'b'
  636. }
  637. };
  638. } );
  639. setTestViewInstance();
  640. view.on( 'a', spy1 );
  641. view.on( 'b', spy2 );
  642. view.on( 'c', spy3 );
  643. // Test "test@p".
  644. dispatchEvent( view.element, 'test' );
  645. sinon.assert.callCount( spy1, 0 );
  646. sinon.assert.callCount( spy2, 0 );
  647. sinon.assert.callCount( spy3, 0 );
  648. // Test "test@.y".
  649. dispatchEvent( view.element.firstChild, 'test' );
  650. expect( spy1.firstCall.calledWithExactly(
  651. sinon.match.has( 'name', 'a' ),
  652. sinon.match.has( 'target', view.element.firstChild )
  653. ) ).to.be.true;
  654. sinon.assert.callCount( spy2, 0 );
  655. sinon.assert.callCount( spy3, 0 );
  656. // Test "test@div".
  657. dispatchEvent( view.element.lastChild, 'test' );
  658. sinon.assert.callCount( spy1, 1 );
  659. expect( spy2.firstCall.calledWithExactly(
  660. sinon.match.has( 'name', 'b' ),
  661. sinon.match.has( 'target', view.element.lastChild )
  662. ) ).to.be.true;
  663. sinon.assert.callCount( spy3, 0 );
  664. // Test "test@.y".
  665. dispatchEvent( view.element.lastChild.firstChild, 'test' );
  666. expect( spy1.secondCall.calledWithExactly(
  667. sinon.match.has( 'name', 'a' ),
  668. sinon.match.has( 'target', view.element.lastChild.firstChild )
  669. ) ).to.be.true;
  670. sinon.assert.callCount( spy2, 1 );
  671. sinon.assert.callCount( spy3, 0 );
  672. } );
  673. it( 'accepts function callbacks', () => {
  674. const spy1 = testUtils.sinon.spy();
  675. const spy2 = testUtils.sinon.spy();
  676. setTestViewClass( function() {
  677. return {
  678. tag: 'p',
  679. children: [
  680. {
  681. tag: 'span'
  682. }
  683. ],
  684. on: {
  685. x: spy1,
  686. 'y@span': [ spy2, 'c' ],
  687. }
  688. };
  689. } );
  690. setTestViewInstance();
  691. dispatchEvent( view.element, 'x' );
  692. dispatchEvent( view.element.firstChild, 'y' );
  693. sinon.assert.calledWithExactly( spy1,
  694. sinon.match.has( 'target', view.element )
  695. );
  696. sinon.assert.calledWithExactly( spy2,
  697. sinon.match.has( 'target', view.element.firstChild )
  698. );
  699. } );
  700. it( 'supports event delegation', () => {
  701. const spy = testUtils.sinon.spy();
  702. setTestViewClass( function() {
  703. return {
  704. tag: 'p',
  705. children: [
  706. {
  707. tag: 'span'
  708. }
  709. ],
  710. on: {
  711. x: 'a',
  712. }
  713. };
  714. } );
  715. setTestViewInstance();
  716. view.on( 'a', spy );
  717. dispatchEvent( view.element.firstChild, 'x' );
  718. sinon.assert.calledWithExactly( spy,
  719. sinon.match.has( 'name', 'a' ),
  720. sinon.match.has( 'target', view.element.firstChild )
  721. );
  722. } );
  723. it( 'works for future elements', () => {
  724. const spy = testUtils.sinon.spy();
  725. setTestViewClass( function() {
  726. return {
  727. tag: 'p',
  728. on: {
  729. 'test@div': 'a'
  730. }
  731. };
  732. } );
  733. setTestViewInstance();
  734. view.on( 'a', spy );
  735. const div = document.createElement( 'div' );
  736. view.element.appendChild( div );
  737. dispatchEvent( div, 'test' );
  738. sinon.assert.calledWithExactly( spy, sinon.match.has( 'name', 'a' ), sinon.match.has( 'target', div ) );
  739. } );
  740. } );
  741. describe( 'destroy', () => {
  742. beforeEach( createViewInstanceWithTemplate );
  743. it( 'should destroy the view', () => {
  744. view.destroy();
  745. expect( view.model ).to.be.null;
  746. expect( view.regions ).to.be.null;
  747. expect( view.template ).to.be.null;
  748. expect( view.locale ).to.be.null;
  749. expect( view.t ).to.be.null;
  750. } );
  751. it( 'detaches the element from DOM', () => {
  752. const elRef = view.element;
  753. document.createElement( 'div' ).appendChild( view.element );
  754. view.destroy();
  755. expect( elRef.parentNode ).to.be.null;
  756. } );
  757. it( 'destroys child regions', () => {
  758. const region = new Region( 'x' );
  759. const spy = testUtils.sinon.spy( region, 'destroy' );
  760. const regionsRef = view.regions;
  761. const regionViewsRef = region.views;
  762. view.register( region, true );
  763. view.regions.get( 'x' ).views.add( new View() );
  764. view.destroy();
  765. expect( regionsRef.length ).to.equal( 0 );
  766. expect( regionViewsRef.length ).to.equal( 0 );
  767. expect( spy.calledOnce ).to.be.true;
  768. } );
  769. it( 'detaches bound model listeners', () => {
  770. setTestViewClass( function() {
  771. const bind = this.attributeBinder;
  772. return {
  773. tag: 'p',
  774. children: [
  775. { text: bind.to( 'foo' ) }
  776. ]
  777. };
  778. } );
  779. setTestViewInstance( { foo: 'bar' } );
  780. const modelRef = view.model;
  781. const elRef = view.element;
  782. expect( view.element.outerHTML ).to.equal( '<p>bar</p>' );
  783. modelRef.foo = 'baz';
  784. expect( view.element.outerHTML ).to.equal( '<p>baz</p>' );
  785. view.destroy();
  786. modelRef.foo = 'abc';
  787. expect( elRef.outerHTML ).to.equal( '<p>baz</p>' );
  788. } );
  789. it( 'destroy a template–less view', () => {
  790. view = new View();
  791. expect( () => {
  792. view.destroy();
  793. } ).to.not.throw();
  794. } );
  795. } );
  796. describe( 'applyTemplateToElement', () => {
  797. beforeEach( () => {
  798. setTestViewClass();
  799. setTestViewInstance( { a: 'foo', b: 42 } );
  800. } );
  801. it( 'should initialize attribute bindings', () => {
  802. const el = document.createElement( 'div' );
  803. const bind = view.attributeBinder;
  804. view.applyTemplateToElement( el, {
  805. tag: 'div',
  806. attributes: {
  807. class: bind.to( 'b' ),
  808. id: 'foo',
  809. checked: 'checked'
  810. }
  811. } );
  812. expect( el.outerHTML ).to.equal( '<div class="42" id="foo" checked="checked"></div>' );
  813. view.model.b = 64;
  814. expect( el.outerHTML ).to.equal( '<div class="64" id="foo" checked="checked"></div>' );
  815. } );
  816. it( 'should initialize DOM listeners', () => {
  817. const el = document.createElement( 'div' );
  818. const spy = testUtils.sinon.spy();
  819. view.applyTemplateToElement( el, {
  820. tag: 'div',
  821. on: {
  822. click: spy
  823. }
  824. } );
  825. document.body.appendChild( el );
  826. dispatchEvent( el, 'click' );
  827. sinon.assert.calledOnce( spy );
  828. view.stopListening( el, 'click' );
  829. dispatchEvent( el, 'click' );
  830. sinon.assert.calledOnce( spy );
  831. } );
  832. it( 'should work for deep DOM structure', () => {
  833. const el = document.createElement( 'div' );
  834. const childA = document.createElement( 'a' );
  835. const childB = document.createElement( 'b' );
  836. childA.textContent = 'anchor';
  837. childB.textContent = 'bold';
  838. el.appendChild( childA );
  839. el.appendChild( childB );
  840. expect( el.outerHTML ).to.equal( '<div><a>anchor</a><b>bold</b></div>' );
  841. const spy1 = testUtils.sinon.spy();
  842. const spy2 = testUtils.sinon.spy();
  843. const spy3 = testUtils.sinon.spy();
  844. const bind = view.attributeBinder;
  845. view.applyTemplateToElement( el, {
  846. tag: 'div',
  847. children: [
  848. {
  849. tag: 'a',
  850. on: {
  851. keyup: spy2
  852. },
  853. attributes: {
  854. class: bind.to( 'b', b => 'applied-A-' + b ),
  855. id: 'applied-A'
  856. },
  857. children: [ 'Text applied to childA.' ]
  858. },
  859. {
  860. tag: 'b',
  861. on: {
  862. keydown: spy3
  863. },
  864. attributes: {
  865. class: bind.to( 'b', b => 'applied-B-' + b ),
  866. id: 'applied-B'
  867. },
  868. children: [ 'Text applied to childB.' ]
  869. },
  870. 'Text which is not to be applied because it does NOT exist in original element.'
  871. ],
  872. on: {
  873. 'mouseover@a': spy1
  874. },
  875. attributes: {
  876. id: bind.to( 'a', a => a.toUpperCase() ),
  877. class: bind.to( 'b', b => 'applied-parent-' + b )
  878. }
  879. } );
  880. expect( el.outerHTML ).to.equal( '<div id="FOO" class="applied-parent-42">' +
  881. '<a class="applied-A-42" id="applied-A">Text applied to childA.</a>' +
  882. '<b class="applied-B-42" id="applied-B">Text applied to childB.</b>' +
  883. '</div>' );
  884. view.model.b = 16;
  885. expect( el.outerHTML ).to.equal( '<div id="FOO" class="applied-parent-16">' +
  886. '<a class="applied-A-16" id="applied-A">Text applied to childA.</a>' +
  887. '<b class="applied-B-16" id="applied-B">Text applied to childB.</b>' +
  888. '</div>' );
  889. document.body.appendChild( el );
  890. // Test "mouseover@a".
  891. dispatchEvent( el, 'mouseover' );
  892. dispatchEvent( childA, 'mouseover' );
  893. // Test "keyup".
  894. dispatchEvent( childA, 'keyup' );
  895. // Test "keydown".
  896. dispatchEvent( childB, 'keydown' );
  897. sinon.assert.calledOnce( spy1 );
  898. sinon.assert.calledOnce( spy2 );
  899. sinon.assert.calledOnce( spy3 );
  900. } );
  901. it( 're–uses a template definition object without redundant re–definition of "on" listener attachers', () => {
  902. const el1 = document.createElement( 'div' );
  903. const el2 = document.createElement( 'div' );
  904. const spy = testUtils.sinon.spy();
  905. const def = {
  906. tag: 'div',
  907. on: {
  908. click: spy
  909. }
  910. };
  911. view.applyTemplateToElement( el1, def );
  912. const attacherFn = def.on.click;
  913. view.applyTemplateToElement( el2, def );
  914. // Attacher function didn't change because it's still the same View instance.
  915. expect( attacherFn ).to.equal( def.on.click );
  916. expect( def.on._listenerView ).to.equal( view );
  917. document.body.appendChild( el1 );
  918. document.body.appendChild( el2 );
  919. dispatchEvent( el1, 'click' );
  920. sinon.assert.calledOnce( spy );
  921. dispatchEvent( el2, 'click' );
  922. sinon.assert.calledTwice( spy );
  923. } );
  924. it( 'shares a template definition between View instances – event listeners', () => {
  925. const el = document.createElement( 'div' );
  926. const spy = testUtils.sinon.spy();
  927. const view1 = new View();
  928. const view2 = new View();
  929. const def = {
  930. tag: 'div',
  931. on: {
  932. click: spy
  933. }
  934. };
  935. document.body.appendChild( el );
  936. view1.applyTemplateToElement( el, def );
  937. expect( def.on._listenerView ).to.equal( view1 );
  938. dispatchEvent( el, 'click' );
  939. sinon.assert.calledOnce( spy );
  940. // Use another view1 to see if attachers are re–defined.
  941. view2.applyTemplateToElement( el, def );
  942. expect( def.on._listenerView ).to.equal( view2 );
  943. dispatchEvent( el, 'click' );
  944. // Spy was called for view1 (1st dispatch), then for view1 and view2 (2nd dispatch).
  945. sinon.assert.calledThrice( spy );
  946. } );
  947. } );
  948. } );
  949. function createViewInstanceWithTemplate() {
  950. setTestViewClass( () => ( { tag: 'a' } ) );
  951. setTestViewInstance();
  952. }
  953. function setTestViewClass( templateFn, regionsFn ) {
  954. TestView = class V extends View {
  955. constructor( model ) {
  956. super( model );
  957. if ( templateFn ) {
  958. this.template = templateFn.call( this );
  959. }
  960. if ( templateFn && regionsFn ) {
  961. regionsFn.call( this );
  962. }
  963. }
  964. };
  965. }
  966. function setTestViewInstance( model ) {
  967. view = new TestView( new Model( model ) );
  968. if ( view.template ) {
  969. document.body.appendChild( view.element );
  970. }
  971. }
  972. function dispatchEvent( el, domEvtName ) {
  973. if ( !el.parentNode ) {
  974. throw new Error( 'To dispatch an event, element must be in DOM. Otherwise #target is null.' );
  975. }
  976. el.dispatchEvent( new Event( domEvtName, {
  977. bubbles: true
  978. } ) );
  979. }