view.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document, HTMLElement */
  6. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  7. import View from '../src/view';
  8. import Template from '../src/template';
  9. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  10. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  11. import ViewCollection from '../src/viewcollection';
  12. import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
  13. let TestView, view, childA, childB;
  14. testUtils.createSinonSandbox();
  15. describe( 'View', () => {
  16. describe( 'constructor()', () => {
  17. beforeEach( () => {
  18. setTestViewClass();
  19. setTestViewInstance();
  20. } );
  21. it( 'defines basic view properties', () => {
  22. view = new View();
  23. expect( view.t ).to.be.undefined;
  24. expect( view.locale ).to.be.undefined;
  25. expect( view.isRendered ).to.be.false;
  26. expect( view.template ).to.be.undefined;
  27. expect( view._viewCollections ).to.be.instanceOf( Collection );
  28. expect( view._unboundChildren ).to.be.instanceOf( ViewCollection );
  29. } );
  30. it( 'defines the locale property and the "t" function', () => {
  31. const locale = { t() {} };
  32. view = new View( locale );
  33. expect( view.locale ).to.equal( locale );
  34. expect( view.t ).to.equal( locale.t );
  35. } );
  36. describe( '_viewCollections', () => {
  37. it( 'manages #locale property', () => {
  38. const locale = {
  39. t() {}
  40. };
  41. const view = new View( locale );
  42. const collection = new ViewCollection();
  43. expect( view.locale ).to.equal( locale );
  44. expect( collection.locale ).to.be.undefined;
  45. view._viewCollections.add( collection );
  46. expect( collection.locale ).to.equal( view.locale );
  47. } );
  48. } );
  49. } );
  50. describe( 'createCollection()', () => {
  51. beforeEach( () => {
  52. setTestViewClass();
  53. setTestViewInstance();
  54. } );
  55. it( 'returns an instance of view collection', () => {
  56. expect( view.createCollection() ).to.be.instanceOf( ViewCollection );
  57. } );
  58. it( 'adds a new collection to the #_viewCollections', () => {
  59. expect( view._viewCollections ).to.have.length( 1 );
  60. const collection = view.createCollection();
  61. expect( view._viewCollections ).to.have.length( 2 );
  62. expect( view._viewCollections.get( 1 ) ).to.equal( collection );
  63. } );
  64. } );
  65. describe( 'registerChildren()', () => {
  66. beforeEach( () => {
  67. setTestViewClass();
  68. setTestViewInstance();
  69. } );
  70. it( 'should add a single view to #_unboundChildren', () => {
  71. expect( view._unboundChildren ).to.have.length( 0 );
  72. const child = new View();
  73. view.registerChildren( child );
  74. expect( view._unboundChildren ).to.have.length( 1 );
  75. expect( view._unboundChildren.get( 0 ) ).to.equal( child );
  76. } );
  77. it( 'should support iterables', () => {
  78. expect( view._unboundChildren ).to.have.length( 0 );
  79. view.registerChildren( [ new View(), new View(), new View() ] );
  80. expect( view._unboundChildren ).to.have.length( 3 );
  81. } );
  82. } );
  83. describe( 'deregisterChildren()', () => {
  84. beforeEach( () => {
  85. setTestViewClass();
  86. setTestViewInstance();
  87. } );
  88. it( 'should remove a single view from #_unboundChildren', () => {
  89. const child1 = new View();
  90. const child2 = new View();
  91. view.registerChildren( child1 );
  92. view.registerChildren( child2 );
  93. expect( view._unboundChildren ).to.have.length( 2 );
  94. view.deregisterChildren( child2 );
  95. expect( view._unboundChildren ).to.have.length( 1 );
  96. expect( view._unboundChildren.get( 0 ) ).to.equal( child1 );
  97. } );
  98. it( 'should support iterables', () => {
  99. const child1 = new View();
  100. const child2 = new View();
  101. const child3 = new View();
  102. view.registerChildren( [ child1, child2, child3 ] );
  103. expect( view._unboundChildren ).to.have.length( 3 );
  104. view.deregisterChildren( [ child2, child3 ] );
  105. expect( view._unboundChildren ).to.have.length( 1 );
  106. expect( view._unboundChildren.get( 0 ) ).to.equal( child1 );
  107. } );
  108. } );
  109. describe( 'setTemplate()', () => {
  110. it( 'sets the template', () => {
  111. const view = new View();
  112. const bind = view.bindTemplate;
  113. view.set( 'foo', 'bar' );
  114. view.setTemplate( {
  115. tag: 'div',
  116. attributes: {
  117. class: [
  118. bind.to( 'foo' )
  119. ]
  120. }
  121. } );
  122. view.render();
  123. expect( normalizeHtml( view.element.outerHTML ) ).to.equal( '<div class="bar"></div>' );
  124. } );
  125. } );
  126. describe( 'extendTemplate()', () => {
  127. it( 'extends the template', () => {
  128. const view = new View();
  129. const bind = view.bindTemplate;
  130. view.set( 'foo', 'bar' );
  131. view.setTemplate( {
  132. tag: 'div'
  133. } );
  134. view.extendTemplate( {
  135. attributes: {
  136. class: [
  137. bind.to( 'foo' )
  138. ]
  139. }
  140. } );
  141. view.render();
  142. expect( normalizeHtml( view.element.outerHTML ) ).to.equal( '<div class="bar"></div>' );
  143. } );
  144. } );
  145. describe( 'render()', () => {
  146. it( 'should throw if already rendered', () => {
  147. const view = new View();
  148. view.render();
  149. try {
  150. view.render();
  151. throw new Error( 'This should not be executed.' );
  152. } catch ( err ) {
  153. expect( err ).to.be.instanceof( CKEditorError );
  154. expect( err.message ).to.match( /^ui-view-render-already-rendered:/ );
  155. }
  156. } );
  157. it( 'should set view#isRendered', () => {
  158. const view = new View();
  159. view.setTemplate( {
  160. tag: 'div'
  161. } );
  162. expect( view.isRendered ).to.be.false;
  163. view.render();
  164. expect( view.isRendered ).to.be.true;
  165. } );
  166. } );
  167. describe( 'bind', () => {
  168. beforeEach( () => {
  169. setTestViewClass();
  170. setTestViewInstance();
  171. } );
  172. it( 'returns a shorthand for Template binding', () => {
  173. expect( view.bindTemplate.to ).to.be.a( 'function' );
  174. expect( view.bindTemplate.if ).to.be.a( 'function' );
  175. const binding = view.bindTemplate.to( 'a' );
  176. expect( binding.observable ).to.equal( view );
  177. expect( binding.emitter ).to.equal( view );
  178. } );
  179. } );
  180. describe( 'element', () => {
  181. beforeEach( createViewInstanceWithTemplate );
  182. it( 'invokes out of #template', () => {
  183. setTestViewInstance();
  184. expect( view.element ).to.be.an.instanceof( HTMLElement );
  185. expect( view.element.nodeName ).to.equal( 'A' );
  186. } );
  187. it( 'can be explicitly declared', () => {
  188. class CustomView extends View {
  189. constructor() {
  190. super();
  191. this.element = document.createElement( 'span' );
  192. }
  193. }
  194. view = new CustomView();
  195. expect( view.element ).to.be.an.instanceof( HTMLElement );
  196. } );
  197. it( 'is null when there is no template', () => {
  198. expect( new View().element ).to.be.null;
  199. } );
  200. it( 'registers child views found in the template', () => {
  201. const view = new View();
  202. const viewA = new View();
  203. const viewB = new View();
  204. const viewC = new View();
  205. viewA.setTemplate( { tag: 'a' } );
  206. viewB.setTemplate( { tag: 'b' } );
  207. viewC.setTemplate( { tag: 'c' } );
  208. view.setTemplate( {
  209. tag: 'div',
  210. children: [
  211. viewA,
  212. viewB,
  213. {
  214. tag: 'p',
  215. children: [
  216. viewC
  217. ]
  218. },
  219. {
  220. text: 'foo'
  221. }
  222. ]
  223. } );
  224. expect( view._unboundChildren ).to.have.length( 0 );
  225. view.render();
  226. expect( view._unboundChildren ).to.have.length( 3 );
  227. expect( view._unboundChildren.get( 0 ) ).to.equal( viewA );
  228. expect( view._unboundChildren.get( 1 ) ).to.equal( viewB );
  229. expect( view._unboundChildren.get( 2 ) ).to.equal( viewC );
  230. } );
  231. } );
  232. describe( 'destroy()', () => {
  233. beforeEach( createViewWithChildren );
  234. it( 'can be called multiple times', () => {
  235. expect( () => {
  236. view.destroy();
  237. } ).to.not.throw();
  238. } );
  239. it( 'should not touch the basic properties', () => {
  240. view.destroy();
  241. expect( view.element ).to.be.an.instanceof( HTMLElement );
  242. expect( view.template ).to.be.an.instanceof( Template );
  243. expect( view.locale ).to.be.an( 'object' );
  244. expect( view.locale.t ).to.be.a( 'function' );
  245. expect( view._viewCollections ).to.be.instanceOf( Collection );
  246. expect( view._unboundChildren ).to.be.instanceOf( ViewCollection );
  247. } );
  248. it( 'should not clear the #_unboundChildren', () => {
  249. const cached = view._unboundChildren;
  250. view.registerChildren( [ new View(), new View() ] );
  251. expect( cached ).to.have.length( 4 );
  252. view.destroy();
  253. expect( cached ).to.have.length( 4 );
  254. } );
  255. it( 'should not clear the #_viewCollections', () => {
  256. const cached = view._viewCollections;
  257. expect( cached ).to.have.length( 1 );
  258. view.destroy();
  259. expect( cached ).to.have.length( 1 );
  260. } );
  261. it( 'leaves the #element in DOM', () => {
  262. const elRef = view.element;
  263. const parentEl = document.createElement( 'div' );
  264. parentEl.appendChild( view.element );
  265. view.destroy();
  266. expect( elRef.parentNode ).to.equal( parentEl );
  267. } );
  268. it( 'calls destroy() on all view#_viewCollections', () => {
  269. const collectionA = view.createCollection();
  270. const collectionB = view.createCollection();
  271. const spyA = testUtils.sinon.spy( collectionA, 'destroy' );
  272. const spyB = testUtils.sinon.spy( collectionB, 'destroy' );
  273. view.destroy();
  274. sinon.assert.calledOnce( spyA );
  275. sinon.assert.calledOnce( spyB );
  276. sinon.assert.callOrder( spyA, spyB );
  277. } );
  278. it( 'destroy a template–less view', () => {
  279. view = new View();
  280. expect( () => {
  281. view.destroy();
  282. } ).to.not.throw();
  283. } );
  284. } );
  285. } );
  286. function createViewInstanceWithTemplate() {
  287. setTestViewClass( { tag: 'a' } );
  288. setTestViewInstance();
  289. }
  290. function setTestViewClass( templateDef ) {
  291. TestView = class V extends View {
  292. constructor() {
  293. super();
  294. this.locale = { t() {} };
  295. if ( templateDef ) {
  296. this.setTemplate( templateDef );
  297. }
  298. }
  299. };
  300. }
  301. function setTestViewInstance() {
  302. view = new TestView();
  303. if ( view.template ) {
  304. view.render();
  305. document.body.appendChild( view.element );
  306. }
  307. }
  308. function createViewWithChildren() {
  309. class ChildView extends View {
  310. constructor() {
  311. super();
  312. this.setTemplate( {
  313. tag: 'span'
  314. } );
  315. }
  316. }
  317. childA = new ChildView();
  318. childB = new ChildView();
  319. setTestViewClass( {
  320. tag: 'p',
  321. children: [ childA, childB ]
  322. } );
  323. setTestViewInstance();
  324. }