Browse Source

Merge pull request #39 from ckeditor/t/3

T/3: An utility to extend existing templates.
Piotrek Koszuliński 9 years ago
parent
commit
9aed142e8f

+ 5 - 4
packages/ckeditor5-ui/src/editableui/editableuiview.js

@@ -6,6 +6,7 @@
 'use strict';
 
 import View from '../view.js';
+import Template from '../template.js';
 
 /**
  * @memberOf ui.editableUI
@@ -23,13 +24,13 @@ export default class EditableUIView extends View {
 	constructor( model, locale, editableElement ) {
 		super( model, locale );
 
-		const bind = this.attributeBinder;
+		const bind = this.bind;
 
 		if ( editableElement ) {
 			this.element = this.editableElement = editableElement;
 		}
 
-		this.template = {
+		this.template = new Template( {
 			tag: 'div',
 			attributes: {
 				class: [
@@ -38,7 +39,7 @@ export default class EditableUIView extends View {
 				],
 				contenteditable: bind.to( 'isReadOnly', value => !value ),
 			}
-		};
+		} );
 
 		/**
 		 * The element which is the main editable element (usually the one with `contentEditable="true"`).
@@ -54,7 +55,7 @@ export default class EditableUIView extends View {
 	 */
 	init() {
 		if ( this.editableElement ) {
-			this.applyTemplateToElement( this.editableElement, this.template );
+			this.template.apply( this.editableElement );
 		} else {
 			this.editableElement = this.element;
 		}

+ 8 - 6
packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.js

@@ -6,6 +6,7 @@
 'use strict';
 
 import EditableUIView from '../../editableui/editableuiview.js';
+import Template from '../../template.js';
 
 /**
  * The class implementing an inline {@link ui.editableUI.EditableUIView}.
@@ -27,12 +28,13 @@ export default class InlineEditableUIView extends EditableUIView {
 
 		const label = this.t( 'Rich Text Editor, %0', [ this.model.name ] );
 
-		Object.assign( this.template.attributes, {
-			role: 'textbox',
-			'aria-label': label,
-			title: label
+		Template.extend( this.template, {
+			attributes: {
+				role: 'textbox',
+				'aria-label': label,
+				title: label,
+				class: 'ck-editor__editable_inline'
+			}
 		} );
-
-		this.template.attributes.class.push( 'ck-editor__editable_inline' );
 	}
 }

+ 3 - 2
packages/ckeditor5-ui/src/editorui/boxed/boxededitoruiview.js

@@ -7,6 +7,7 @@
 
 import EditorUIView from '../../editorui/editoruiview.js';
 import uid from '../../../utils/uid.js';
+import Template from '../../template.js';
 
 /**
  * Boxed editor UI view.
@@ -27,7 +28,7 @@ export default class BoxedEditorUIView extends EditorUIView {
 		const t = this.t;
 		const ariaLabelUid = uid();
 
-		this.template = {
+		this.template = new Template( {
 			tag: 'div',
 
 			attributes: {
@@ -69,7 +70,7 @@ export default class BoxedEditorUIView extends EditorUIView {
 					}
 				}
 			]
-		};
+		} );
 
 		this.register( 'top', '.ck-editor__top' );
 		this.register( 'main', '.ck-editor__main' );

+ 3 - 2
packages/ckeditor5-ui/src/editorui/editoruiview.js

@@ -6,6 +6,7 @@
 'use strict';
 
 import View from '../view.js';
+import Template from '../template.js';
 
 /**
  * Base class for the editor main views.
@@ -47,11 +48,11 @@ export default class EditorUIView extends View {
 		const bodyElement = document.createElement( 'div' );
 		document.body.appendChild( bodyElement );
 
-		this.applyTemplateToElement( bodyElement, {
+		new Template( {
 			attributes: {
 				class: 'ck-body ck-reset-all'
 			}
-		} );
+		} ).apply( bodyElement );
 
 		this._bodyRegionContainer = bodyElement;
 

+ 3 - 2
packages/ckeditor5-ui/src/iconmanagerview.js

@@ -6,6 +6,7 @@
 'use strict';
 
 import View from './view.js';
+import Template from './template.js';
 
 /**
  * Icon manager view using {@link ui.iconManager.IconManagerModel}.
@@ -18,13 +19,13 @@ export default class IconManagerView extends View {
 	constructor( model, locale ) {
 		super( model, locale );
 
-		this.template = {
+		this.template = new Template( {
 			tag: 'svg',
 			ns: 'http://www.w3.org/2000/svg',
 			attributes: {
 				class: 'ck-icon-manager-sprite'
 			}
-		};
+		} );
 	}
 
 	init() {

File diff suppressed because it is too large
+ 733 - 101
packages/ckeditor5-ui/src/template.js


+ 11 - 479
packages/ckeditor5-ui/src/view.js

@@ -11,10 +11,6 @@ import Template from './template.js';
 import CKEditorError from '../utils/ckeditorerror.js';
 import DOMEmitterMixin from './domemittermixin.js';
 import mix from '../utils/mix.js';
-import isPlainObject from '../utils/lib/lodash/isPlainObject.js';
-
-const bindToSymbol = Symbol( 'bindTo' );
-const bindIfSymbol = Symbol( 'bindIf' );
 
 /**
  * Basic View class.
@@ -63,9 +59,17 @@ export default class View {
 		} );
 
 		/**
+		 * Shorthand for {@link ui.Template#bind}, bound to {@link ui.View#model}
+		 * and {@link ui.View}.
+		 *
+		 * @method ui.View#bind
+		 */
+		this.bind = Template.bind( this.model, this );
+
+		/**
 		 * Template of this view.
 		 *
-		 * @member {Object} ui.View#template
+		 * @member {ui.Template} ui.View#template
 		 */
 
 		/**
@@ -82,13 +86,6 @@ export default class View {
 		 * @private
 		 * @member {HTMLElement} ui.View.#_element
 		 */
-
-		/**
-		 * An instance of Template to generate {@link ui.View#_el}.
-		 *
-		 * @private
-		 * @member {ui.Template} ui.View#_template
-		 */
 	}
 
 	/**
@@ -107,15 +104,7 @@ export default class View {
 			return null;
 		}
 
-		// Prepare pre–defined listeners.
-		this._extendTemplateWithListenerAttachers( this.template );
-
-		// Prepare pre–defined attribute bindings.
-		this._extendTemplateWithModelBinders( this.template );
-
-		this._template = new Template( this.template );
-
-		return ( this._element = this._template.render() );
+		return ( this._element = this.template.render() );
 	}
 
 	set element( el ) {
@@ -123,93 +112,6 @@ export default class View {
 	}
 
 	/**
-	 * And entry point to the interface which allows binding attributes of {@link View#model}
-	 * to the DOM items like HTMLElement attributes or Text Node `textContent`, so their state
-	 * is synchronized with {@link View#model}.
-	 *
-	 * @readonly
-	 * @type ui.ViewModelBinding
-	 */
-	get attributeBinder() {
-		if ( this._attributeBinder ) {
-			return this._attributeBinder;
-		}
-
-		const model = this.model;
-		const binder = {
-			/**
-			 * Binds {@link View#model} to HTMLElement attribute or Text Node `textContent`
-			 * so remains in sync with the Model when it changes.
-			 *
-			 *		this.template = {
-			 *			tag: 'p',
-			 *			attributes: {
-			 *				// class="..." attribute gets bound to this.model.a
-			 *				'class': bind.to( 'a' )
-			 *			},
-			 *			children: [
-			 *				// <p>...</p> gets bound to this.model.b; always `toUpperCase()`.
-			 *				{ text: bind.to( 'b', ( value, node ) => value.toUpperCase() ) }
-			 *			]
-			 *		}
-			 *
-			 * @property {attributeBinder.to}
-			 * @param {String} attribute Name of {@link View#model} used in the binding.
-			 * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
-			 * @return {ui.ViewModelBinding}
-			 */
-			to( attribute, callback ) {
-				return {
-					type: bindToSymbol,
-					model: model,
-					attribute,
-					callback
-				};
-			},
-
-			/**
-			 * Binds {@link View#model} to HTMLElement attribute or Text Node `textContent`
-			 * so remains in sync with the Model when it changes. Unlike {@link View#attributeBinder.to},
-			 * it controls the presence of the attribute/`textContent` depending on the "falseness" of
-			 * {@link View#model} attribute.
-			 *
-			 *		this.template = {
-			 *			tag: 'input',
-			 *			attributes: {
-			 *				// <input checked> this.model.a is not undefined/null/false/''
-			 *				// <input> this.model.a is undefined/null/false
-			 *				checked: bind.if( 'a' )
-			 *			},
-			 *			children: [
-			 *				{
-			 *					// <input>"b-is-not-set"</input> when this.model.b is undefined/null/false/''
-			 *					// <input></input> when this.model.b is not "falsy"
-			 *					text: bind.if( 'b', 'b-is-not-set', ( value, node ) => !value )
-			 *				}
-			 *			]
-			 *		}
-			 *
-			 * @property {attributeBinder.if}
-			 * @param {String} attribute Name of {@link View#model} used in the binding.
-			 * @param {String} [valueIfTrue] Value set when {@link View#model} attribute is not undefined/null/false/''.
-			 * @param {Function} [callback] Allows processing of the value. Accepts `Node` and `value` as arguments.
-			 * @return {ui.ViewModelBinding}
-			 */
-			if( attribute, valueIfTrue, callback ) {
-				return {
-					type: bindIfSymbol,
-					model: model,
-					attribute,
-					valueIfTrue,
-					callback
-				};
-			}
-		};
-
-		return ( this._attributeBinder = binder );
-	}
-
-	/**
 	 * Initializes the view.
 	 *
 	 * Note: {@link ui.Controller} supports if a promise is returned by this method,
@@ -295,42 +197,6 @@ export default class View {
 	}
 
 	/**
-	 * Applies template to existing DOM element in the context of a View.
-	 *
-	 *		const element = document.createElement( 'div' );
-	 *		const view = new View( new Model( { divClass: 'my-div' } ) );
-	 *
-	 *		view.applyTemplateToElement( element, {
-	 *			attrs: {
-	 *				id: 'first-div',
-	 *				class: view.bindToAttribute( 'divClass' )
-	 *			},
-	 *			on: {
-	 *				click: 'elementClicked' // Will be fired by the View instance.
-	 *			}
-	 *			children: [
-	 *				'Div text.'
-	 *			]
-	 *		} );
-	 *
-	 *		element.outerHTML == "<div id="first-div" class="my-div">Div text.</div>"
-	 *
-	 * See: {@link ui.Template#apply}.
-	 *
-	 * @param {DOMElement} element DOM Element to initialize.
-	 * @param {ui.TemplateDefinition} def Template definition to be applied.
-	 */
-	applyTemplateToElement( element, def ) {
-		// Prepare pre–defined listeners.
-		this._extendTemplateWithListenerAttachers( def );
-
-		// Prepare pre–defined attribute bindings.
-		this._extendTemplateWithModelBinders( def );
-
-		new Template( def ).apply( element );
-	}
-
-	/**
 	 * Destroys the view instance. The process includes:
 	 *
 	 * 1. Removal of child views from {@link ui.View#regions}.
@@ -355,7 +221,7 @@ export default class View {
 		}
 
 		this.model = this.regions = this.template = this.locale = this.t = null;
-		this._regionSelectors = this._element = this._template = null;
+		this._regionSelectors = this._element = null;
 	}
 
 	/**
@@ -381,266 +247,6 @@ export default class View {
 			region.init( regionEl );
 		}
 	}
-
-	/**
-	 * For a given event name or callback, returns a function which,
-	 * once executed in a context of an element, attaches native DOM listener
-	 * to the element. The listener executes given callback or fires View's event
-	 * of given name.
-	 *
-	 * @protected
-	 * @param {String|Function} evtNameOrCallback Event name to be fired on View or callback to execute.
-	 * @returns {Function} A listener attacher function to be executed in the context of an element.
-	 */
-	_getDOMListenerAttacher( evtNameOrCallback ) {
-		/**
-		 * Attaches a native DOM listener to given element. The listener executes the
-		 * callback or fires View's event.
-		 *
-		 * Note: If the selector is supplied, it narrows the scope to relevant targets only.
-		 * So instead of
-		 *
-		 *		children: [
-		 *			{ tag: 'span', on: { click: 'foo' } }
-		 *			{ tag: 'span', on: { click: 'foo' } }
-		 *		]
-		 *
-		 * a single, more efficient listener can be attached that uses **event delegation**:
-		 *
-		 *		children: [
-		 *			{ tag: 'span' }
-		 *			{ tag: 'span' }
-		 *		],
-		 *		on: {
-		 *			'click@span': 'foo',
-		 *		}
-		 *
-		 * @param {HTMLElement} el Element, to which the native DOM Event listener is attached.
-		 * @param {String} domEventName The name of native DOM Event.
-		 * @param {String} [selector] If provided, the selector narrows the scope to relevant targets only.
-		 */
-		return ( el, domEvtName, selector ) => {
-			// Use View's listenTo, so the listener is detached, when the View dies.
-			this.listenTo( el, domEvtName, ( evt, domEvt ) => {
-				if ( !selector || domEvt.target.matches( selector ) ) {
-					if ( typeof evtNameOrCallback == 'function' ) {
-						evtNameOrCallback( domEvt );
-					} else {
-						this.fire( evtNameOrCallback, domEvt );
-					}
-				}
-			} );
-		};
-	}
-
-	/**
-	 * For given {@link ui.TemplateValueSchema} found by (@link _extendTemplateWithModelBinders} containing
-	 * {@link ui.ViewModelBinding} it returns a function, which when called by {@link Template#render}
-	 * or {@link Template#apply} activates the binding and sets its initial value.
-	 *
-	 * Note: {@link ui.TemplateValueSchema} can be for HTMLElement attributes or Text Node `textContent`.
-	 *
-	 * @protected
-	 * @param {ui.TemplateValueSchema}
-	 * @return {Function}
-	 */
-	_getModelBinder( valueSchema ) {
-		// Normalize attributes with additional data like namespace:
-		// class: { ns: 'abc', value: [ ... ] }
-		if ( valueSchema.value ) {
-			valueSchema = valueSchema.value;
-		}
-
-		valueSchema = normalizeBinderValueSchema( valueSchema );
-
-		// Assembles the value using {@link ui.TemplateValueSchema} and stores it in a form of
-		// an Array. Each entry of an Array corresponds to one of {@link ui.TemplateValueSchema}
-		// items.
-		//
-		// @private
-		// @param {Node} node
-		// @return {Array}
-		const getBoundValue = ( node ) => {
-			let model, modelValue;
-
-			return valueSchema.map( schemaItem => {
-				model = schemaItem.model;
-
-				if ( model ) {
-					modelValue = model[ schemaItem.attribute ];
-
-					if ( schemaItem.callback ) {
-						modelValue = schemaItem.callback( modelValue, node );
-					}
-
-					if ( schemaItem.type === bindIfSymbol ) {
-						return !!modelValue ? schemaItem.valueIfTrue || true : '';
-					} else {
-						return modelValue;
-					}
-				} else {
-					return schemaItem;
-				}
-			} );
-		};
-
-		/**
-		 * Attaches a listener to {@link View#model}, which updates DOM with a value constructed from
-		 * {@link ui.TemplateValueSchema} when {@link View#model} attribute value changes.
-		 *
-		 * This function is called by {@link Template#render} or {@link Template#apply}.
-		 *
-		 * @param {Node} node DOM Node to be updated when {@link View#model} changes.
-		 * @param {Function} domUpdater A function provided by {@link Template} which updates corresponding
-		 * DOM attribute or `textContent`.
-		 */
-		return ( node, domUpdater ) => {
-			// Check if valueSchema is a single bind.if, like:
-			//		{ class: bind.if( 'foo' ) }
-			const isPlainBindIf = valueSchema.length == 1 && valueSchema[ 0 ].type == bindIfSymbol;
-
-			// A function executed each time bound model attribute changes.
-			const onModelChange = () => {
-				let value = getBoundValue( node );
-				let shouldSet;
-
-				if ( isPlainBindIf ) {
-					value = value[ 0 ];
-					shouldSet = value !== '';
-
-					if ( shouldSet ) {
-						value = value === true ? '' : value;
-					}
-				} else {
-					value = value.reduce( binderValueReducer, '' );
-					shouldSet = value;
-				}
-
-				if ( shouldSet ) {
-					domUpdater.set( value );
-				} else {
-					domUpdater.remove();
-				}
-			};
-
-			valueSchema
-				.filter( schemaItem => schemaItem.model )
-				.forEach( schemaItem => {
-					this.listenTo( schemaItem.model, 'change:' + schemaItem.attribute, onModelChange );
-				} );
-
-			// Set initial values.
-			onModelChange();
-		};
-	}
-
-	/**
-	 * Iterates over "attributes" and "text" properties in {@link TemplateDefinition} and
-	 * locates existing {@link ui.ViewModelBinding} created by {@link ui.View#attributeBinder}.
-	 * Then, for each such a binding, it creates corresponding entry in {@link Template#_modelBinders},
-	 * which can be then activated by {@link Template#render} or {@link Template#apply}.
-	 *
-	 * @protected
-	 * @param {ui.TemplateDefinition} def
-	 */
-	_extendTemplateWithModelBinders( def ) {
-		const attributes = def.attributes;
-		const text = def.text;
-		let binders = def._modelBinders;
-		let attrName, attrValue;
-
-		if ( !binders && isPlainObject( def ) ) {
-			Object.defineProperty( def, '_modelBinders', {
-				enumerable: false,
-				writable: true,
-				value: {
-					attributes: {}
-				}
-			} );
-
-			binders = def._modelBinders;
-		}
-
-		if ( attributes ) {
-			for ( attrName in attributes ) {
-				attrValue = attributes[ attrName ];
-
-				if ( hasModelBinding( attrValue ) ) {
-					binders.attributes[ attrName ] = this._getModelBinder( attrValue );
-				}
-			}
-		}
-
-		if ( text && hasModelBinding( text ) ) {
-			binders.text = this._getModelBinder( text );
-		}
-
-		// Repeat recursively for the children.
-		if ( def.children ) {
-			def.children.forEach( this._extendTemplateWithModelBinders, this );
-		}
-	}
-
-	/**
-	 * Iterates over "on" property in {@link TemplateDefinition} to recursively
-	 * replace each listener declaration with a function which, once executed in a context
-	 * of an element, attaches native DOM listener to that element.
-	 *
-	 * @protected
-	 * @param {ui.TemplateDefinition} def Template definition.
-	 */
-	_extendTemplateWithListenerAttachers( def ) {
-		const on = def.on;
-
-		// Don't create attachers if they're already here or in the context of the same (this) View instance.
-		if ( on && ( !on._listenerAttachers || on._listenerView != this ) ) {
-			let domEvtName, evtNameOrCallback;
-
-			Object.defineProperty( on, '_listenerAttachers', {
-				enumerable: false,
-				writable: true,
-				value: {}
-			} );
-
-			for ( domEvtName in on ) {
-				evtNameOrCallback = on[ domEvtName ];
-
-				// Listeners allow definition with an array:
-				//
-				//    on: {
-				//        'DOMEventName@selector': [ 'event1', callback ],
-				//        'DOMEventName': [ callback, 'event2', 'event3' ]
-				//        ...
-				//    }
-				if ( Array.isArray( evtNameOrCallback ) ) {
-					on._listenerAttachers[ domEvtName ] = on[ domEvtName ].map( this._getDOMListenerAttacher, this );
-				}
-				// Listeners allow definition with a string containing event name:
-				//
-				//    on: {
-				//       'DOMEventName@selector': 'event1',
-				//       'DOMEventName': 'event2'
-				//       ...
-				//    }
-				else {
-					on._listenerAttachers[ domEvtName ] = this._getDOMListenerAttacher( evtNameOrCallback );
-				}
-			}
-
-			// Set this property to be known that these attachers has already been created
-			// in the context of this particular View instance.
-			Object.defineProperty( on, '_listenerView', {
-				enumerable: false,
-				writable: true,
-				value: this
-			} );
-		}
-
-		// Repeat recursively for the children.
-		if ( def.children ) {
-			def.children.forEach( this._extendTemplateWithListenerAttachers, this );
-		}
-	}
 }
 
 mix( View, DOMEmitterMixin );
