Browse Source

Code refactoring in Template class. Switched to property–oriented templates (previously definition–based) and allowed easier extension of the children through a collection.

Aleksander Nowodzinski 9 years ago
parent
commit
dcdea1d8dc
2 changed files with 211 additions and 187 deletions
  1. 162 148
      packages/ckeditor5-ui/src/template.js
  2. 49 39
      packages/ckeditor5-ui/tests/template.js

+ 162 - 148
packages/ckeditor5-ui/src/template.js

@@ -8,14 +8,16 @@
 import CKEditorError from '../utils/ckeditorerror.js';
 import mix from '../utils/mix.js';
 import EmitterMixin from '../utils/emittermixin.js';
+import Collection from '../utils/collection.js';
 import cloneDeepWith from '../utils/lib/lodash/cloneDeepWith.js';
 import isObject from '../utils/lib/lodash/isObject.js';
 
 const bindToSymbol = Symbol( 'bindTo' );
 const bindIfSymbol = Symbol( 'bindIf' );
+const xhtmlNs = 'http://www.w3.org/1999/xhtml';
 
 /**
- * A basic Template class. It renders DOM HTMLElements from {@link ui.TemplateDefinition} and supports
+ * A basic Template class. It renders DOM HTMLElement or Text from {@link ui.TemplateDefinition} and supports
  * element attributes, children, bindings to {@link utils.ObservableMixin} instances and DOM events
  * propagation. For example:
  *
@@ -36,7 +38,7 @@ const bindIfSymbol = Symbol( 'bindIf' );
  *
  *		<p class="foo" style="background-color: yellow;">A paragraph.</p>
  *
- * See {@link ui.TemplateDefinition} to know more about templates and see complex examples.
+ * See {@link ui.TemplateDefinition} to know more about templates and complex template definitions.
  *
  * @memberOf ui
  */
