Przeglądaj źródła

Removed Model binding and listener attaching logic from View class.

Aleksander Nowodzinski 9 lat temu
rodzic
commit
da9310d91f
2 zmienionych plików z 13 dodań i 1270 usunięć
  1. 4 439
      packages/ckeditor5-ui/src/view.js
  2. 9 831
      packages/ckeditor5-ui/tests/view.js

+ 4 - 439
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.
@@ -107,12 +103,6 @@ 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() );
@@ -122,93 +112,6 @@ export default class View {
 		this._element = el;
 	}
 
-	/**
-	 * 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.
 	 *
@@ -298,12 +201,14 @@ 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' } ) );
+	 *		const model = new Model( { divClass: 'my-div' } );
+	 *		const view = new View( model );
+	 *		const bind = Template.bind( model, view )
 	 *
 	 *		view.applyTemplateToElement( element, {
 	 *			attrs: {
 	 *				id: 'first-div',
-	 *				class: view.bindToAttribute( 'divClass' )
+	 *				class: bind.to( 'divClass' )
 	 *			},
 	 *			on: {
 	 *				click: 'elementClicked' // Will be fired by the View instance.
@@ -321,12 +226,6 @@ export default class View {
 	 * @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 );
 	}
 
@@ -381,266 +280,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 +297,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]
- */

+ 9 - 831
packages/ckeditor5-ui/tests/view.js

@@ -10,6 +10,7 @@
 
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import View from '/ckeditor5/ui/view.js';
+import Template from '/ckeditor5/ui/template.js';
 import Region from '/ckeditor5/ui/region.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import Model from '/ckeditor5/ui/model.js';
@@ -237,704 +238,6 @@ describe( 'View', () => {
 		} );
 	} );
 
