template.js 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* bender-tags: ui */
  6. /* globals HTMLElement, Event, document */
  7. import testUtils from 'tests/core/_utils/utils.js';
  8. import Template from 'ckeditor5/ui/template.js';
  9. import { TemplateToBinding, TemplateIfBinding } from 'ckeditor5/ui/template.js';
  10. import View from 'ckeditor5/ui/view.js';
  11. import ViewCollection from 'ckeditor5/ui/viewcollection.js';
  12. import Model from 'ckeditor5/ui/model.js';
  13. import CKEditorError from 'ckeditor5/utils/ckeditorerror.js';
  14. import EmitterMixin from 'ckeditor5/utils/emittermixin.js';
  15. import DomEmitterMixin from 'ckeditor5/utils/dom/emittermixin.js';
  16. import Collection from 'ckeditor5/utils/collection.js';
  17. import normalizeHtml from 'tests/utils/_utils/normalizehtml.js';
  18. testUtils.createSinonSandbox();
  19. let el, text;
  20. describe( 'Template', () => {
  21. describe( 'constructor()', () => {
  22. it( 'accepts and normalizes the definition', () => {
  23. const bind = Template.bind( new Model( {} ), Object.create( DomEmitterMixin ) );
  24. const tpl = new Template( {
  25. tag: 'p',
  26. attributes: {
  27. a: 'foo',
  28. b: [ 'bar', 'baz' ],
  29. c: {
  30. ns: 'abc',
  31. value: bind.to( 'qux' )
  32. }
  33. },
  34. children: [
  35. {
  36. text: 'content'
  37. },
  38. {
  39. text: bind.to( 'x' )
  40. },
  41. 'abc',
  42. {
  43. text: [ 'a', 'b' ]
  44. }
  45. ],
  46. on: {
  47. 'a@span': bind.to( 'b' ),
  48. 'b@span': bind.to( () => {} ),
  49. 'c@span': [
  50. bind.to( 'c' ),
  51. bind.to( () => {} )
  52. ]
  53. }
  54. } );
  55. expect( tpl.attributes.a[ 0 ] ).to.equal( 'foo' );
  56. expect( tpl.attributes.b[ 0 ] ).to.equal( 'bar' );
  57. expect( tpl.attributes.b[ 1 ] ).to.equal( 'baz' );
  58. expect( tpl.attributes.c[ 0 ].value[ 0 ] ).to.be.instanceof( TemplateToBinding );
  59. expect( tpl.children ).to.have.length( 4 );
  60. expect( tpl.children.get( 0 ).text[ 0 ] ).to.equal( 'content' );
  61. expect( tpl.children.get( 1 ).text[ 0 ] ).to.be.instanceof( TemplateToBinding );
  62. expect( tpl.children.get( 2 ).text[ 0 ] ).to.equal( 'abc' );
  63. expect( tpl.children.get( 3 ).text[ 0 ] ).to.equal( 'a' );
  64. expect( tpl.children.get( 3 ).text[ 1 ] ).to.equal( 'b' );
  65. expect( tpl.eventListeners[ 'a@span' ][ 0 ] ).to.be.instanceof( TemplateToBinding );
  66. expect( tpl.eventListeners[ 'b@span' ][ 0 ] ).to.be.instanceof( TemplateToBinding );
  67. expect( tpl.eventListeners[ 'c@span' ][ 0 ] ).to.be.instanceof( TemplateToBinding );
  68. expect( tpl.eventListeners[ 'c@span' ][ 1 ] ).to.be.instanceof( TemplateToBinding );
  69. // Note that Template mixes EmitterMixin.
  70. expect( tpl.on ).to.be.a( 'function' );
  71. expect( tpl.on[ 'a@span' ] ).to.be.undefined;
  72. } );
  73. it( 'defines #children collection', () => {
  74. const elementTpl = new Template( {
  75. tag: 'p',
  76. } );
  77. const textTpl = new Template( {
  78. text: 'foo'
  79. } );
  80. expect( elementTpl.children ).to.be.instanceof( Collection );
  81. expect( elementTpl.children ).to.have.length( 0 );
  82. // Text will never have children.
  83. expect( textTpl.children ).to.be.undefined;
  84. } );
  85. it( 'does not modify passed definition', () => {
  86. const def = {
  87. tag: 'p',
  88. attributes: {
  89. a: 'foo',
  90. },
  91. children: [
  92. {
  93. tag: 'span'
  94. }
  95. ]
  96. };
  97. const tpl = new Template( def );
  98. expect( def.attributes ).to.not.equal( tpl.attributes );
  99. expect( def.children ).to.not.equal( tpl.children );
  100. expect( def.children[ 0 ] ).to.not.equal( tpl.children.get( 0 ) );
  101. expect( def.attributes.a ).to.equal( 'foo' );
  102. expect( def.children[ 0 ].tag ).to.equal( 'span' );
  103. expect( tpl.attributes.a[ 0 ] ).to.equal( 'foo' );
  104. expect( tpl.children.get( 0 ).tag ).to.equal( 'span' );
  105. } );
  106. } );
  107. describe( 'render', () => {
  108. it( 'throws when the template definition is wrong', () => {
  109. expect( () => {
  110. new Template( {} ).render();
  111. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  112. expect( () => {
  113. new Template( {
  114. tag: 'p',
  115. text: 'foo'
  116. } ).render();
  117. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  118. } );
  119. describe( 'DOM Node', () => {
  120. it( 'creates HTMLElement', () => {
  121. const el = new Template( {
  122. tag: 'p',
  123. } ).render();
  124. expect( el ).to.be.instanceof( HTMLElement );
  125. expect( el.parentNode ).to.be.null;
  126. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p></p>' );
  127. expect( el.namespaceURI ).to.equal( 'http://www.w3.org/1999/xhtml' );
  128. } );
  129. it( 'creates an element in a custom namespace', () => {
  130. const el = new Template( {
  131. tag: 'p',
  132. ns: 'foo'
  133. } ).render();
  134. expect( el.namespaceURI ).to.equal( 'foo' );
  135. } );
  136. it( 'creates a Text node', () => {
  137. const node = new Template( { text: 'foo' } ).render();
  138. expect( node.nodeType ).to.equal( 3 );
  139. expect( node.textContent ).to.equal( 'foo' );
  140. } );
  141. } );
  142. describe( 'attributes', () => {
  143. it( 'renders HTMLElement attributes', () => {
  144. const el = new Template( {
  145. tag: 'p',
  146. attributes: {
  147. 'class': [ 'a', 'b' ],
  148. x: 'bar'
  149. },
  150. children: [ 'foo' ]
  151. } ).render();
  152. expect( el ).to.be.instanceof( HTMLElement );
  153. expect( el.parentNode ).to.be.null;
  154. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="a b" x="bar">foo</p>' );
  155. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.be.null;
  156. } );
  157. it( 'renders HTMLElement attributes – empty', () => {
  158. const el = new Template( {
  159. tag: 'p',
  160. attributes: {
  161. class: '',
  162. x: [ '', '' ]
  163. },
  164. children: [ 'foo' ]
  165. } ).render();
  166. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>foo</p>' );
  167. } );
  168. it( 'renders HTMLElement attributes – falsy values', () => {
  169. const el = new Template( {
  170. tag: 'p',
  171. attributes: {
  172. class: false,
  173. x: [ '', null, undefined, false ],
  174. y: [ 'foo', null, undefined, false ]
  175. },
  176. children: [ 'foo' ]
  177. } ).render();
  178. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p y="foo">foo</p>' );
  179. } );
  180. it( 'renders HTMLElement attributes in a custom namespace', () => {
  181. const el = new Template( {
  182. tag: 'p',
  183. attributes: {
  184. class: {
  185. ns: 'foo',
  186. value: [ 'a', 'b' ]
  187. },
  188. x: {
  189. ns: 'abc',
  190. value: 'bar'
  191. }
  192. },
  193. children: [ 'foo' ]
  194. } ).render();
  195. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="a b" x="bar">foo</p>' );
  196. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  197. expect( el.attributes.getNamedItem( 'x' ).namespaceURI ).to.equal( 'abc' );
  198. } );
  199. describe( 'style', () => {
  200. let observable, emitter, bind;
  201. beforeEach( () => {
  202. observable = new Model( {
  203. width: '10px',
  204. backgroundColor: 'yellow'
  205. } );
  206. emitter = Object.create( EmitterMixin );
  207. bind = Template.bind( observable, emitter );
  208. } );
  209. it( 'renders as a static value', () => {
  210. setElement( {
  211. tag: 'p',
  212. attributes: {
  213. style: 'color: red'
  214. }
  215. } );
  216. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p style="color:red;"></p>' );
  217. } );
  218. it( 'renders as a static value (Array of values)', () => {
  219. setElement( {
  220. tag: 'p',
  221. attributes: {
  222. style: [ 'color: red;', 'display: block;' ]
  223. }
  224. } );
  225. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p style="color:red;display:block;"></p>' );
  226. } );
  227. it( 'renders as a value bound to the model', () => {
  228. setElement( {
  229. tag: 'p',
  230. attributes: {
  231. style: bind.to( 'width', w => `width: ${ w }` )
  232. }
  233. } );
  234. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p style="width:10px;"></p>' );
  235. observable.width = '1em';
  236. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p style="width:1em;"></p>' );
  237. } );
  238. it( 'renders as a value bound to the model (Array of bindings)', () => {
  239. setElement( {
  240. tag: 'p',
  241. attributes: {
  242. style: [
  243. bind.to( 'width', w => `width: ${ w };` ),
  244. bind.to( 'backgroundColor', c => `background-color: ${ c };` )
  245. ]
  246. }
  247. } );
  248. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p style="width:10px;background-color:yellow;"></p>' );
  249. observable.width = '1em';
  250. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p style="width:1em;background-color:yellow;"></p>' );
  251. } );
  252. describe( 'object', () => {
  253. it( 'renders with static and bound attributes', () => {
  254. setElement( {
  255. tag: 'p',
  256. attributes: {
  257. style: {
  258. width: bind.to( 'width' ),
  259. height: '10px',
  260. backgroundColor: bind.to( 'backgroundColor' )
  261. }
  262. }
  263. } );
  264. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p style="width:10px;height:10px;background-color:yellow;"></p>' );
  265. observable.width = '20px';
  266. observable.backgroundColor = 'green';
  267. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p style="width:20px;height:10px;background-color:green;"></p>' );
  268. } );
  269. it( 'renders with empty string attributes', () => {
  270. setElement( {
  271. tag: 'p',
  272. attributes: {
  273. style: {
  274. width: '',
  275. backgroundColor: bind.to( 'backgroundColor' )
  276. }
  277. }
  278. } );
  279. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p style="background-color:yellow;"></p>' );
  280. observable.backgroundColor = '';
  281. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p></p>' );
  282. } );
  283. it( 'renders with falsy values', () => {
  284. setElement( {
  285. tag: 'p',
  286. attributes: {
  287. style: {
  288. width: null,
  289. height: false,
  290. color: undefined
  291. }
  292. }
  293. } );
  294. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p></p>' );
  295. } );
  296. } );
  297. } );
  298. } );
  299. describe( 'children', () => {
  300. it( 'creates HTMLElement children', () => {
  301. const el = new Template( {
  302. tag: 'p',
  303. attributes: {
  304. a: 'A'
  305. },
  306. children: [
  307. {
  308. tag: 'b',
  309. children: [ 'B' ]
  310. },
  311. {
  312. tag: 'i',
  313. children: [
  314. 'C',
  315. {
  316. tag: 'b',
  317. children: [ 'D' ]
  318. }
  319. ]
  320. }
  321. ]
  322. } ).render();
  323. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p a="A"><b>B</b><i>C<b>D</b></i></p>' );
  324. } );
  325. it( 'creates a child Text Node (different syntaxes)', () => {
  326. const el = new Template( {
  327. tag: 'p',
  328. children: [
  329. 'foo',
  330. { text: 'bar' }
  331. ]
  332. } ).render();
  333. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>foobar</p>' );
  334. } );
  335. it( 'creates multiple child Text Nodes', () => {
  336. const el = new Template( {
  337. tag: 'p',
  338. children: [
  339. 'a',
  340. 'b',
  341. { text: 'c' },
  342. 'd',
  343. { text: [ 'e', 'f' ] }
  344. ]
  345. } ).render();
  346. expect( el.childNodes ).to.have.length( 5 );
  347. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abcdef</p>' );
  348. } );
  349. it( 'renders view children', () => {
  350. const v1 = getView( {
  351. tag: 'span',
  352. attributes: {
  353. class: [
  354. 'v1'
  355. ]
  356. }
  357. } );
  358. const v2 = getView( {
  359. tag: 'span',
  360. attributes: {
  361. class: [
  362. 'v2'
  363. ]
  364. }
  365. } );
  366. const tpl = new Template( {
  367. tag: 'p',
  368. children: [ v1, v2 ]
  369. } );
  370. expect( tpl.children.get( 0 ) ).to.equal( v1 );
  371. expect( tpl.children.get( 1 ) ).to.equal( v2 );
  372. const rendered = tpl.render();
  373. expect( normalizeHtml( rendered.outerHTML ) ).to.equal( '<p><span class="v1"></span><span class="v2"></span></p>' );
  374. // Make sure the child views will not re–render their elements but
  375. // use ones rendered by the template instance above.
  376. expect( v1.element ).to.equal( rendered.firstChild );
  377. expect( v2.element ).to.equal( rendered.lastChild );
  378. } );
  379. it( 'renders view collection', () => {
  380. const collection = new ViewCollection();
  381. const v1 = getView( {
  382. tag: 'span',
  383. attributes: {
  384. class: [
  385. 'v1'
  386. ]
  387. }
  388. } );
  389. const v2 = getView( {
  390. tag: 'span',
  391. attributes: {
  392. class: [
  393. 'v2'
  394. ]
  395. }
  396. } );
  397. collection.add( v1 );
  398. collection.add( v2 );
  399. const rendered = new Template( {
  400. tag: 'p',
  401. children: collection
  402. } ).render();
  403. expect( normalizeHtml( rendered.outerHTML ) ).to.equal( '<p><span class="v1"></span><span class="v2"></span></p>' );
  404. // Make sure the child views will not re–render their elements but
  405. // use ones rendered by the template instance above.
  406. expect( v1.element ).to.equal( rendered.firstChild );
  407. expect( v2.element ).to.equal( rendered.lastChild );
  408. expect( collection._parentElement ).to.equal( rendered );
  409. } );
  410. // #117
  411. it( 'renders template children', () => {
  412. const childTplA = new Template( {
  413. tag: 'a'
  414. } );
  415. const childTplB = new Template( {
  416. tag: 'b'
  417. } );
  418. const view = new View();
  419. view.set( {
  420. foo: 'bar',
  421. bar: 'foo'
  422. } );
  423. const bind = Template.bind( view, view );
  424. // Yes, this TC is crazy in some sort of way. It's all about template
  425. // normalization and deep cloning and the like.
  426. //
  427. // To **really** prove the code is safe there must be a View instance,
  428. // which has some child bound to its attribute and... there must be a
  429. // Template instance (below), which also has a child bound to the same attribute.
  430. //
  431. // I know, the view instance and its template aren't even rendered.
  432. //
  433. // The truth is that madness behind this test case is so deep there are no
  434. // words to explain it. But what actually matters is that it proves the Template
  435. // class is free of "Maximum call stack size exceeded" error in certain
  436. // situations.
  437. view.template = new Template( {
  438. tag: 'span',
  439. children: [
  440. {
  441. text: bind.to( 'bar' )
  442. }
  443. ]
  444. } );
  445. const childTplC = new Template( {
  446. tag: 'i',
  447. children: [
  448. {
  449. text: bind.to( 'bar' )
  450. }
  451. ]
  452. } );
  453. const tpl = new Template( {
  454. tag: 'p',
  455. children: [
  456. childTplA,
  457. childTplB,
  458. childTplC
  459. ]
  460. } );
  461. // Make sure child instances weren't cloned.
  462. expect( tpl.children.get( 0 ) ).to.equal( childTplA );
  463. expect( tpl.children.get( 1 ) ).to.equal( childTplB );
  464. expect( tpl.children.get( 2 ) ).to.equal( childTplC );
  465. expect( normalizeHtml( tpl.render().outerHTML ) ).to.equal(
  466. '<p><a></a><b></b><i>foo</i></p>'
  467. );
  468. } );
  469. } );
  470. describe( 'bindings', () => {
  471. it( 'activates model bindings – root', () => {
  472. const observable = new Model( {
  473. foo: 'bar'
  474. } );
  475. const emitter = Object.create( EmitterMixin );
  476. const bind = Template.bind( observable, emitter );
  477. const el = new Template( {
  478. tag: 'div',
  479. attributes: {
  480. class: bind.to( 'foo' )
  481. }
  482. } ).render();
  483. expect( el.getAttribute( 'class' ) ).to.equal( 'bar' );
  484. observable.foo = 'baz';
  485. expect( el.getAttribute( 'class' ) ).to.equal( 'baz' );
  486. } );
  487. it( 'activates model bindings – children', () => {
  488. const observable = new Model( {
  489. foo: 'bar'
  490. } );
  491. const emitter = Object.create( EmitterMixin );
  492. const bind = Template.bind( observable, emitter );
  493. const el = new Template( {
  494. tag: 'div',
  495. children: [
  496. {
  497. tag: 'span',
  498. children: [
  499. {
  500. text: [
  501. bind.to( 'foo' ),
  502. 'static'
  503. ]
  504. }
  505. ]
  506. }
  507. ]
  508. } ).render();
  509. expect( el.firstChild.textContent ).to.equal( 'bar static' );
  510. observable.foo = 'baz';
  511. expect( el.firstChild.textContent ).to.equal( 'baz static' );
  512. } );
  513. } );
  514. } );
  515. describe( 'apply', () => {
  516. let observable, domEmitter, bind;
  517. beforeEach( () => {
  518. el = document.createElement( 'div' );
  519. text = document.createTextNode( '' );
  520. observable = new Model( {
  521. foo: 'bar',
  522. baz: 'qux'
  523. } );
  524. domEmitter = Object.create( DomEmitterMixin );
  525. bind = Template.bind( observable, domEmitter );
  526. } );
  527. it( 'throws when wrong template definition', () => {
  528. expect( () => {
  529. new Template( {
  530. tag: 'p',
  531. text: 'foo'
  532. } ).apply( el );
  533. } ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
  534. } );
  535. it( 'throws when no HTMLElement passed', () => {
  536. expect( () => {
  537. new Template( {
  538. tag: 'p'
  539. } ).apply();
  540. } ).to.throw( CKEditorError, /ui-template-wrong-node/ );
  541. } );
  542. it( 'accepts empty template definition', () => {
  543. new Template( {} ).apply( el );
  544. new Template( {} ).apply( text );
  545. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div></div>' );
  546. expect( text.textContent ).to.equal( '' );
  547. } );
  548. describe( 'text', () => {
  549. it( 'applies textContent to a Text Node', () => {
  550. new Template( {
  551. text: 'abc'
  552. } ).apply( text );
  553. expect( text.textContent ).to.equal( 'abc' );
  554. } );
  555. it( 'applies new textContent to an existing Text Node of an HTMLElement', () => {
  556. el.textContent = 'bar';
  557. new Template( {
  558. tag: 'div',
  559. children: [ 'foo' ]
  560. } ).apply( el );
  561. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div>foo</div>' );
  562. } );
  563. } );
  564. describe( 'attributes', () => {
  565. it( 'applies attributes to an HTMLElement', () => {
  566. new Template( {
  567. tag: 'div',
  568. attributes: {
  569. 'class': [ 'a', 'b' ],
  570. x: 'bar'
  571. }
  572. } ).apply( el );
  573. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div class="a b" x="bar"></div>' );
  574. } );
  575. it( 'applies attributes and TextContent to a DOM tree', () => {
  576. el.textContent = 'abc';
  577. el.appendChild( document.createElement( 'span' ) );
  578. new Template( {
  579. tag: 'div',
  580. attributes: {
  581. 'class': 'parent'
  582. },
  583. children: [
  584. 'Children:',
  585. {
  586. tag: 'span',
  587. attributes: {
  588. class: 'child'
  589. }
  590. }
  591. ]
  592. } ).apply( el );
  593. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div class="parent">Children:<span class="child"></span></div>' );
  594. } );
  595. } );
  596. describe( 'children', () => {
  597. it( 'doesn\'t apply new child to an HTMLElement – Text Node', () => {
  598. new Template( {
  599. tag: 'div',
  600. children: [ 'foo' ]
  601. } ).apply( el );
  602. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div></div>' );
  603. } );
  604. it( 'doesn\'t apply new child to an HTMLElement – HTMLElement', () => {
  605. new Template( {
  606. tag: 'div',
  607. children: [
  608. {
  609. tag: 'span'
  610. }
  611. ]
  612. } ).apply( el );
  613. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div></div>' );
  614. } );
  615. it( 'doesn\'t apply new child to an HTMLElement – view', () => {
  616. const view = getView( {
  617. tag: 'span',
  618. attributes: {
  619. class: [
  620. 'v1'
  621. ]
  622. }
  623. } );
  624. new Template( {
  625. tag: 'p',
  626. children: [ view ]
  627. } ).apply( el );
  628. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div></div>' );
  629. } );
  630. it( 'doesn\'t apply new child to an HTMLElement – view collection', () => {
  631. const collection = new ViewCollection();
  632. collection.add( getView( {
  633. tag: 'span',
  634. attributes: {
  635. class: [
  636. 'v1'
  637. ]
  638. }
  639. } ) );
  640. new Template( {
  641. tag: 'p',
  642. children: collection
  643. } ).apply( el );
  644. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div></div>' );
  645. expect( collection._parentElement ).to.be.null;
  646. } );
  647. it( 'should work for deep DOM structure', () => {
  648. const childA = document.createElement( 'a' );
  649. const childB = document.createElement( 'b' );
  650. childA.textContent = 'anchor';
  651. childB.textContent = 'bold';
  652. el.appendChild( childA );
  653. el.appendChild( childB );
  654. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div><a>anchor</a><b>bold</b></div>' );
  655. const spy1 = testUtils.sinon.spy();
  656. const spy2 = testUtils.sinon.spy();
  657. const spy3 = testUtils.sinon.spy();
  658. observable.on( 'ku', spy1 );
  659. observable.on( 'kd', spy2 );
  660. observable.on( 'mo', spy3 );
  661. new Template( {
  662. tag: 'div',
  663. children: [
  664. {
  665. tag: 'a',
  666. on: {
  667. keyup: bind.to( 'ku' )
  668. },
  669. attributes: {
  670. class: bind.to( 'foo', val => 'applied-A-' + val ),
  671. id: 'applied-A'
  672. },
  673. children: [ 'Text applied to childA.' ]
  674. },
  675. {
  676. tag: 'b',
  677. on: {
  678. keydown: bind.to( 'kd' )
  679. },
  680. attributes: {
  681. class: bind.to( 'baz', val => 'applied-B-' + val ),
  682. id: 'applied-B'
  683. },
  684. children: [ 'Text applied to childB.' ]
  685. },
  686. 'Text which is not to be applied because it does NOT exist in original element.'
  687. ],
  688. on: {
  689. 'mouseover@a': bind.to( 'mo' )
  690. },
  691. attributes: {
  692. id: bind.to( 'foo', val => val.toUpperCase() ),
  693. class: bind.to( 'baz', val => 'applied-parent-' + val )
  694. }
  695. } ).apply( el );
  696. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div class="applied-parent-qux" id="BAR">' +
  697. '<a class="applied-A-bar" id="applied-A">Text applied to childA.</a>' +
  698. '<b class="applied-B-qux" id="applied-B">Text applied to childB.</b>' +
  699. '</div>' );
  700. observable.foo = 'updated';
  701. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<div class="applied-parent-qux" id="UPDATED">' +
  702. '<a class="applied-A-updated" id="applied-A">Text applied to childA.</a>' +
  703. '<b class="applied-B-qux" id="applied-B">Text applied to childB.</b>' +
  704. '</div>' );
  705. document.body.appendChild( el );
  706. // Test "mouseover@a".
  707. dispatchEvent( el, 'mouseover' );
  708. dispatchEvent( childA, 'mouseover' );
  709. // Test "keyup".
  710. dispatchEvent( childA, 'keyup' );
  711. // Test "keydown".
  712. dispatchEvent( childB, 'keydown' );
  713. sinon.assert.calledOnce( spy1 );
  714. sinon.assert.calledOnce( spy2 );
  715. sinon.assert.calledOnce( spy3 );
  716. } );
  717. } );
  718. } );
  719. describe( 'bind', () => {
  720. it( 'returns object', () => {
  721. expect( Template.bind() ).to.be.an( 'object' );
  722. } );
  723. it( 'provides "to" and "if" interface', () => {
  724. const bind = Template.bind();
  725. expect( bind ).to.have.keys( 'to', 'if' );
  726. expect( bind.to ).to.be.a( 'function' );
  727. expect( bind.if ).to.be.a( 'function' );
  728. } );
  729. describe( 'event', () => {
  730. let observable, domEmitter, bind;
  731. beforeEach( () => {
  732. observable = new Model( {
  733. foo: 'bar',
  734. baz: 'qux'
  735. } );
  736. domEmitter = Object.create( DomEmitterMixin );
  737. bind = Template.bind( observable, domEmitter );
  738. } );
  739. it( 'accepts plain binding', () => {
  740. const spy = testUtils.sinon.spy();
  741. setElement( {
  742. tag: 'p',
  743. on: {
  744. x: bind.to( 'a' ),
  745. }
  746. } );
  747. observable.on( 'a', spy );
  748. dispatchEvent( el, 'x' );
  749. sinon.assert.calledWithExactly( spy,
  750. sinon.match.has( 'name', 'a' ),
  751. sinon.match.has( 'target', el )
  752. );
  753. } );
  754. it( 'accepts an array of event bindings', () => {
  755. const spy1 = testUtils.sinon.spy();
  756. const spy2 = testUtils.sinon.spy();
  757. setElement( {
  758. tag: 'p',
  759. on: {
  760. x: [
  761. bind.to( 'a' ),
  762. bind.to( 'b' )
  763. ]
  764. }
  765. } );
  766. observable.on( 'a', spy1 );
  767. observable.on( 'b', spy2 );
  768. dispatchEvent( el, 'x' );
  769. sinon.assert.calledWithExactly( spy1,
  770. sinon.match.has( 'name', 'a' ),
  771. sinon.match.has( 'target', el )
  772. );
  773. sinon.assert.calledWithExactly( spy2,
  774. sinon.match.has( 'name', 'b' ),
  775. sinon.match.has( 'target', el )
  776. );
  777. } );
  778. it( 'accepts DOM selectors', () => {
  779. const spy1 = testUtils.sinon.spy();
  780. const spy2 = testUtils.sinon.spy();
  781. const spy3 = testUtils.sinon.spy();
  782. setElement( {
  783. tag: 'p',
  784. children: [
  785. {
  786. tag: 'span',
  787. attributes: {
  788. 'class': 'y',
  789. },
  790. on: {
  791. 'test@p': bind.to( 'c' )
  792. }
  793. },
  794. {
  795. tag: 'div',
  796. children: [
  797. {
  798. tag: 'span',
  799. attributes: {
  800. 'class': 'y',
  801. }
  802. }
  803. ],
  804. }
  805. ],
  806. on: {
  807. 'test@.y': bind.to( 'a' ),
  808. 'test@div': bind.to( 'b' )
  809. }
  810. } );
  811. observable.on( 'a', spy1 );
  812. observable.on( 'b', spy2 );
  813. observable.on( 'c', spy3 );
  814. // Test "test@p".
  815. dispatchEvent( el, 'test' );
  816. sinon.assert.callCount( spy1, 0 );
  817. sinon.assert.callCount( spy2, 0 );
  818. sinon.assert.callCount( spy3, 0 );
  819. // Test "test@.y".
  820. dispatchEvent( el.firstChild, 'test' );
  821. expect( spy1.firstCall.calledWithExactly(
  822. sinon.match.has( 'name', 'a' ),
  823. sinon.match.has( 'target', el.firstChild )
  824. ) ).to.be.true;
  825. sinon.assert.callCount( spy2, 0 );
  826. sinon.assert.callCount( spy3, 0 );
  827. // Test "test@div".
  828. dispatchEvent( el.lastChild, 'test' );
  829. sinon.assert.callCount( spy1, 1 );
  830. expect( spy2.firstCall.calledWithExactly(
  831. sinon.match.has( 'name', 'b' ),
  832. sinon.match.has( 'target', el.lastChild )
  833. ) ).to.be.true;
  834. sinon.assert.callCount( spy3, 0 );
  835. // Test "test@.y".
  836. dispatchEvent( el.lastChild.firstChild, 'test' );
  837. expect( spy1.secondCall.calledWithExactly(
  838. sinon.match.has( 'name', 'a' ),
  839. sinon.match.has( 'target', el.lastChild.firstChild )
  840. ) ).to.be.true;
  841. sinon.assert.callCount( spy2, 1 );
  842. sinon.assert.callCount( spy3, 0 );
  843. } );
  844. it( 'accepts function callbacks', () => {
  845. const spy1 = testUtils.sinon.spy();
  846. const spy2 = testUtils.sinon.spy();
  847. setElement( {
  848. tag: 'p',
  849. children: [
  850. {
  851. tag: 'span'
  852. }
  853. ],
  854. on: {
  855. x: bind.to( spy1 ),
  856. 'y@span': [
  857. bind.to( spy2 ),
  858. bind.to( 'c' )
  859. ]
  860. }
  861. } );
  862. dispatchEvent( el, 'x' );
  863. dispatchEvent( el.firstChild, 'y' );
  864. sinon.assert.calledWithExactly( spy1,
  865. sinon.match.has( 'target', el )
  866. );
  867. sinon.assert.calledWithExactly( spy2,
  868. sinon.match.has( 'target', el.firstChild )
  869. );
  870. } );
  871. it( 'supports event delegation', () => {
  872. const spy = testUtils.sinon.spy();
  873. setElement( {
  874. tag: 'p',
  875. children: [
  876. {
  877. tag: 'span'
  878. }
  879. ],
  880. on: {
  881. x: bind.to( 'a' ),
  882. }
  883. } );
  884. observable.on( 'a', spy );
  885. dispatchEvent( el.firstChild, 'x' );
  886. sinon.assert.calledWithExactly( spy,
  887. sinon.match.has( 'name', 'a' ),
  888. sinon.match.has( 'target', el.firstChild )
  889. );
  890. } );
  891. it( 'works for future elements', () => {
  892. const spy = testUtils.sinon.spy();
  893. setElement( {
  894. tag: 'p',
  895. on: {
  896. 'test@div': bind.to( 'a' )
  897. }
  898. } );
  899. observable.on( 'a', spy );
  900. const div = document.createElement( 'div' );
  901. el.appendChild( div );
  902. dispatchEvent( div, 'test' );
  903. sinon.assert.calledWithExactly( spy, sinon.match.has( 'name', 'a' ), sinon.match.has( 'target', div ) );
  904. } );
  905. } );
  906. describe( 'model', () => {
  907. let observable, emitter, bind;
  908. beforeEach( () => {
  909. observable = new Model( {
  910. foo: 'bar',
  911. baz: 'qux'
  912. } );
  913. emitter = Object.create( EmitterMixin );
  914. bind = Template.bind( observable, emitter );
  915. } );
  916. describe( 'to', () => {
  917. it( 'returns an instance of TemplateToBinding', () => {
  918. const spy = testUtils.sinon.spy();
  919. const binding = bind.to( 'foo', spy );
  920. expect( binding ).to.be.instanceof( TemplateToBinding );
  921. expect( spy.called ).to.be.false;
  922. expect( binding ).to.have.keys( [ 'observable', 'eventNameOrFunction', 'emitter', 'attribute', 'callback' ] );
  923. expect( binding.observable ).to.equal( observable );
  924. expect( binding.callback ).to.equal( spy );
  925. expect( binding.attribute ).to.equal( 'foo' );
  926. } );
  927. it( 'allows binding attribute to the observable – simple (HTMLElement attribute)', () => {
  928. setElement( {
  929. tag: 'p',
  930. attributes: {
  931. 'class': bind.to( 'foo' )
  932. },
  933. children: [ 'abc' ]
  934. } );
  935. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="bar">abc</p>' );
  936. observable.foo = 'baz';
  937. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="baz">abc</p>' );
  938. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.be.null;
  939. } );
  940. it( 'allows binding attribute to the observable – simple (Text Node)', () => {
  941. setElement( {
  942. tag: 'p',
  943. children: [
  944. {
  945. text: bind.to( 'foo' )
  946. }
  947. ]
  948. } );
  949. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>bar</p>' );
  950. observable.foo = 'baz';
  951. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>baz</p>' );
  952. } );
  953. it( 'allows binding attribute to the observable – value processing', () => {
  954. const callback = value => value > 0 ? 'positive' : 'negative';
  955. setElement( {
  956. tag: 'p',
  957. attributes: {
  958. 'class': bind.to( 'foo', callback )
  959. },
  960. children: [
  961. {
  962. text: bind.to( 'foo', callback )
  963. }
  964. ]
  965. } );
  966. observable.foo = 3;
  967. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="positive">positive</p>' );
  968. observable.foo = -7;
  969. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="negative">negative</p>' );
  970. } );
  971. it( 'allows binding attribute to the observable – value processing (use Node)', () => {
  972. const callback = ( value, node ) => {
  973. return ( !!node.tagName && value > 0 ) ? 'HTMLElement positive' : '';
  974. };
  975. setElement( {
  976. tag: 'p',
  977. attributes: {
  978. 'class': bind.to( 'foo', callback )
  979. },
  980. children: [
  981. {
  982. text: bind.to( 'foo', callback )
  983. }
  984. ]
  985. } );
  986. observable.foo = 3;
  987. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="HTMLElement positive"></p>' );
  988. observable.foo = -7;
  989. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p></p>' );
  990. } );
  991. it( 'allows binding attribute to the observable – custom callback', () => {
  992. setElement( {
  993. tag: 'p',
  994. attributes: {
  995. 'class': bind.to( 'foo', ( value, el ) => {
  996. el.innerHTML = value;
  997. if ( value == 'changed' ) {
  998. return value;
  999. }
  1000. } )
  1001. }
  1002. } );
  1003. observable.foo = 'moo';
  1004. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>moo</p>' );
  1005. observable.foo = 'changed';
  1006. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="changed">changed</p>' );
  1007. } );
  1008. it( 'allows binding attribute to the observable – array of bindings (HTMLElement attribute)', () => {
  1009. setElement( {
  1010. tag: 'p',
  1011. attributes: {
  1012. 'class': [
  1013. 'ck-class',
  1014. bind.to( 'foo' ),
  1015. bind.to( 'baz' ),
  1016. bind.to( 'foo', value => `foo-is-${value}` ),
  1017. 'ck-end'
  1018. ]
  1019. },
  1020. children: [ 'abc' ]
  1021. } );
  1022. observable.foo = 'a';
  1023. observable.baz = 'b';
  1024. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="ck-class a b foo-is-a ck-end">abc</p>' );
  1025. observable.foo = 'c';
  1026. observable.baz = 'd';
  1027. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="ck-class c d foo-is-c ck-end">abc</p>' );
  1028. } );
  1029. it( 'allows binding attribute to the observable – array of bindings (Text Node)', () => {
  1030. setElement( {
  1031. tag: 'p',
  1032. attributes: {
  1033. },
  1034. children: [
  1035. {
  1036. text: [
  1037. 'ck-class',
  1038. bind.to( 'foo' ),
  1039. bind.to( 'baz' ),
  1040. bind.to( 'foo', value => `foo-is-${value}` ),
  1041. 'ck-end'
  1042. ]
  1043. }
  1044. ]
  1045. } );
  1046. observable.foo = 'a';
  1047. observable.baz = 'b';
  1048. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>ck-class a b foo-is-a ck-end</p>' );
  1049. observable.foo = 'c';
  1050. observable.baz = 'd';
  1051. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>ck-class c d foo-is-c ck-end</p>' );
  1052. } );
  1053. it( 'allows binding attribute to the observable – falsy values', () => {
  1054. setElement( {
  1055. tag: 'p',
  1056. attributes: {
  1057. simple: bind.to( 'foo' ),
  1058. complex: [ null, bind.to( 'foo' ), undefined, false ],
  1059. zero: [ 0, bind.to( 'foo' ) ],
  1060. emptystring: [ '', bind.to( 'foo' ) ]
  1061. },
  1062. children: [ 'abc' ]
  1063. } );
  1064. observable.foo = 'bar';
  1065. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p complex="bar" emptystring="bar" simple="bar" zero="0 bar">abc</p>' );
  1066. observable.foo = 0;
  1067. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p complex="0" emptystring="0" simple="0" zero="0 0">abc</p>' );
  1068. observable.foo = false;
  1069. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p zero="0">abc</p>' );
  1070. observable.foo = null;
  1071. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p zero="0">abc</p>' );
  1072. observable.foo = undefined;
  1073. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p zero="0">abc</p>' );
  1074. observable.foo = '';
  1075. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p zero="0">abc</p>' );
  1076. } );
  1077. it( 'allows binding attribute to the observable – a custom namespace', () => {
  1078. setElement( {
  1079. tag: 'p',
  1080. attributes: {
  1081. class: {
  1082. ns: 'foo',
  1083. value: bind.to( 'foo' )
  1084. },
  1085. custom: {
  1086. ns: 'foo',
  1087. value: [
  1088. bind.to( 'foo' ),
  1089. bind.to( 'baz' )
  1090. ]
  1091. }
  1092. },
  1093. children: [ 'abc' ]
  1094. } );
  1095. observable.foo = 'bar';
  1096. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="bar" custom="bar qux">abc</p>' );
  1097. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  1098. observable.foo = 'baz';
  1099. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="baz" custom="baz qux">abc</p>' );
  1100. expect( el.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
  1101. } );
  1102. } );
  1103. describe( 'if', () => {
  1104. it( 'returns an object which describes the binding', () => {
  1105. const spy = testUtils.sinon.spy();
  1106. const binding = bind.if( 'foo', 'whenTrue', spy );
  1107. expect( binding ).to.be.instanceof( TemplateIfBinding );
  1108. expect( spy.called ).to.be.false;
  1109. expect( binding ).to.have.keys( [ 'observable', 'emitter', 'attribute', 'callback', 'valueIfTrue' ] );
  1110. expect( binding.observable ).to.equal( observable );
  1111. expect( binding.callback ).to.equal( spy );
  1112. expect( binding.attribute ).to.equal( 'foo' );
  1113. expect( binding.valueIfTrue ).to.equal( 'whenTrue' );
  1114. } );
  1115. it( 'allows binding attribute to the observable – presence of an attribute (HTMLElement attribute)', () => {
  1116. setElement( {
  1117. tag: 'p',
  1118. attributes: {
  1119. 'class': bind.if( 'foo' )
  1120. },
  1121. children: [ 'abc' ]
  1122. } );
  1123. observable.foo = 'bar';
  1124. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="true">abc</p>' );
  1125. observable.foo = true;
  1126. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="true">abc</p>' );
  1127. observable.foo = 0;
  1128. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="true">abc</p>' );
  1129. observable.foo = false;
  1130. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1131. observable.foo = null;
  1132. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1133. observable.foo = undefined;
  1134. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1135. observable.foo = '';
  1136. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1137. } );
  1138. // TODO: Is this alright? It makes sense but it's pretty useless. Text Node cannot be
  1139. // removed just like an attribute of some HTMLElement.
  1140. it( 'allows binding attribute to the observable – presence of an attribute (Text Node)', () => {
  1141. setElement( {
  1142. tag: 'p',
  1143. children: [
  1144. {
  1145. text: bind.if( 'foo' )
  1146. }
  1147. ]
  1148. } );
  1149. observable.foo = 'abc';
  1150. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>true</p>' );
  1151. observable.foo = true;
  1152. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>true</p>' );
  1153. observable.foo = 0;
  1154. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>true</p>' );
  1155. observable.foo = false;
  1156. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p></p>' );
  1157. observable.foo = null;
  1158. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p></p>' );
  1159. observable.foo = undefined;
  1160. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p></p>' );
  1161. observable.foo = '';
  1162. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p></p>' );
  1163. } );
  1164. it( 'allows binding attribute to the observable – value of an attribute (HTMLElement attribute)', () => {
  1165. setElement( {
  1166. tag: 'p',
  1167. attributes: {
  1168. 'class': bind.if( 'foo', 'bar' )
  1169. },
  1170. children: [ 'abc' ]
  1171. } );
  1172. observable.foo = 'bar';
  1173. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="bar">abc</p>' );
  1174. observable.foo = true;
  1175. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="bar">abc</p>' );
  1176. observable.foo = 0;
  1177. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="bar">abc</p>' );
  1178. observable.foo = 64;
  1179. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="bar">abc</p>' );
  1180. observable.foo = false;
  1181. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1182. observable.foo = null;
  1183. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1184. observable.foo = undefined;
  1185. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1186. } );
  1187. it( 'allows binding attribute to the observable – value of an attribute (Text Node)', () => {
  1188. setElement( {
  1189. tag: 'p',
  1190. children: [
  1191. {
  1192. text: bind.if( 'foo', 'bar' )
  1193. }
  1194. ]
  1195. } );
  1196. observable.foo = 'bar';
  1197. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>bar</p>' );
  1198. observable.foo = false;
  1199. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p></p>' );
  1200. observable.foo = 64;
  1201. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>bar</p>' );
  1202. } );
  1203. it( 'allows binding attribute to the observable – array of bindings (HTMLElement attribute)', () => {
  1204. setElement( {
  1205. tag: 'p',
  1206. attributes: {
  1207. 'class': [
  1208. 'ck-class',
  1209. bind.if( 'foo', 'foo-set' ),
  1210. bind.if( 'bar', 'bar-not-set', ( value ) => !value ),
  1211. 'ck-end'
  1212. ]
  1213. },
  1214. children: [ 'abc' ]
  1215. } );
  1216. observable.foo = observable.bar = true;
  1217. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="ck-class foo-set ck-end">abc</p>' );
  1218. observable.foo = observable.bar = false;
  1219. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="ck-class bar-not-set ck-end">abc</p>' );
  1220. } );
  1221. it( 'allows binding attribute to the observable – value of an attribute processed by a callback', () => {
  1222. setElement( {
  1223. tag: 'p',
  1224. attributes: {
  1225. 'class': bind.if( 'foo', 'there–is–no–foo', value => !value )
  1226. },
  1227. children: [ 'abc' ]
  1228. } );
  1229. observable.foo = 'bar';
  1230. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1231. observable.foo = false;
  1232. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="there–is–no–foo">abc</p>' );
  1233. observable.foo = 64;
  1234. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1235. } );
  1236. it( 'allows binding attribute to the observable – value of an attribute processed by a callback (use Node)', () => {
  1237. setElement( {
  1238. tag: 'p',
  1239. attributes: {
  1240. 'class': bind.if( 'foo', 'eqls-tag-name', ( value, el ) => el.tagName === value )
  1241. },
  1242. children: [ 'abc' ]
  1243. } );
  1244. observable.foo = 'bar';
  1245. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1246. observable.foo = 'P';
  1247. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="eqls-tag-name">abc</p>' );
  1248. observable.foo = 64;
  1249. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1250. } );
  1251. it( 'allows binding attribute to the observable – falsy values', () => {
  1252. setElement( {
  1253. tag: 'p',
  1254. attributes: {
  1255. 'class': bind.if( 'foo', 'foo-is-set' )
  1256. },
  1257. children: [ 'abc' ]
  1258. } );
  1259. observable.foo = 'bar';
  1260. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="foo-is-set">abc</p>' );
  1261. observable.foo = true;
  1262. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="foo-is-set">abc</p>' );
  1263. observable.foo = 0;
  1264. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p class="foo-is-set">abc</p>' );
  1265. observable.foo = false;
  1266. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1267. observable.foo = null;
  1268. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1269. observable.foo = undefined;
  1270. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1271. observable.foo = '';
  1272. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc</p>' );
  1273. } );
  1274. } );
  1275. it( 'works with Template#apply() – root element', () => {
  1276. new Template( {
  1277. tag: 'div',
  1278. attributes: {
  1279. class: bind.to( 'foo' )
  1280. }
  1281. } ).apply( el );
  1282. expect( el.getAttribute( 'class' ) ).to.equal( 'bar' );
  1283. observable.foo = 'baz';
  1284. expect( el.getAttribute( 'class' ) ).to.equal( 'baz' );
  1285. } );
  1286. it( 'works with Template#apply() – children', () => {
  1287. const el = document.createElement( 'div' );
  1288. const child = document.createElement( 'span' );
  1289. child.textContent = 'foo';
  1290. el.appendChild( child );
  1291. new Template( {
  1292. tag: 'div',
  1293. children: [
  1294. {
  1295. tag: 'span',
  1296. children: [
  1297. {
  1298. text: bind.to( 'foo' )
  1299. }
  1300. ]
  1301. }
  1302. ]
  1303. } ).apply( el );
  1304. expect( child.textContent ).to.equal( 'bar' );
  1305. observable.foo = 'baz';
  1306. expect( child.textContent ).to.equal( 'baz' );
  1307. } );
  1308. } );
  1309. } );
  1310. describe( 'extend', () => {
  1311. let observable, emitter, bind;
  1312. beforeEach( () => {
  1313. observable = new Model( {
  1314. foo: 'bar',
  1315. baz: 'qux'
  1316. } );
  1317. emitter = Object.create( DomEmitterMixin );
  1318. bind = Template.bind( observable, emitter );
  1319. } );
  1320. it( 'does not modify passed definition', () => {
  1321. const def = {
  1322. tag: 'p',
  1323. attributes: {
  1324. a: 'foo',
  1325. }
  1326. };
  1327. const ext = {
  1328. attributes: {
  1329. b: 'bar'
  1330. }
  1331. };
  1332. const tpl = new Template( def );
  1333. Template.extend( tpl, ext );
  1334. expect( ext.attributes.b ).to.equal( 'bar' );
  1335. expect( tpl.attributes.a[ 0 ] ).to.equal( 'foo' );
  1336. expect( tpl.attributes.b[ 0 ] ).to.equal( 'bar' );
  1337. } );
  1338. describe( 'attributes', () => {
  1339. it( 'extends existing - simple', () => {
  1340. extensionTest(
  1341. {
  1342. tag: 'p',
  1343. attributes: {
  1344. a: 'b'
  1345. }
  1346. },
  1347. {
  1348. attributes: {
  1349. a: 'c'
  1350. }
  1351. },
  1352. '<p a="b c"></p>'
  1353. );
  1354. } );
  1355. it( 'extends existing - complex #1', () => {
  1356. extensionTest(
  1357. {
  1358. tag: 'p',
  1359. attributes: {
  1360. a: [ 'b', 'c' ]
  1361. }
  1362. },
  1363. {
  1364. attributes: {
  1365. a: 'd'
  1366. }
  1367. },
  1368. '<p a="b c d"></p>'
  1369. );
  1370. } );
  1371. it( 'extends existing - complex #2', () => {
  1372. extensionTest(
  1373. {
  1374. tag: 'p',
  1375. attributes: {
  1376. a: {
  1377. value: 'b'
  1378. }
  1379. }
  1380. },
  1381. {
  1382. attributes: {
  1383. a: [ 'c', 'd' ]
  1384. }
  1385. },
  1386. '<p a="b c d"></p>'
  1387. );
  1388. } );
  1389. it( 'extends existing - complex #3', () => {
  1390. extensionTest(
  1391. {
  1392. tag: 'p',
  1393. attributes: {
  1394. a: [ 'b' ]
  1395. }
  1396. },
  1397. {
  1398. attributes: {
  1399. a: [ 'c', 'd' ]
  1400. }
  1401. },
  1402. '<p a="b c d"></p>'
  1403. );
  1404. } );
  1405. it( 'extends existing - bindings #1', () => {
  1406. const el = extensionTest(
  1407. {
  1408. tag: 'p',
  1409. attributes: {
  1410. a: bind.to( 'foo' )
  1411. }
  1412. },
  1413. {
  1414. attributes: {
  1415. a: [ 'c', 'd' ]
  1416. }
  1417. },
  1418. '<p a="bar c d"></p>'
  1419. );
  1420. observable.foo = 'baz';
  1421. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p a="baz c d"></p>' );
  1422. } );
  1423. it( 'extends existing - bindings #2', () => {
  1424. const el = extensionTest(
  1425. {
  1426. tag: 'p',
  1427. attributes: {
  1428. a: [ 'b', bind.to( 'foo' ) ]
  1429. }
  1430. },
  1431. {
  1432. attributes: {
  1433. a: [ 'c', bind.to( 'baz' ) ]
  1434. }
  1435. },
  1436. '<p a="b bar c qux"></p>'
  1437. );
  1438. observable.foo = 'abc';
  1439. observable.baz = 'def';
  1440. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p a="b abc c def"></p>' );
  1441. } );
  1442. it( 'creates new - no attributes', () => {
  1443. extensionTest(
  1444. {
  1445. tag: 'p',
  1446. },
  1447. {
  1448. attributes: {
  1449. c: 'd'
  1450. }
  1451. },
  1452. '<p c="d"></p>'
  1453. );
  1454. } );
  1455. it( 'creates new - simple', () => {
  1456. extensionTest(
  1457. {
  1458. tag: 'p',
  1459. attributes: {
  1460. a: 'b'
  1461. }
  1462. },
  1463. {
  1464. attributes: {
  1465. c: 'd'
  1466. }
  1467. },
  1468. '<p a="b" c="d"></p>'
  1469. );
  1470. } );
  1471. it( 'creates new - array', () => {
  1472. extensionTest(
  1473. {
  1474. tag: 'p',
  1475. attributes: {
  1476. a: 'b'
  1477. }
  1478. },
  1479. {
  1480. attributes: {
  1481. c: [ 'd', 'e' ]
  1482. }
  1483. },
  1484. '<p a="b" c="d e"></p>'
  1485. );
  1486. } );
  1487. it( 'creates new - bindings #1', () => {
  1488. const el = extensionTest(
  1489. {
  1490. tag: 'p',
  1491. attributes: {
  1492. a: 'b'
  1493. }
  1494. },
  1495. {
  1496. attributes: {
  1497. c: bind.to( 'foo' )
  1498. }
  1499. },
  1500. '<p a="b" c="bar"></p>'
  1501. );
  1502. observable.foo = 'abc';
  1503. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p a="b" c="abc"></p>' );
  1504. } );
  1505. it( 'creates new - bindings #2', () => {
  1506. const el = extensionTest(
  1507. {
  1508. tag: 'p',
  1509. attributes: {
  1510. a: 'b'
  1511. }
  1512. },
  1513. {
  1514. attributes: {
  1515. c: [ 'd', bind.to( 'foo' ) ]
  1516. }
  1517. },
  1518. '<p a="b" c="d bar"></p>'
  1519. );
  1520. observable.foo = 'abc';
  1521. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p a="b" c="d abc"></p>' );
  1522. } );
  1523. } );
  1524. describe( 'text', () => {
  1525. it( 'extends existing - simple', () => {
  1526. const el = extensionTest(
  1527. {
  1528. tag: 'p',
  1529. children: [
  1530. 'foo'
  1531. ]
  1532. },
  1533. {
  1534. children: [
  1535. 'bar'
  1536. ]
  1537. },
  1538. '<p>foobar</p>'
  1539. );
  1540. expect( el.childNodes ).to.have.length( 1 );
  1541. } );
  1542. it( 'extends existing - complex #1', () => {
  1543. const el = extensionTest(
  1544. {
  1545. tag: 'p',
  1546. children: [
  1547. { text: 'foo' }
  1548. ]
  1549. },
  1550. {
  1551. children: [
  1552. 'bar'
  1553. ]
  1554. },
  1555. '<p>foobar</p>'
  1556. );
  1557. expect( el.childNodes ).to.have.length( 1 );
  1558. } );
  1559. it( 'extends existing - complex #2', () => {
  1560. const el = extensionTest(
  1561. {
  1562. tag: 'p',
  1563. children: [
  1564. { text: 'foo' }
  1565. ]
  1566. },
  1567. {
  1568. children: [
  1569. { text: 'bar' }
  1570. ]
  1571. },
  1572. '<p>foobar</p>'
  1573. );
  1574. expect( el.childNodes ).to.have.length( 1 );
  1575. } );
  1576. it( 'extends existing - bindings #1', () => {
  1577. const el = extensionTest(
  1578. {
  1579. tag: 'p',
  1580. children: [
  1581. { text: bind.to( 'foo' ) }
  1582. ]
  1583. },
  1584. {
  1585. children: [
  1586. 'abc'
  1587. ]
  1588. },
  1589. '<p>bar abc</p>'
  1590. );
  1591. observable.foo = 'asd';
  1592. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>asd abc</p>' );
  1593. expect( el.childNodes ).to.have.length( 1 );
  1594. } );
  1595. it( 'extends existing - bindings #2', () => {
  1596. const el = extensionTest(
  1597. {
  1598. tag: 'p',
  1599. children: [
  1600. 'abc'
  1601. ]
  1602. },
  1603. {
  1604. children: [
  1605. { text: bind.to( 'foo' ) }
  1606. ]
  1607. },
  1608. '<p>abc bar</p>'
  1609. );
  1610. observable.foo = 'asd';
  1611. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>abc asd</p>' );
  1612. expect( el.childNodes ).to.have.length( 1 );
  1613. } );
  1614. it( 'extends existing - bindings #3', () => {
  1615. const el = extensionTest(
  1616. {
  1617. tag: 'p',
  1618. children: [
  1619. { text: bind.to( 'foo' ) },
  1620. 'X'
  1621. ]
  1622. },
  1623. {
  1624. children: [
  1625. { text: bind.to( 'baz' ) },
  1626. 'Y'
  1627. ]
  1628. },
  1629. '<p>bar quxXY</p>'
  1630. );
  1631. observable.foo = 'A';
  1632. observable.baz = 'B';
  1633. expect( normalizeHtml( el.outerHTML ) ).to.equal( '<p>A BXY</p>' );
  1634. expect( el.childNodes ).to.have.length( 2 );
  1635. } );
  1636. } );
  1637. describe( 'children', () => {
  1638. it( 'should throw when the number of children does not correspond', () => {
  1639. expect( () => {
  1640. extensionTest(
  1641. {
  1642. tag: 'p',
  1643. children: [
  1644. 'foo'
  1645. ]
  1646. },
  1647. {
  1648. children: [
  1649. 'foo',
  1650. 'bar'
  1651. ]
  1652. },
  1653. 'it should fail'
  1654. );
  1655. } ).to.throw( CKEditorError, /ui-template-extend-children-mismatch/ );
  1656. } );
  1657. it( 'should throw when no children in target but extending one', () => {
  1658. expect( () => {
  1659. extensionTest(
  1660. {
  1661. tag: 'p',
  1662. },
  1663. {
  1664. children: [
  1665. {
  1666. tag: 'b'
  1667. }
  1668. ]
  1669. },
  1670. 'it should fail'
  1671. );
  1672. } ).to.throw( CKEditorError, /ui-template-extend-children-mismatch/ );
  1673. } );
  1674. it( 'should throw when the number of children does not correspond on some deeper level', () => {
  1675. expect( () => {
  1676. extensionTest(
  1677. {
  1678. tag: 'p',
  1679. children: [
  1680. {
  1681. tag: 'span',
  1682. attributes: {
  1683. class: 'A'
  1684. },
  1685. children: [
  1686. 'A',
  1687. {
  1688. tag: 'span',
  1689. attributes: {
  1690. class: 'AA'
  1691. },
  1692. children: [
  1693. 'AA'
  1694. ]
  1695. }
  1696. ]
  1697. }
  1698. ]
  1699. },
  1700. {
  1701. children: [
  1702. {
  1703. attributes: {
  1704. class: 'B'
  1705. },
  1706. children: [
  1707. 'B'
  1708. ]
  1709. }
  1710. ]
  1711. },
  1712. 'it should fail'
  1713. );
  1714. } ).to.throw( CKEditorError, /ui-template-extend-children-mismatch/ );
  1715. } );
  1716. it( 'extends existing - simple', () => {
  1717. extensionTest(
  1718. {
  1719. tag: 'p',
  1720. children: [
  1721. {
  1722. tag: 'span',
  1723. attributes: {
  1724. class: 'foo'
  1725. }
  1726. }
  1727. ]
  1728. },
  1729. {
  1730. children: [
  1731. {
  1732. tag: 'span',
  1733. attributes: {
  1734. class: 'bar'
  1735. }
  1736. }
  1737. ]
  1738. },
  1739. '<p><span class="foo bar"></span></p>'
  1740. );
  1741. } );
  1742. it( 'extends existing - complex', () => {
  1743. extensionTest(
  1744. {
  1745. tag: 'p',
  1746. children: [
  1747. {
  1748. tag: 'span',
  1749. attributes: {
  1750. class: 'A'
  1751. },
  1752. children: [
  1753. 'A',
  1754. {
  1755. tag: 'span',
  1756. attributes: {
  1757. class: 'AA'
  1758. },
  1759. children: [
  1760. 'AA'
  1761. ]
  1762. }
  1763. ]
  1764. }
  1765. ]
  1766. },
  1767. {
  1768. children: [
  1769. {
  1770. tag: 'span',
  1771. attributes: {
  1772. class: 'B'
  1773. },
  1774. children: [
  1775. 'B',
  1776. {
  1777. tag: 'span',
  1778. attributes: {
  1779. class: 'BB'
  1780. },
  1781. children: [
  1782. 'BB'
  1783. ]
  1784. }
  1785. ]
  1786. }
  1787. ]
  1788. },
  1789. '<p><span class="A B">AB<span class="AA BB">AABB</span></span></p>'
  1790. );
  1791. } );
  1792. it( 'allows extending a particular child', () => {
  1793. const template = new Template( {
  1794. tag: 'p',
  1795. children: [
  1796. {
  1797. tag: 'span',
  1798. attributes: {
  1799. class: 'foo'
  1800. }
  1801. }
  1802. ]
  1803. } );
  1804. Template.extend( template.children.get( 0 ), {
  1805. attributes: {
  1806. class: 'bar'
  1807. }
  1808. } );
  1809. expect( template.render().outerHTML ).to.equal( '<p><span class="foo bar"></span></p>' );
  1810. } );
  1811. it( 'allows extending a particular child – recursively', () => {
  1812. const template = new Template( {
  1813. tag: 'p',
  1814. children: [
  1815. {
  1816. tag: 'span',
  1817. attributes: {
  1818. class: 'A'
  1819. },
  1820. children: [
  1821. 'A',
  1822. {
  1823. tag: 'span',
  1824. attributes: {
  1825. class: 'AA'
  1826. },
  1827. children: [
  1828. 'AA'
  1829. ]
  1830. }
  1831. ]
  1832. }
  1833. ]
  1834. } );
  1835. Template.extend( template.children.get( 0 ), {
  1836. attributes: {
  1837. class: 'B',
  1838. },
  1839. children: [
  1840. 'B',
  1841. {
  1842. attributes: {
  1843. class: 'BB'
  1844. }
  1845. }
  1846. ]
  1847. } );
  1848. expect( template.render().outerHTML ).to.equal( '<p><span class="A B">AB<span class="AA BB">AA</span></span></p>' );
  1849. } );
  1850. } );
  1851. describe( 'listeners', () => {
  1852. it( 'extends existing', () => {
  1853. const spy1 = testUtils.sinon.spy();
  1854. const spy2 = testUtils.sinon.spy();
  1855. const spy3 = testUtils.sinon.spy();
  1856. const spy4 = testUtils.sinon.spy();
  1857. const spy5 = testUtils.sinon.spy();
  1858. observable.on( 'A', spy1 );
  1859. observable.on( 'C', spy2 );
  1860. observable.on( 'B', spy3 );
  1861. observable.on( 'D', spy4 );
  1862. const el = extensionTest(
  1863. {
  1864. tag: 'p',
  1865. children: [
  1866. {
  1867. tag: 'span'
  1868. }
  1869. ],
  1870. on: {
  1871. click: bind.to( 'A' ),
  1872. 'click@span': [
  1873. bind.to( 'B' ),
  1874. bind.to( spy5 )
  1875. ]
  1876. }
  1877. },
  1878. {
  1879. on: {
  1880. click: bind.to( 'C' ),
  1881. 'click@span': bind.to( 'D' )
  1882. }
  1883. },
  1884. '<p><span></span></p>'
  1885. );
  1886. dispatchEvent( el, 'click' );
  1887. expect( spy1.calledOnce ).to.be.true;
  1888. expect( spy2.calledOnce ).to.be.true;
  1889. expect( spy3.called ).to.be.false;
  1890. expect( spy4.called ).to.be.false;
  1891. expect( spy5.called ).to.be.false;
  1892. dispatchEvent( el.firstChild, 'click' );
  1893. expect( spy1.calledTwice ).to.be.true;
  1894. expect( spy2.calledTwice ).to.be.true;
  1895. expect( spy3.calledOnce ).to.be.true;
  1896. expect( spy4.calledOnce ).to.be.true;
  1897. expect( spy5.calledOnce ).to.be.true;
  1898. } );
  1899. it( 'creates new', () => {
  1900. const spy1 = testUtils.sinon.spy();
  1901. const spy2 = testUtils.sinon.spy();
  1902. const spy3 = testUtils.sinon.spy();
  1903. observable.on( 'A', spy1 );
  1904. observable.on( 'B', spy2 );
  1905. const el = extensionTest(
  1906. {
  1907. tag: 'p',
  1908. children: [
  1909. {
  1910. tag: 'span'
  1911. }
  1912. ],
  1913. },
  1914. {
  1915. on: {
  1916. click: bind.to( 'A' ),
  1917. 'click@span': [
  1918. bind.to( 'B' ),
  1919. bind.to( spy3 )
  1920. ]
  1921. }
  1922. },
  1923. '<p><span></span></p>'
  1924. );
  1925. dispatchEvent( el, 'click' );
  1926. expect( spy1.calledOnce ).to.be.true;
  1927. expect( spy2.called ).to.be.false;
  1928. expect( spy3.called ).to.be.false;
  1929. dispatchEvent( el.firstChild, 'click' );
  1930. expect( spy1.calledTwice ).to.be.true;
  1931. expect( spy2.calledOnce ).to.be.true;
  1932. expect( spy3.calledOnce ).to.be.true;
  1933. } );
  1934. } );
  1935. } );
  1936. } );
  1937. function setElement( template ) {
  1938. el = new Template( template ).render();
  1939. document.body.appendChild( el );
  1940. }
  1941. function extensionTest( baseDefinition, extendedDefinition, expectedHtml ) {
  1942. const template = new Template( baseDefinition );
  1943. Template.extend( template, extendedDefinition );
  1944. const el = template.render();
  1945. document.body.appendChild( el );
  1946. expect( normalizeHtml( el.outerHTML ) ).to.equal( expectedHtml );
  1947. return el;
  1948. }
  1949. function dispatchEvent( el, domEvtName ) {
  1950. if ( !el.parentNode ) {
  1951. throw new Error( 'To dispatch an event, element must be in DOM. Otherwise #target is null.' );
  1952. }
  1953. el.dispatchEvent( new Event( domEvtName, {
  1954. bubbles: true
  1955. } ) );
  1956. }
  1957. function getView( def ) {
  1958. const view = new View();
  1959. view.template = new Template( def );
  1960. return view;
  1961. }