@@ -658,77 +264,3 @@ const validSelectorTypes = new Set( [ 'string', 'boolean', 'function' ] );
 function isValidRegionSelector( selector ) {
 	return validSelectorTypes.has( typeof selector ) && selector !== false;
 }
-
-/**
- * Normalizes given {@link ui.TemplateValueSchema} it's always in an Array–like format:
- *
- * 		{ attributeName/text: 'bar' } ->
- * 			{ attributeName/text: [ 'bar' ] }
- *
- * 		{ attributeName/text: { model: ..., modelAttributeName: ..., callback: ... } } ->
- * 			{ attributeName/text: [ { model: ..., modelAttributeName: ..., callback: ... } ] }
- *
- * 		{ attributeName/text: [ 'bar', { model: ..., modelAttributeName: ... }, 'baz' ] }
- *
- * @ignore
- * @private
- * @param {ui.TemplateValueSchema} valueSchema
- * @returns {Array}
- */
-function normalizeBinderValueSchema( valueSchema ) {
-	return Array.isArray( valueSchema ) ? valueSchema : [ valueSchema ];
-}
-
-/**
- * Checks whether given {@link ui.TemplateValueSchema} contains a
- * {@link ui.ViewModelBinding}.
- *
- * @ignore
- * @private
- * @param {ui.TemplateValueSchema} valueSchema
- * @returns {Boolean}
- */
-function hasModelBinding( valueSchema ) {
-	// Normalize attributes with additional data like namespace:
-	// class: { ns: 'abc', value: [ ... ] }
-	if ( valueSchema.value ) {
-		valueSchema = valueSchema.value;
-	}
-
-	if ( Array.isArray( valueSchema ) ) {
-		return valueSchema.some( hasModelBinding );
-	} else if ( valueSchema.model ) {
-		return true;
-	}
-
-	return false;
-}
-
-/**
- * A helper which concatenates the value avoiding unwanted
- * leading white spaces.
- *
- * @ignore
- * @private
- * @param {String} prev
- * @param {String} cur
- * @returns {String}
- */
-function binderValueReducer( prev, cur ) {
-	return prev === '' ?
-			`${cur}`
-		:
-			cur === '' ? `${prev}` : `${prev} ${cur}`;
-}
-
-/**
- * Describes Model binding created by {@link View#attributeBinder}.
- *
- * @typedef ui.ViewModelBinding
- * @type Object
- * @property {Symbol} type
- * @property {ui.Model} model
- * @property {String} attribute
- * @property {String} [valueIfTrue]
- * @property {Function} [callback]
- */