-	describe( 'attributeBinder', () => {
-		it( 'provides "to" and "if" interface', () => {
-			const bind = view.attributeBinder;
-
-			expect( bind ).to.have.keys( 'to', 'if' );
-			expect( typeof bind.to ).to.equal( 'function' );
-			expect( typeof bind.if ).to.equal( 'function' );
-		} );
-
-		describe( 'to', () => {
-			it( 'returns an object which describes the binding', () => {
-				setTestViewInstance( { a: 1 } );
-
-				const spy = testUtils.sinon.spy();
-				const bind = view.attributeBinder;
-				const binding = bind.to( 'a', spy );
-
-				expect( spy.called ).to.be.false;
-				expect( binding ).to.have.keys( [ 'type', 'model', 'attribute', 'callback' ] );
-				expect( binding.model ).to.equal( view.model );
-				expect( binding.callback ).to.equal( spy );
-				expect( binding.attribute ).to.equal( 'a' );
-			} );
-
-			it( 'allows binding attribute to the model – simple (HTMLElement attribute)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.to( 'foo' )
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'bar' } );
-				expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
-
-				view.model.foo = 'baz';
-				expect( view.element.outerHTML ).to.equal( '<p class="baz">abc</p>' );
-				expect( view.element.attributes.getNamedItem( 'class' ).namespaceURI ).to.be.null;
-			} );
-
-			it( 'allows binding attribute to the model – simple (Text Node)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						children: [
-							{
-								text: bind.to( 'foo' )
-							}
-						]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'bar' } );
-				expect( view.element.outerHTML ).to.equal( '<p>bar</p>' );
-
-				view.model.foo = 'baz';
-				expect( view.element.outerHTML ).to.equal( '<p>baz</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – value processing', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-					const callback = value => value > 0 ? 'positive' : 'negative';
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.to( 'foo', callback )
-						},
-						children: [
-							{
-								text: bind.to( 'foo', callback )
-							}
-						]
-					};
-				} );
-
-				setTestViewInstance( { foo: 3 } );
-				expect( view.element.outerHTML ).to.equal( '<p class="positive">positive</p>' );
-
-				view.model.foo = -7;
-				expect( view.element.outerHTML ).to.equal( '<p class="negative">negative</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – value processing (use Node)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-					const callback = ( value, node ) => {
-						return ( !!node.tagName && value > 0 ) ? 'HTMLElement positive' : '';
-					};
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.to( 'foo', callback )
-						},
-						children: [
-							{
-								text: bind.to( 'foo', callback )
-							}
-						]
-					};
-				} );
-
-				setTestViewInstance( { foo: 3 } );
-				expect( view.element.outerHTML ).to.equal( '<p class="HTMLElement positive"></p>' );
-
-				view.model.foo = -7;
-				expect( view.element.outerHTML ).to.equal( '<p></p>' );
-			} );
-
-			it( 'allows binding attribute to the model – custom callback', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.to( 'foo', ( value, el ) => {
-								el.innerHTML = value;
-
-								if ( value == 'changed' ) {
-									return value;
-								}
-							} )
-						}
-					};
-				} );
-
-				setTestViewInstance( { foo: 'moo' } );
-				expect( view.element.outerHTML ).to.equal( '<p class="undefined">moo</p>' );
-
-				view.model.foo = 'changed';
-				expect( view.element.outerHTML ).to.equal( '<p class="changed">changed</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – array of bindings (HTMLElement attribute)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': [
-								'ck-class',
-								bind.to( 'foo' ),
-								bind.to( 'bar' ),
-								bind.to( 'foo', value => `foo-is-${value}` ),
-								'ck-end'
-							]
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'a', bar: 'b' } );
-				expect( view.element.outerHTML ).to.equal( '<p class="ck-class a b foo-is-a ck-end">abc</p>' );
-
-				view.model.foo = 'c';
-				view.model.bar = 'd';
-				expect( view.element.outerHTML ).to.equal( '<p class="ck-class c d foo-is-c ck-end">abc</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – array of bindings (Text Node)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-						},
-						children: [
-							{
-								text: [
-									'ck-class',
-									bind.to( 'foo' ),
-									bind.to( 'bar' ),
-									bind.to( 'foo', value => `foo-is-${value}` ),
-									'ck-end'
-								]
-							}
-						]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'a', bar: 'b' } );
-				expect( view.element.outerHTML ).to.equal( '<p>ck-class a b foo-is-a ck-end</p>' );
-
-				view.model.foo = 'c';
-				view.model.bar = 'd';
-				expect( view.element.outerHTML ).to.equal( '<p>ck-class c d foo-is-c ck-end</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – falsy values', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.to( 'foo' )
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'bar' } );
-				expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
-
-				view.model.foo = false;
-				expect( view.element.outerHTML ).to.equal( '<p class="false">abc</p>' );
-
-				view.model.foo = null;
-				expect( view.element.outerHTML ).to.equal( '<p class="null">abc</p>' );
-
-				view.model.foo = undefined;
-				expect( view.element.outerHTML ).to.equal( '<p class="undefined">abc</p>' );
-
-				view.model.foo = 0;
-				expect( view.element.outerHTML ).to.equal( '<p class="0">abc</p>' );
-
-				view.model.foo = '';
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – a custom namespace', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': {
-								ns: 'foo',
-								value: bind.to( 'foo' )
-							}
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'bar' } );
-				expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
-				expect( view.element.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
-
-				view.model.foo = 'baz';
-				expect( view.element.outerHTML ).to.equal( '<p class="baz">abc</p>' );
-				expect( view.element.attributes.getNamedItem( 'class' ).namespaceURI ).to.equal( 'foo' );
-			} );
-		} );
-
-		describe( 'if', () => {
-			it( 'returns an object which describes the binding', () => {
-				setTestViewInstance( { a: 1 } );
-
-				const spy = testUtils.sinon.spy();
-				const bind = view.attributeBinder;
-				const binding = bind.if( 'a', 'foo', spy );
-
-				expect( spy.called ).to.be.false;
-				expect( binding ).to.have.keys( [ 'type', 'model', 'attribute', 'callback', 'valueIfTrue' ] );
-				expect( binding.model ).to.equal( view.model );
-				expect( binding.callback ).to.equal( spy );
-				expect( binding.attribute ).to.equal( 'a' );
-				expect( binding.valueIfTrue ).to.equal( 'foo' );
-			} );
-
-			it( 'allows binding attribute to the model – presence of an attribute (HTMLElement attribute)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.if( 'foo' )
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: true } );
-				expect( view.element.outerHTML ).to.equal( '<p class="">abc</p>' );
-
-				view.model.foo = false;
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-
-				view.model.foo = 'bar';
-				expect( view.element.outerHTML ).to.equal( '<p class="">abc</p>' );
-			} );
-
-			// TODO: Is this alright? It makes sense but it's pretty useless. Text Node cannot be
-			// removed just like an attribute of some HTMLElement.
-			it( 'allows binding attribute to the model – presence of an attribute (Text Node)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						children: [
-							{
-								text: bind.if( 'foo' )
-							}
-						]
-					};
-				} );
-
-				setTestViewInstance( { foo: true } );
-				expect( view.element.outerHTML ).to.equal( '<p></p>' );
-
-				view.model.foo = false;
-				expect( view.element.outerHTML ).to.equal( '<p></p>' );
-
-				view.model.foo = 'bar';
-				expect( view.element.outerHTML ).to.equal( '<p></p>' );
-			} );
-
-			it( 'allows binding attribute to the model – value of an attribute (HTMLElement attribute)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.if( 'foo', 'bar' )
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'bar' } );
-				expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
-
-				view.model.foo = false;
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-
-				view.model.foo = 64;
-				expect( view.element.outerHTML ).to.equal( '<p class="bar">abc</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – value of an attribute (Text Node)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						children: [
-							{
-								text: bind.if( 'foo', 'bar' )
-							}
-						]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'bar' } );
-				expect( view.element.outerHTML ).to.equal( '<p>bar</p>' );
-
-				view.model.foo = false;
-				expect( view.element.outerHTML ).to.equal( '<p></p>' );
-
-				view.model.foo = 64;
-				expect( view.element.outerHTML ).to.equal( '<p>bar</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – array of bindings (HTMLElement attribute)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': [
-								'ck-class',
-								bind.if( 'foo', 'foo-set' ),
-								bind.if( 'bar', 'bar-not-set', ( value ) => !value ),
-								'ck-end'
-							]
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: true, bar: true } );
-				expect( view.element.outerHTML ).to.equal( '<p class="ck-class foo-set ck-end">abc</p>' );
-
-				view.model.foo = view.model.bar = false;
-				expect( view.element.outerHTML ).to.equal( '<p class="ck-class bar-not-set ck-end">abc</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – value of an attribute processed by a callback', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.if( 'foo', 'there–is–no–foo', value => !value )
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'bar' } );
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-
-				view.model.foo = false;
-				expect( view.element.outerHTML ).to.equal( '<p class="there–is–no–foo">abc</p>' );
-
-				view.model.foo = 64;
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – value of an attribute processed by a callback (use Node)', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.if( 'foo', 'eqls-tag-name', ( value, el ) => el.tagName === value )
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'bar' } );
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-
-				view.model.foo = 'P';
-				expect( view.element.outerHTML ).to.equal( '<p class="eqls-tag-name">abc</p>' );
-
-				view.model.foo = 64;
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-			} );
-
-			it( 'allows binding attribute to the model – falsy values', () => {
-				setTestViewClass( function() {
-					const bind = this.attributeBinder;
-
-					return {
-						tag: 'p',
-						attributes: {
-							'class': bind.if( 'foo', 'foo-is-set' )
-						},
-						children: [ 'abc' ]
-					};
-				} );
-
-				setTestViewInstance( { foo: 'bar' } );
-				expect( view.element.outerHTML ).to.equal( '<p class="foo-is-set">abc</p>' );
-
-				view.model.foo = false;
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-
-				view.model.foo = null;
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-
-				view.model.foo = undefined;
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-
-				view.model.foo = '';
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-
-				view.model.foo = 0;
-				expect( view.element.outerHTML ).to.equal( '<p>abc</p>' );
-			} );
-		} );
-	} );
-
-	describe( 'on', () => {
-		it( 'accepts plain binding', () => {
-			const spy = testUtils.sinon.spy();
-
-			setTestViewClass( function() {
-				return {
-					tag: 'p',
-					on: {
-						x: 'a',
-					}
-				};
-			} );
-
-			setTestViewInstance();
-
-			view.on( 'a', spy );
-
-			dispatchEvent( view.element, 'x' );
-			sinon.assert.calledWithExactly( spy,
-				sinon.match.has( 'name', 'a' ),
-				sinon.match.has( 'target', view.element )
-			);
-		} );
-
-		it( 'accepts an array of event bindings', () => {
-			const spy1 = testUtils.sinon.spy();
-			const spy2 = testUtils.sinon.spy();
-
-			setTestViewClass( function() {
-				return {
-					tag: 'p',
-					on: {
-						x: [ 'a', 'b' ]
-					}
-				};
-			} );
-
-			setTestViewInstance();
-
-			view.on( 'a', spy1 );
-			view.on( 'b', spy2 );
-
-			dispatchEvent( view.element, 'x' );
-			sinon.assert.calledWithExactly( spy1,
-				sinon.match.has( 'name', 'a' ),
-				sinon.match.has( 'target', view.element )
-			);
-			sinon.assert.calledWithExactly( spy2,
-				sinon.match.has( 'name', 'b' ),
-				sinon.match.has( 'target', view.element )
-			);
-		} );
-
-		it( 'accepts DOM selectors', () => {
-			const spy1 = testUtils.sinon.spy();
-			const spy2 = testUtils.sinon.spy();
-			const spy3 = testUtils.sinon.spy();
-
-			setTestViewClass( function() {
-				return {
-					tag: 'p',
-					children: [
-						{
-							tag: 'span',
-							attributes: {
-								'class': 'y',
-							},
-							on: {
-								'test@p': 'c'
-							}
-						},
-						{
-							tag: 'div',
-							children: [
-								{
-									tag: 'span',
-									attributes: {
-										'class': 'y',
-									}
-								}
-							],
-						}
-					],
-					on: {
-						'test@.y': 'a',
-						'test@div': 'b'
-					}
-				};
-			} );
-
-			setTestViewInstance();
-
-			view.on( 'a', spy1 );
-			view.on( 'b', spy2 );
-			view.on( 'c', spy3 );
-
-			// Test "test@p".
-			dispatchEvent( view.element, 'test' );
-
-			sinon.assert.callCount( spy1, 0 );
-			sinon.assert.callCount( spy2, 0 );
-			sinon.assert.callCount( spy3, 0 );
-
-			// Test "test@.y".
-			dispatchEvent( view.element.firstChild, 'test' );
-
-			expect( spy1.firstCall.calledWithExactly(
-				sinon.match.has( 'name', 'a' ),
-				sinon.match.has( 'target', view.element.firstChild )
-			) ).to.be.true;
-
-			sinon.assert.callCount( spy2, 0 );
-			sinon.assert.callCount( spy3, 0 );
-
-			// Test "test@div".
-			dispatchEvent( view.element.lastChild, 'test' );
-
-			sinon.assert.callCount( spy1, 1 );
-
-			expect( spy2.firstCall.calledWithExactly(
-				sinon.match.has( 'name', 'b' ),
-				sinon.match.has( 'target', view.element.lastChild )
-			) ).to.be.true;
-
-			sinon.assert.callCount( spy3, 0 );
-
-			// Test "test@.y".
-			dispatchEvent( view.element.lastChild.firstChild, 'test' );
-
-			expect( spy1.secondCall.calledWithExactly(
-				sinon.match.has( 'name', 'a' ),
-				sinon.match.has( 'target', view.element.lastChild.firstChild )
-			) ).to.be.true;
-
-			sinon.assert.callCount( spy2, 1 );
-			sinon.assert.callCount( spy3, 0 );
-		} );
-
-		it( 'accepts function callbacks', () => {
-			const spy1 = testUtils.sinon.spy();
-			const spy2 = testUtils.sinon.spy();
-
-			setTestViewClass( function() {
-				return {
-					tag: 'p',
-					children: [
-						{
-							tag: 'span'
-						}
-					],
-					on: {
-						x: spy1,
-						'y@span': [ spy2, 'c' ],
-					}
-				};
-			} );
-
-			setTestViewInstance();
-
-			dispatchEvent( view.element, 'x' );
-			dispatchEvent( view.element.firstChild, 'y' );
-
-			sinon.assert.calledWithExactly( spy1,
-				sinon.match.has( 'target', view.element )
-			);
-
-			sinon.assert.calledWithExactly( spy2,
-				sinon.match.has( 'target', view.element.firstChild )
-			);
-		} );
-
-		it( 'supports event delegation', () => {
-			const spy = testUtils.sinon.spy();
-
-			setTestViewClass( function() {
-				return {
-					tag: 'p',
-					children: [
-						{
-							tag: 'span'
-						}
-					],
-					on: {
-						x: 'a',
-					}
-				};
-			} );
-
-			setTestViewInstance();
-
-			view.on( 'a', spy );
-
-			dispatchEvent( view.element.firstChild, 'x' );
-			sinon.assert.calledWithExactly( spy,
-				sinon.match.has( 'name', 'a' ),
-				sinon.match.has( 'target', view.element.firstChild )
-			);
-		} );
-
-		it( 'works for future elements', () => {
-			const spy = testUtils.sinon.spy();
-
-			setTestViewClass( function() {
-				return {
-					tag: 'p',
-					on: {
-						'test@div': 'a'
-					}
-				};
-			} );
-
-			setTestViewInstance();
-
-			view.on( 'a', spy );
-
-			const div = document.createElement( 'div' );
-			view.element.appendChild( div );
-
-			dispatchEvent( div, 'test' );
-			sinon.assert.calledWithExactly( spy, sinon.match.has( 'name', 'a' ), sinon.match.has( 'target', div ) );
-		} );
-	} );
-
 	describe( 'destroy', () => {
 		beforeEach( createViewInstanceWithTemplate );
 
@@ -973,34 +276,6 @@ describe( 'View', () => {
 			expect( spy.calledOnce ).to.be.true;
 		} );
 
-		it( 'detaches bound model listeners', () => {
-			setTestViewClass( function() {
-				const bind = this.attributeBinder;
-
-				return {
-					tag: 'p',
-					children: [
-						{ text: bind.to( 'foo' ) }
-					]
-				};
-			} );
-
-			setTestViewInstance( { foo: 'bar' } );
-
-			const modelRef = view.model;
-			const elRef = view.element;
-
-			expect( view.element.outerHTML ).to.equal( '<p>bar</p>' );
-
-			modelRef.foo = 'baz';
-			expect( view.element.outerHTML ).to.equal( '<p>baz</p>' );
-
-			view.destroy();
-
-			modelRef.foo = 'abc';
-			expect( elRef.outerHTML ).to.equal( '<p>baz</p>' );
-		} );
-
 		it( 'destroy a template–less view', () => {
 			view = new View();
 
@@ -1016,49 +291,6 @@ describe( 'View', () => {
 			setTestViewInstance( { a: 'foo', b: 42 } );
 		} );
 
-		it( 'should initialize attribute bindings', () => {
-			const el = document.createElement( 'div' );
-			const bind = view.attributeBinder;
-
-			view.applyTemplateToElement( el, {
-				tag: 'div',
-				attributes: {
-					class: bind.to( 'b' ),
-					id: 'foo',
-					checked: 'checked'
-				}
-			} );
-
-			expect( el.outerHTML ).to.equal( '<div class="42" id="foo" checked="checked"></div>' );
-
-			view.model.b = 64;
-
-			expect( el.outerHTML ).to.equal( '<div class="64" id="foo" checked="checked"></div>' );
-		} );
-
-		it( 'should initialize DOM listeners', () => {
-			const el = document.createElement( 'div' );
-			const spy = testUtils.sinon.spy();
-
-			view.applyTemplateToElement( el, {
-				tag: 'div',
-				on: {
-					click: spy
-				}
-			} );
-
-			document.body.appendChild( el );
-
-			dispatchEvent( el, 'click' );
-
-			sinon.assert.calledOnce( spy );
-
-			view.stopListening( el, 'click' );
-			dispatchEvent( el, 'click' );
-
-			sinon.assert.calledOnce( spy );
-		} );
-
 		it( 'should work for deep DOM structure', () => {
 			const el = document.createElement( 'div' );
 			const childA = document.createElement( 'a' );
@@ -1075,7 +307,11 @@ describe( 'View', () => {
 			const spy1 = testUtils.sinon.spy();
 			const spy2 = testUtils.sinon.spy();
 			const spy3 = testUtils.sinon.spy();
-			const bind = view.attributeBinder;
+			const bind = Template.bind( view.model, view );
+
+			view.model.on( 'ku', spy1 );
+			view.model.on( 'kd', spy2 );
+			view.model.on( 'mo', spy3 );
 
 			view.applyTemplateToElement( el, {
 				tag: 'div',
@@ -1083,7 +319,7 @@ describe( 'View', () => {
 					{
 						tag: 'a',
 						on: {
-							keyup: spy2
+							keyup: bind.to( 'ku' )
 						},
 						attributes: {
 							class: bind.to( 'b', b => 'applied-A-' + b ),
@@ -1094,7 +330,7 @@ describe( 'View', () => {
 					{
 						tag: 'b',
 						on: {
-							keydown: spy3
+							keydown: bind.to( 'kd' )
 						},
 						attributes: {
 							class: bind.to( 'b', b => 'applied-B-' + b ),
@@ -1105,7 +341,7 @@ describe( 'View', () => {
 					'Text which is not to be applied because it does NOT exist in original element.'
 				],
 				on: {
-					'mouseover@a': spy1
+					'mouseover@a': bind.to( 'mo' )
 				},
 				attributes: {
 					id: bind.to( 'a', a => a.toUpperCase() ),
@@ -1141,64 +377,6 @@ describe( 'View', () => {
 			sinon.assert.calledOnce( spy2 );
 			sinon.assert.calledOnce( spy3 );
 		} );
-
-		it( 're–uses a template definition object without redundant re–definition of "on" listener attachers', () => {
-			const el1 = document.createElement( 'div' );
-			const el2 = document.createElement( 'div' );
-			const spy = testUtils.sinon.spy();
-			const def = {
-				tag: 'div',
-				on: {
-					click: spy
-				}
-			};
-
-			view.applyTemplateToElement( el1, def );
-			const attacherFn = def.on.click;
-			view.applyTemplateToElement( el2, def );
-
-			// Attacher function didn't change because it's still the same View instance.
-			expect( attacherFn ).to.equal( def.on.click );
-			expect( def.on._listenerView ).to.equal( view );
-
-			document.body.appendChild( el1 );
-			document.body.appendChild( el2 );
-
-			dispatchEvent( el1, 'click' );
-			sinon.assert.calledOnce( spy );
-
-			dispatchEvent( el2, 'click' );
-			sinon.assert.calledTwice( spy );
-		} );
-
-		it( 'shares a template definition between View instances – event listeners', () => {
-			const el = document.createElement( 'div' );
-			const spy = testUtils.sinon.spy();
-			const view1 = new View();
-			const view2 = new View();
-			const def = {
-				tag: 'div',
-				on: {
-					click: spy
-				}
-			};
-
-			document.body.appendChild( el );
-
-			view1.applyTemplateToElement( el, def );
-			expect( def.on._listenerView ).to.equal( view1 );
-
-			dispatchEvent( el, 'click' );
-			sinon.assert.calledOnce( spy );
-
-			// Use another view1 to see if attachers are re–defined.
-			view2.applyTemplateToElement( el, def );
-			expect( def.on._listenerView ).to.equal( view2 );
-
-			dispatchEvent( el, 'click' );
-			// Spy was called for view1 (1st dispatch), then for view1 and view2 (2nd dispatch).
-			sinon.assert.calledThrice( spy );
-		} );
 	} );
 } );