@@ -47,38 +49,66 @@ export default class Template {
 	 * @param {ui.TemplateDefinition} def The definition of the template.
 	 */
 	constructor( def ) {
-		const defClone = clone( def );
+		Object.assign( this, normalize( clone( def ) ) );
 
-		normalize( defClone );
+		/**
+		 * Tag of this template, i.e. `div`, indicating that the instance will render
+		 * to an HTMLElement.
+		 *
+		 * @member {String} ui.Template#tag
+		 */
+
+		/**
+		 * Text of this template, indicating that the instance will render to a DOM Text.
+		 *
+		 * @member {Array.<String,ui.TemplateValueSchema> ui.Template#text
+		 */
 
 		/**
-		 * Definition of this template.
+		 * Attributes of this template, i.e. `{ id: [ 'ck-id' ] }`, corresponding with
+		 * HTML attributes on HTMLElement.
+		 *
+		 * Note: Only when {@link ui.Template#tag} is defined.
 		 *
-		 * @readonly
-		 * @member {ui.TemplateDefinition} ui.Template#definition
+		 * @member {Object} ui.Template#attributes
+		 */
+
+		/**
+		 * Children of this template; sub–templates. Each one is an independent
+		 * instance of {@link ui.Template}.
+		 *
+		 * Note: Only when {@link ui.Template#tag} is defined.
+		 *
+		 * @member {utils.Collection.<ui.Template>} ui.Template#children
+		 */
+
+		/**
+		 * DOM event listeners of this template.
+		 *
+		 * @member {Object} ui.Template#eventListeners
 		 */
-		this.definition = defClone;
 	}
 
 	/**
-	 * Renders DOM Node using {@link ui.Template#definition}.
+	 * Renders a DOM Node (`HTMLElement` or `Text`) out of the template.
 	 *
 	 * @see ui.Template#apply
 	 *
-	 * @returns {HTMLElement}
+	 * @returns {HTMLElement|Text}
 	 */
 	render() {
-		return this._renderNode( this.definition, undefined, true );
+		return this._renderNode( undefined, true );
 	}
 
 	/**
-	 * Applies template {@link ui.Template#def} to existing DOM tree.
+	 * Applies template {@link ui.Template#def} to existing DOM Node, either `HTMLElement` or `Text`.
+	 *
+	 * **Note:** No new DOM nodes (HTMLElement or Text) will be created.
 	 *
-	 * **Note:** No new DOM nodes (elements, text nodes) will be created.
 	 *		const element = document.createElement( 'div' );
 	 *		const bind = Template.bind( observableInstance, emitterInstance );
 	 *
-	 *		const template = new Template( {
+	 *		new Template( {
 	 *			attrs: {
 	 *				id: 'first-div',
 	 *				class: bind.to( 'divClass' )
@@ -89,14 +119,12 @@ export default class Template {
 	 *			children: [
 	 *				'Div text.'
 	 *			]
-	 *		} );
-	 *
-	 *		template.apply( element );
+	 *		} ).apply( element );
 	 *
 	 *		element.outerHTML == "<div id="first-div" class="my-div">Div text.</div>"
 	 *
 	 * @see ui.Template#render
-	 * @param {Node} element Root element for template to apply.
+	 * @param {Node} element Root element for the template to apply.
 	 */
 	apply( node ) {
 		if ( !node ) {
@@ -108,7 +136,7 @@ export default class Template {
 			throw new CKEditorError( 'ui-template-wrong-node: No DOM Node specified.' );
 		}
 
-		return this._renderNode( this.definition, node );
+		return this._renderNode( node );
 	}
 
 	/**
@@ -257,8 +285,8 @@ export default class Template {
 	 *			]
 	 *		} );
 	 *
-	 *		// Fragment extension.
-	 *		Template.extend( instance.definition.children[ 0 ], {
+	 *		// Child extension.
+	 *		Template.extend( instance.children.get( 0 ), {
 	 *			attributes: {
 	 *				class: 'd'
 	 *			}
@@ -270,48 +298,30 @@ export default class Template {
 	 *			<span class="b c d">Span</span>
 	 *		</p>
 	 *
-	 * @param {ui.Template|ui.TemplateDefinition} instanceOrDef Existing Template instance or definition to be extended.
-	 * @param {ui.TemplateDefinition} extDef An extension to existing instance or definition.
+	 * @param {ui.Template} template Existing Template instance to be extended.
+	 * @param {ui.TemplateDefinition} def An extension to existing an template instance.
 	 */
-	static extend( instanceOrDef, extDef ) {
-		const extDefClone = clone( extDef );
-
-		normalize( extDefClone );
-
-		if ( instanceOrDef instanceof Template ) {
-			extendTemplateDefinition( instanceOrDef.definition, extDefClone );
-		}
-		// Extend a particular child in existing template instance.
-		//
-		//		Template.extend( instance.definition.children[ 0 ], {
-		//			attributes: {
-		//				class: 'd'
-		//			}
-		//		} );
-		//
-		else {
-			extendTemplateDefinition( instanceOrDef, extDefClone );
-		}
+	static extend( template, def ) {
+		extendTemplate( template, normalize( clone( def ) ) );
 	}
 
 	/**
-	 * Renders a DOM Node from definition.
+	 * Renders a DOM Node (either `HTMLElement` or `Text`) out of the template.
 	 *
 	 * @protected
-	 * @param {ui.TemplateDefinition} def Definition of a Node.
-	 * @param {Node} applyNode If specified, template `def` will be applied to existing DOM Node.
+	 * @param {Node} applyNode If specified, this template will be applied to existing DOM Node.
 	 * @param {Boolean} intoFragment If set, children are rendered into DocumentFragment.
 	 * @returns {HTMLElement} A rendered Node.
 	 */
-	_renderNode( def, applyNode, intoFragment ) {
+	_renderNode( applyNode, intoFragment ) {
 		let isInvalid;
 
 		if ( applyNode ) {
 			// When applying, a definition cannot have "tag" and "text" at the same time.
-			isInvalid = def.tag && def.text;
+			isInvalid = this.tag && this.text;
 		} else {
-			// When rendering, a definition must have either "tag" or "text": XOR( def.tag, def.text ).
-			isInvalid = def.tag ? def.text : !def.text;
+			// When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
+			isInvalid = this.tag ? this.text : !this.text;
 		}
 
 		if ( isInvalid ) {
@@ -324,84 +334,80 @@ export default class Template {
 			throw new CKEditorError( 'ui-template-wrong-syntax: Node definition must have either "tag" or "text" when rendering new Node.' );
 		}
 
-		return def.text ?
-			this._renderText( def, applyNode ) : this._renderElement( def, applyNode, intoFragment );
+		return this.text ? this._renderText( applyNode ) : this._renderElement( applyNode, intoFragment );
 	}
 
 	/**
-	 * Renders an HTMLElement from TemplateDefinition.
+	 * Renders an `HTMLElement` out of the template.
 	 *
 	 * @protected
-	 * @param {ui.TemplateDefinition} def Definition of an element.
-	 * @param {HTMLElement} applyElement If specified, template `def` will be applied to existing HTMLElement.
+	 * @param {HTMLElement} applyElement If specified, this template will be applied to existing HTMLElement.
 	 * @param {Boolean} intoFragment If set, children are rendered into DocumentFragment.
 	 * @returns {HTMLElement} A rendered element.
 	 */
-	_renderElement( def, applyElement, intoFragment ) {
+	_renderElement( applyElement, intoFragment ) {
 		const el = applyElement ||
-			document.createElementNS( def.ns || 'http://www.w3.org/1999/xhtml', def.tag );
+			document.createElementNS( this.ns || xhtmlNs, this.tag );
 
-		this._renderElementAttributes( def, el );
+		this._renderAttributes( el );
 
 		// Invoke children recursively.
 		if ( intoFragment ) {
 			const docFragment = document.createDocumentFragment();
 
-			this._renderElementChildren( def, docFragment );
+			this._renderElementChildren( docFragment );
 
 			el.appendChild( docFragment );
 		} else {
-			this._renderElementChildren( def, el, !!applyElement );
+			this._renderElementChildren( el, !!applyElement );
 		}
 
 		// Setup DOM bindings event listeners.
-		this._setUpListeners( def, el );
+		this._setUpListeners( el );
 
 		return el;
 	}
 
 	/**
-	 * Renders a Text from TemplateDefinition or String.
+	 * Renders a `Text` node out of {@link ui.Template#text}.
 	 *
 	 * @protected
-	 * @param {TemplateDefinition|String} def Definition of Text or its value.
-	 * @param {HTMLElement} textNode If specified, template `def` will be applied to existing Text Node.
-	 * @returns {Text} A rendered Text.
+	 * @param {HTMLElement} textNode If specified, this template instance will be applied to an existing Text Node.
+	 * @returns {Text} A rendered Text node in DOM.
 	 */
-	_renderText( valueSchemaOrText, textNode = document.createTextNode( '' ) ) {
+	_renderText( textNode = document.createTextNode( '' ) ) {
 		// Check if this Text Node is bound to Observable. Cases:
 		//		{ text: [ Template.bind( ... ).to( ... ) ] }
 		//		{ text: [ 'foo', Template.bind( ... ).to( ... ), ... ] }
-		if ( hasBinding( valueSchemaOrText.text ) ) {
-			this._bindToObservable( valueSchemaOrText.text, textNode, getTextUpdater( textNode ) );
+		if ( hasBinding( this.text ) ) {
+			this._bindToObservable( this.text, textNode, getTextUpdater( textNode ) );
 		}
 
 		// Simply set text. Cases:
 		// 		{ text: [ 'all', 'are', 'static' ] }
 		// 		{ text: [ 'foo' ] }
 		else {
-			textNode.textContent = valueSchemaOrText.text.join( '' );
+			textNode.textContent = this.text.join( '' );
 		}
 
 		return textNode;
 	}
 
 	/**
-	 * Renders element attributes from definition.
+	 * Renders an `HTMLElement` attributes out of {@link ui.Template#attributes}.
 	 *
 	 * @protected
-	 * @param {ui.TemplateDefinition} def Definition of an element.
-	 * @param {HTMLElement} el Element which is rendered.
+	 * @param {HTMLElement} el Element which attributes are to be rendered.
 	 */
-	_renderElementAttributes( { attributes }, el ) {
+	_renderAttributes( el ) {
 		let attrName, attrValue, attrNs;
 
-		if ( !attributes ) {
+		if ( !this.attributes ) {
 			return;
 		}
 
-		for ( attrName in attributes ) {
-			attrValue = attributes[ attrName ];
+		for ( attrName in this.attributes ) {
+			attrValue = this.attributes[ attrName ];
 
 			// Detect custom namespace:
 			// 		{ class: { ns: 'abc', value: Template.bind( ... ).to( ... ) } }
@@ -451,7 +457,7 @@ export default class Template {
 	}
 
 	/**
-	 * Renders `style` attribute.
+	 * Renders `style` attribute of an `HTMLElement` based on {@link ui.Template#attributes}.
 	 *
 	 * Style attribute is an {Object} with static values:
 	 *
@@ -497,42 +503,39 @@ export default class Template {
 	}
 
 	/**
-	 * Recursively renders element children from definition by
-	 * calling {@link ui.Template#_renderElement}.
+	 * Recursively renders element children from {@link ui.Template#children}.
 	 *
 	 * @protected
-	 * @param {ui.TemplateDefinition} def Definition of an element.
-	 * @param {HTMLElement} el Element which is rendered.
-	 * @param {Boolean} isApply Traverse existing DOM structure only, don't modify DOM.
+	 * @param {HTMLElement} elOrDocFragment Element or DocumentFragment which is being rendered.
+	 * @param {Boolean} shouldApply Traverse existing DOM structure only, don't modify DOM.
 	 */
-	_renderElementChildren( def, el, isApply ) {
-		if ( def.children ) {
-			def.children.forEach( ( childDef, index ) => {
-				if ( isApply ) {
-					this._renderNode( childDef, el.childNodes[ index ] );
-				} else {
-					el.appendChild( this._renderNode( childDef ) );
-				}
-			} );
+	_renderElementChildren( elOrDocFragment, shouldApply ) {
+		let childIndex = 0;
+
+		for ( let template of this.children ) {
+			if ( shouldApply ) {
+				template._renderNode( elOrDocFragment.childNodes[ childIndex++ ] );
+			} else {
+				elOrDocFragment.appendChild( template.render() );
+			}
 		}
 	}
 
 	/**
-	 * Activates element `on` listeners passed in element definition.
+	 * Activates {@link ui.Template#on} listeners on a passed `HTMLElement`.
 	 *
 	 * @protected
-	 * @param {ui.TemplateDefinition} def Definition of an element.
 	 * @param {HTMLElement} el Element which is being rendered.
 	 */
-	_setUpListeners( def, el ) {
-		if ( !def.on ) {
+	_setUpListeners( el ) {
+		if ( !this.eventListeners ) {
 			return;
 		}
 
-		for ( let key in def.on ) {
+		for ( let key in this.eventListeners ) {
 			const [ domEvtName, domSelector ] = key.split( '@' );
 
-			for ( let schemaItem of def.on[ key ] ) {
+			for ( let schemaItem of this.eventListeners[ key ] ) {
 				schemaItem.emitter.listenTo( el, domEvtName, ( evt, domEvt ) => {
 					if ( !domSelector || domEvt.target.matches( domSelector ) ) {
 						if ( typeof schemaItem.eventNameOrFunction == 'function' ) {
@@ -722,9 +725,10 @@ function getStyleUpdater( el, styleName ) {
 // @returns {ui.TemplateDefinition}
 function clone( def ) {
 	const clone = cloneDeepWith( def, value => {
-		// Don't clone Template.bind* bindings because there are references
-		// to Observable and DOMEmitterMixin instances inside, which are external
-		// to the Template.
+		// Don't clone the `Template.bind`* bindings because of the references to Observable
+		// and DOMEmitterMixin instances inside, which would also be traversed and cloned by greedy
+		// cloneDeepWith algorithm. There's no point in cloning Observable/DOMEmitterMixins
+		// along with the definition.
 		if ( value && value.type ) {
 			return value;
 		}
@@ -738,30 +742,42 @@ function clone( def ) {
 // See:
 //  * {@link normalizeAttributes}
 //  * {@link normalizeListeners}
-//  * {@link normalizeTextString}
+//  * {@link normalizePlainTextDefinition}
 //  * {@link normalizeTextDefinition}
 //
 // @param {ui.TemplateDefinition} def
+// @returns {ui.TemplateDefinition} Normalized definition.
 function normalize( def ) {
-	if ( def.text ) {
+	if ( typeof def == 'string' ) {
+		def = normalizePlainTextDefinition( def );
+	} else if ( def.text ) {
 		normalizeTextDefinition( def );
 	}
 
-	if ( def.attributes ) {
-		normalizeAttributes( def.attributes );
-	}
-
 	if ( def.on ) {
-		normalizeListeners( def.on );
+		def.eventListeners = normalizeListeners( def.on );
+
+		// Template mixes EmitterMixin, so delete #on to avoid collision.
+		delete def.on;
 	}
 
-	if ( def.children ) {
-		// Splicing children array inside so no forEach.
-		for ( let i = def.children.length; i--; ) {
-			normalizeTextString( def.children, def.children[ i ], i );
-			normalize( def.children[ i ] );
+	if ( !def.text ) {
+		if ( def.attributes ) {
+			normalizeAttributes( def.attributes );
+		}
+
+		const children = new Collection();
+
+		if ( def.children ) {
+			for ( let child of def.children ) {
+				children.add( new Template( child ) );
+			}
 		}
+
+		def.children = children;
 	}
+
+	return def;
 }
 
 // Normalizes "attributes" section of {@link ui.TemplateDefinition}.
@@ -812,33 +828,29 @@ function normalizeAttributes( attrs ) {
 //		}
 //
 // @param {Object} listeners
+// @returns {Object} Object containing normalized listeners.
 function normalizeListeners( listeners ) {
 	for ( let l in listeners ) {
 		arrayify( listeners, l );
 	}
+
+	return listeners;
 }
 
-// Normalizes "string" text in "children" section of {@link ui.TemplateDefinition}.
+// Normalizes "string" {@link ui.TemplateDefinition}.
 //
-//		children: [
-//			'abc',
-//		]
+//		"foo"
 //
 // becomes
 //
-//		children: [
-//			{ text: [ 'abc' ] },
-//		]
+//		{ text: [ 'foo' ] },
 //
-// @param {Array} children
-// @param {ui.TemplateDefinition} child
-// @param {Number} index
-function normalizeTextString( children, child, index ) {
-	if ( typeof child == 'string' ) {
-		children.splice( index, 1, {
-			text: [ child ]
-		} );
-	}
+// @param {String} def
+// @returns {ui.TemplateDefinition} Normalized template definition.
+function normalizePlainTextDefinition( def ) {
+	return {
+		text: [ def ]
+	};
 }
 
 // Normalizes text {@link ui.TemplateDefinition}.
@@ -924,34 +936,34 @@ function extendObjectValueArray( obj, ext ) {
 	}
 }
 
-// A helper for {@link ui.Template#extend}. Recursively extends {@link ui.Template#definition}
-// with content from another definition. See {@link ui.Template#extend} to learn more.
+// A helper for {@link ui.Template#extend}. Recursively extends {@link ui.Template} instance
+// with content from {ui.TemplateDefinition}. See {@link ui.Template#extend} to learn more.
 //
-// @param {ui.TemplateDefinition} def A base template definition.
-// @param {ui.TemplateDefinition} extDef An extension to existing definition.
-function extendTemplateDefinition( def, extDef ) {
-	if ( extDef.attributes ) {
-		if ( !def.attributes ) {
-			def.attributes = {};
+// @param {ui.Template} def A template instance to be extended.
+// @param {ui.TemplateDefinition} def A definition which is to extend the template instance.
+function extendTemplate( template, def ) {
+	if ( def.attributes ) {
+		if ( !template.attributes ) {
+			template.attributes = {};
 		}
 
-		extendObjectValueArray( def.attributes, extDef.attributes );
+		extendObjectValueArray( template.attributes, def.attributes );
 	}
 
-	if ( extDef.on ) {
-		if ( !def.on ) {
-			def.on = {};
+	if ( def.eventListeners ) {
+		if ( !template.eventListeners ) {
+			template.eventListeners = {};
 		}
 
-		extendObjectValueArray( def.on, extDef.on );
+		extendObjectValueArray( template.eventListeners, def.eventListeners );
 	}
 
-	if ( extDef.text ) {
-		def.text.push( ...extDef.text );
+	if ( def.text ) {
+		template.text.push( ...def.text );
 	}
 
-	if ( extDef.children ) {
-		if ( !def.children || def.children.length != extDef.children.length ) {
+	if ( def.children && def.children.length ) {
+		if ( template.children.length != def.children.length ) {
 			/**
 			 * The number of children in extended definition does not match.
 			 *
@@ -960,9 +972,11 @@ function extendTemplateDefinition( def, extDef ) {
 			throw new CKEditorError( 'ui-template-extend-children-mismatch: The number of children in extended definition does not match.' );
 		}
 
-		extDef.children.forEach( ( extChildDef, index ) => {
-			extendTemplateDefinition( def.children[ index ], extChildDef );
-		} );
+		let childIndex = 0;
+
+		for ( let childDef of def.children ) {
+			extendTemplate( template.children.get( childIndex++ ), childDef );
+		}
 	}
 }
 
@@ -1011,7 +1025,7 @@ function isFalsy( value ) {
  * @property {String} tag
  * @property {Array.<ui.TemplateDefinition>} [children]
  * @property {Object.<String,ui.TemplateValueSchema>} [attributes]
- * @property {String|ui.TemplateValueSchema} [text]
+ * @property {String|ui.TemplateValueSchema|Array.<String|ui.TemplateValueSchema>} [text]
  * @property {Object.<String,ui.TemplateListenerSchema>} [on]
  */
 

+ 49 - 39
packages/ckeditor5-ui/tests/template.js

@@ -12,6 +12,7 @@ import Model from '/ckeditor5/ui/model.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import EmitterMixin from '/ckeditor5/utils/emittermixin.js';
 import DOMEmitterMixin from '/ckeditor5/ui/domemittermixin.js';
+import Collection from '/ckeditor5/utils/collection.js';
 
 testUtils.createSinonSandbox();
 
@@ -19,16 +20,7 @@ let el, text;
 
 describe( 'Template', () => {
 	describe( 'constructor', () => {
-		it( 'accepts template definition', () => {
-			const def = {
-				tag: 'p'
-			};
-
-			expect( new Template( def ).definition ).to.not.equal( def );
-			expect( new Template( def ).definition.tag ).to.equal( 'p' );
-		} );
-
-		it( 'normalizes template definition', () => {
+		it( 'accepts and normalizes the definition', () => {
 			const bind = Template.bind( new Model( {} ), Object.create( DOMEmitterMixin ) );
 			const tpl = new Template( {
 				tag: 'p',
@@ -62,23 +54,41 @@ describe( 'Template', () => {
 				}
 			} );
 
-			const def = tpl.definition;
+			expect( tpl.attributes.a[ 0 ] ).to.equal( 'foo' );
+			expect( tpl.attributes.b[ 0 ] ).to.equal( 'bar' );
+			expect( tpl.attributes.b[ 1 ] ).to.equal( 'baz' );
+			expect( tpl.attributes.c[ 0 ].value[ 0 ].type ).to.be.a( 'symbol' );
+
+			expect( tpl.children ).to.have.length( 4 );
+			expect( tpl.children.get( 0 ).text[ 0 ] ).to.equal( 'content' );
+			expect( tpl.children.get( 1 ).text[ 0 ].type ).to.be.a( 'symbol' );
+			expect( tpl.children.get( 2 ).text[ 0 ] ).to.equal( 'abc' );
+			expect( tpl.children.get( 3 ).text[ 0 ] ).to.equal( 'a' );
+			expect( tpl.children.get( 3 ).text[ 1 ] ).to.equal( 'b' );
+
+			expect( tpl.eventListeners[ 'a@span' ][ 0 ].type ).to.be.a( 'symbol' );
+			expect( tpl.eventListeners[ 'b@span' ][ 0 ].type ).to.be.a( 'symbol' );
+			expect( tpl.eventListeners[ 'c@span' ][ 0 ].type ).to.be.a( 'symbol' );
+			expect( tpl.eventListeners[ 'c@span' ][ 1 ].type ).to.be.a( 'symbol' );
+
+			// Note that Template mixes EmitterMixin.
+			expect( tpl.on ).to.be.a( 'function' );
+			expect( tpl.on[ 'a@span' ] ).to.be.undefined;
+		} );
 
-			expect( def.attributes.a[ 0 ] ).to.equal( 'foo' );
-			expect( def.attributes.b[ 0 ] ).to.equal( 'bar' );
-			expect( def.attributes.b[ 1 ] ).to.equal( 'baz' );
-			expect( def.attributes.c[ 0 ].value[ 0 ].type ).to.be.a( 'symbol' );
+		it( 'defines #children collection', () => {
+			const elementTpl = new Template( {
+				tag: 'p',
+			} );
 
-			expect( def.children[ 0 ].text[ 0 ] ).to.equal( 'content' );
-			expect( def.children[ 1 ].text[ 0 ].type ).to.be.a( 'symbol' );
-			expect( def.children[ 2 ].text[ 0 ] ).to.equal( 'abc' );
-			expect( def.children[ 3 ].text[ 0 ] ).to.equal( 'a' );
-			expect( def.children[ 3 ].text[ 1 ] ).to.equal( 'b' );
+			const textTpl = new Template( {
+				text: 'foo'
+			} );
 
-			expect( def.on[ 'a@span' ][ 0 ].type ).to.be.a( 'symbol' );
-			expect( def.on[ 'b@span' ][ 0 ].type ).to.be.a( 'symbol' );
-			expect( def.on[ 'c@span' ][ 0 ].type ).to.be.a( 'symbol' );
-			expect( def.on[ 'c@span' ][ 1 ].type ).to.be.a( 'symbol' );
+			expect( elementTpl.children ).to.be.instanceof( Collection );
+			expect( elementTpl.children ).to.have.length( 0 );
+			// Text will never have children.
+			expect( textTpl.children ).to.be.undefined;
 		} );
 
 		it( 'does not modify passed definition', () => {
@@ -95,18 +105,19 @@ describe( 'Template', () => {
 			};
 			const tpl = new Template( def );
 
-			expect( def ).to.not.equal( tpl.definition );
-			expect( def.attributes ).to.not.equal( tpl.definition.attributes );
-			expect( def.children ).to.not.equal( tpl.definition.children );
-			expect( def.children[ 0 ] ).to.not.equal( tpl.definition.children[ 0 ] );
-
-			expect( tpl.definition.attributes.a[ 0 ] ).to.equal( 'foo' );
+			expect( def.attributes ).to.not.equal( tpl.attributes );
+			expect( def.children ).to.not.equal( tpl.children );
+			expect( def.children[ 0 ] ).to.not.equal( tpl.children.get( 0 ) );
 			expect( def.attributes.a ).to.equal( 'foo' );
+			expect( def.children[ 0 ].tag ).to.equal( 'span' );
+
+			expect( tpl.attributes.a[ 0 ] ).to.equal( 'foo' );
+			expect( tpl.children.get( 0 ).tag ).to.equal( 'span' );
 		} );
 	} );
 
 	describe( 'render', () => {
-		it( 'throws when wrong template definition', () => {
+		it( 'throws when the template definition is wrong', () => {
 			expect( () => {
 				new Template( {} ).render();
 			} ).to.throw( CKEditorError, /ui-template-wrong-syntax/ );
@@ -1399,11 +1410,10 @@ describe( 'Template', () => {
 
 			Template.extend( tpl, ext );
 
-			expect( def.attributes.a ).to.equal( 'foo' );
 			expect( ext.attributes.b ).to.equal( 'bar' );
 
-			expect( tpl.definition.attributes.a[ 0 ] ).to.equal( 'foo' );
-			expect( tpl.definition.attributes.b[ 0 ] ).to.equal( 'bar' );
+			expect( tpl.attributes.a[ 0 ] ).to.equal( 'foo' );
+			expect( tpl.attributes.b[ 0 ] ).to.equal( 'bar' );
 		} );
 
 		describe( 'attributes', () => {
@@ -1912,7 +1922,7 @@ describe( 'Template', () => {
 					]
 				} );
 
-				Template.extend( template.definition.children[ 0 ], {
+				Template.extend( template.children.get( 0 ), {
 					attributes: {
 						class: 'bar'
 					}
@@ -1946,7 +1956,7 @@ describe( 'Template', () => {
 					]
 				} );
 
-				Template.extend( template.definition.children[ 0 ], {
+				Template.extend( template.children.get( 0 ), {
 					attributes: {
 						class: 'B',
 					},
@@ -2070,10 +2080,10 @@ function setElement( template ) {
 	document.body.appendChild( el );
 }
 
-function extensionTest( base, extension, expectedHtml ) {
-	const template = new Template( base );
+function extensionTest( baseDefinition, extendedDefinition, expectedHtml ) {
+	const template = new Template( baseDefinition );
 
-	Template.extend( template, extension );
+	Template.extend( template, extendedDefinition );
 
 	const el = template.render();