+ 2 - 3
packages/ckeditor5-ui/tests/domemittermixin.js

@@ -9,7 +9,6 @@
 'use strict';
 
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
-import extend from '/ckeditor5/utils/lib/lodash/extend.js';
 import DOMEmitterMixin from '/ckeditor5/ui/domemittermixin.js';
 import EmitterMixin from '/ckeditor5/utils/emittermixin.js';
 
@@ -17,8 +16,8 @@ let emitter, domEmitter, node;
 
 testUtils.createSinonSandbox();
 
-const getEmitterInstance = () => extend( {}, EmitterMixin );
-const getDOMEmitterInstance = () => extend( {}, DOMEmitterMixin );
+const getEmitterInstance = () => Object.create( EmitterMixin );
+const getDOMEmitterInstance = () => Object.create( DOMEmitterMixin );
 const getDOMNodeInstance = () => document.createElement( 'div' );
 
 function updateEmitterInstance() {

+ 4 - 1
packages/ckeditor5-ui/tests/editableui/editableuiview.js

@@ -15,7 +15,10 @@ describe( 'EditableUIView', () => {
 	let model, view, editableElement, locale;
 
 	beforeEach( () => {
-		model = new Model( { isReadOnly: false, isFocused: false } );
+		model = new Model( {
+			isReadOnly: false,
+			isFocused: false
+		} );
 		locale = new Locale( 'en' );
 		view = new EditableUIView( model, locale );
 		editableElement = document.createElement( 'div' );

+ 3 - 2
packages/ckeditor5-ui/tests/region.js

@@ -10,6 +10,7 @@
 
 import Region from '/ckeditor5/ui/region.js';
 import View from '/ckeditor5/ui/view.js';
+import Template from '/ckeditor5/ui/template.js';
 
 let TestViewA, TestViewB;
 let region, el;
@@ -121,14 +122,14 @@ function createRegionInstance() {
 	class A extends View {
 		constructor() {
 			super();
-			this.template = { tag: 'a' };
+			this.template = new Template( { tag: 'a' } );
 		}
 	}
 
 	class B extends View {
 		constructor() {
 			super();
-			this.template = { tag: 'b' };
+			this.template = new Template( { tag: 'b' } );
 		}
 	}
 

File diff suppressed because it is too large
+ 1601 - 228
packages/ckeditor5-ui/tests/template.js


File diff suppressed because it is too large
+ 20 - 936
packages/ckeditor5-ui/tests/view.js