Browse Source

Core refactoring in ui/view and ui/template (with tests).

Aleksander Nowodzinski 10 years ago
parent
commit
b724085bed

+ 8 - 5
packages/ckeditor5-ui/src/ui/template.js

@@ -39,8 +39,11 @@ CKEDITOR.define( function() {
 		}
 	}
 
-	var textUpdater = () => ( el, value ) => el.innerHTML = value;
-	var attributeUpdater = ( attr ) => ( el, value ) => el.setAttribute( attr, value );
+	var getTextUpdater = () =>
+		( el, value ) => el.innerHTML = value;
+
+	var getAttributeUpdater = ( attr ) =>
+		( el, value ) => el.setAttribute( attr, value );
 
 	function renderElement( def ) {
 		if ( !def ) {
@@ -64,7 +67,7 @@ CKEDITOR.define( function() {
 	function renderElementText( def, el ) {
 		if ( def.text ) {
 			if ( typeof def.text == 'function' ) {
-				def.text( el, textUpdater() );
+				def.text( el, getTextUpdater() );
 			} else {
 				el.innerHTML = def.text;
 			}
@@ -80,7 +83,7 @@ CKEDITOR.define( function() {
 
 			// Attribute bound directly to the model.
 			if ( typeof value == 'function' ) {
-				value( el, attributeUpdater( attr ) );
+				value( el, getAttributeUpdater( attr ) );
 			}
 
 			// Explicit attribute definition (string).
@@ -90,7 +93,7 @@ CKEDITOR.define( function() {
 					value = value.join( ' ' );
 				}
 
-				attributeUpdater( attr )( el, value );
+				el.setAttribute( attr, value );
 			}
 		}
 	}

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

@@ -16,8 +16,10 @@ CKEDITOR.define( [
 	'collection',
 	'model',
 	'ui/template',
-	'ckeditorerror'
-], function( Collection, Model, Template, CKEditorError ) {
+	'ckeditorerror',
+	'ui/domemittermixin',
+	'utils'
+], function( Collection, Model, Template, CKEditorError, DOMEmitterMixin, utils ) {
 	class View extends Model {
 		/**
 		 * Creates an instance of the {@link View} class.
@@ -77,23 +79,23 @@ CKEDITOR.define( [
 		bind( property, callback ) {
 			var model = this.model;
 
-			return function( el, updater ) {
+			return function attachModelListener( el, domUpdater ) {
 				// TODO: Use ES6 default arguments syntax.
-				var changeCallback = callback || updater;
+				callback = callback || domUpdater;
 
-				function executeCallback( el, value ) {
-					var result = changeCallback( el, value );
+				var listenerCallback = ( el, value ) => {
+					var processedValue = callback( el, value );
 
-					if ( typeof result != 'undefined' ) {
-						updater( el, result );
+					if ( typeof processedValue != 'undefined' ) {
+						domUpdater( el, processedValue );
 					}
-				}
+				};
 
 				// Execute callback when the property changes.
-				model.on( 'change:' + property, ( evt, value ) => executeCallback( el, value ) );
+				model.on( 'change:' + property, ( evt, value ) => listenerCallback( el, value ) );
 
 				// Set the initial state of the view.
-				executeCallback( el, model[ property ] );
+				listenerCallback( el, model[ property ] );
 			};
 		}
 
@@ -142,5 +144,7 @@ CKEDITOR.define( [
 		}
 	}
 
+	utils.extend( View.prototype, DOMEmitterMixin );
+
 	return View;
 } );

+ 81 - 73
packages/ckeditor5-ui/tests/ui/template.js

@@ -4,21 +4,17 @@
  */
 
 /* bender-tags: core, ui */
-/* global document, HTMLElement */
+/* global HTMLElement */
 
 'use strict';
 
 var modules = bender.amd.require( 'ckeditor', 'ui/view', 'ui/template' );
+var Template;
 
-describe( 'Template', function() {
-	var View;
-	var Template;
-
-	beforeEach( 'Create a test view instance', function() {
-		View = modules[ 'ui/view' ];
-		Template = modules[ 'ui/template' ];
-	} );
+bender.tools.createSinonSandbox();
+beforeEach( createClassReferences );
 
+describe( 'constructor', function() {
 	it( 'accepts the definition', function() {
 		var def = {
 			tag: 'p'
@@ -26,8 +22,10 @@ describe( 'Template', function() {
 
 		expect( new Template( def ).def ).to.equal( def );
 	} );
+} );
 
-	it( 'renders the element', function() {
+describe( 'render', function() {
+	it( 'creates an element', function() {
 		var el = new Template( {
 			tag: 'p',
 			attributes: {
@@ -40,10 +38,10 @@ describe( 'Template', function() {
 		expect( el ).to.be.instanceof( HTMLElement );
 		expect( el.parentNode ).to.be.null();
 
-		expect( getOuterHtml( el ) ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
+		expect( el.outerHTML ).to.be.equal( '<p class="a b" x="bar">foo</p>' );
 	} );
 
-	it( 'renders element\'s children', function() {
+	it( 'creates element\'s children', function() {
 		var el = new Template( {
 			tag: 'p',
 			attributes: {
@@ -67,103 +65,113 @@ describe( 'Template', function() {
 			]
 		} ).render();
 
-		expect( getOuterHtml( el ) ).to.be.equal( '<p a="A"><b>B</b><i>C<b>D</b></i></p>' );
+		expect( el.outerHTML ).to.be.equal( '<p a="A"><b>B</b><i>C<b>D</b></i></p>' );
 	} );
+} );
 
-	it( 'binds to the model', function() {
-		var view = new View( {
-			foo: 'bar'
-		} );
+describe( 'callback value', function() {
+	it( 'works for attributes', function() {
+		var spy1 = bender.sinon.spy();
+		var spy2 = bender.sinon.spy();
 
 		var el = new Template( {
 			tag: 'p',
 			attributes: {
-				'class': view.bind( 'foo' )
+				'class': spy1
 			},
-			text: view.bind( 'foo' )
+			children: [
+				{
+					tag: 'span',
+					attributes: {
+						id: spy2
+					}
+				}
+			]
 		} ).render();
 
-		expect( getOuterHtml( el ) ).to.be.equal( '<p class="bar">bar</p>' );
+		sinon.assert.calledWithExactly( spy1, el, sinon.match.func );
+		sinon.assert.calledWithExactly( spy2, el.firstChild, sinon.match.func );
 
-		view.model.foo = 'baz';
-		expect( getOuterHtml( el ) ).to.be.equal( '<p class="baz">baz</p>' );
-	} );
+		spy1.firstCall.args[ 1 ]( el, 'foo' );
+		spy2.firstCall.args[ 1 ]( el.firstChild, 'bar' );
 
-	it( 'binds to the model and processes the property', function() {
-		var view = new View( {
-			foo: 7
-		} );
+		expect( el.outerHTML ).to.be.equal( '<p class="foo"><span id="bar"></span></p>' );
+	} );
 
-		var callback = ( el, value ) => ( value > 0 ? 'positive' : 'negative' );
+	it( 'works for "text" property', function() {
+		var spy1 = bender.sinon.spy();
+		var spy2 = bender.sinon.spy();
 
 		var el = new Template( {
 			tag: 'p',
-			attributes: {
-				'class': view.bind( 'foo', callback )
-			},
-			text: view.bind( 'foo', callback )
+			text: spy1,
+			children: [
+				{
+					tag: 'span',
+					text: spy2
+				}
+			]
 		} ).render();
 
-		expect( getOuterHtml( el ) ).to.be.equal( '<p class="positive">positive</p>' );
-
-		view.model.foo = -7;
-		expect( getOuterHtml( el ) ).to.be.equal( '<p class="negative">negative</p>' );
-	} );
-
-	it( 'binds to the model and executes custom action', function() {
-		var view = new View( {
-			foo: 'moo'
-		} );
+		sinon.assert.calledWithExactly( spy1, el, sinon.match.func );
+		sinon.assert.calledWithExactly( spy2, el.firstChild, sinon.match.func );
 
-		var callback = ( el, value ) => {
-			el.innerHTML = value;
+		spy2.firstCall.args[ 1 ]( el.firstChild, 'bar' );
+		expect( el.outerHTML ).to.be.equal( '<p><span>bar</span></p>' );
 
-			if ( value == 'changed' ) {
-				return value;
-			}
-		};
+		spy1.firstCall.args[ 1 ]( el, 'foo' );
+		expect( el.outerHTML ).to.be.equal( '<p>foo</p>' );
+	} );
+} );
 
+describe( 'listeners', function() {
+	it( 'accept plain definitions', function() {
 		var el = new Template( {
 			tag: 'p',
-			attributes: {
-				'class': view.bind( 'foo', callback )
-			},
-			text: 'bar'
+			listeners: {
+				x: 'a',
+				y: [ 'b', 'c' ],
+			}
 		} ).render();
 
-		expect( getOuterHtml( el ) ).to.be.equal( '<p>moo</p>' );
-
-		view.model.foo = 'changed';
-		expect( getOuterHtml( el ) ).to.be.equal( '<p class="changed">changed</p>' );
+		el.dispatchEvent( new Event( 'x' ) );
+		el.dispatchEvent( new Event( 'y' ) );
 	} );
 
-	it( 'binds to the model and updates element\'s text', function() {
-		var view = new View( {
-			foo: 'bar'
-		} );
-
+	it( 'accept definition with selectors', function() {
 		var el = new Template( {
 			tag: 'p',
 			children: [
 				{
-					tag: 'b',
-					text: 'baz'
+					tag: 'span',
+					'class': '.y'
+				},
+				{
+					tag: 'div',
+					children: [
+						{
+							tag: 'span',
+							'class': '.y'
+						}
+					],
 				}
 			],
-			text: view.bind( 'foo' )
+			listeners: {
+				'x@.y': 'a',
+				'y@div': 'b'
+			}
 		} ).render();
 
-		expect( getOuterHtml( el ) ).to.be.equal( '<p>bar<b>baz</b></p>' );
+		el.childNodes[ 0 ].dispatchEvent( new Event( 'x' ) );
+		el.childNodes[ 1 ].dispatchEvent( new Event( 'x' ) ); // false
+		el.childNodes[ 1 ].childNodes[ 0 ].dispatchEvent( new Event( 'x' ) );
 
-		// TODO: A solution to avoid nuking the children?
-		view.model.foo = 'qux';
-		expect( getOuterHtml( el ) ).to.be.equal( '<p>qux</p>' );
+		el.childNodes[ 0 ].dispatchEvent( new Event( 'y' ) ); // false
+		el.childNodes[ 1 ].dispatchEvent( new Event( 'y' ) );
+		el.childNodes[ 1 ].childNodes[ 0 ].dispatchEvent( new Event( 'y' ) ); // false
 	} );
 } );
 
-function getOuterHtml( el ) {
-	var container = document.createElement( 'div' );
-	container.appendChild( el.cloneNode( 1 ) );
-
-	return container.innerHTML;
+function createClassReferences() {
+	Template = modules[ 'ui/template' ];
 }

+ 134 - 30
packages/ckeditor5-ui/tests/ui/view.js

@@ -9,39 +9,22 @@
 'use strict';
 
 var modules = bender.amd.require( 'ckeditor', 'ui/view', 'ui/region', 'ckeditorerror', 'model' );
+var View, TestView;
+var view;
 
 bender.tools.createSinonSandbox();
+beforeEach( createViewInstance );
 
-describe( 'View', function() {
-	var view;
-	var View;
-	var TestView;
-
-	beforeEach( 'Create a test view instance', function() {
-		View = modules[ 'ui/view' ];
-
-		view = new View( {
-			a: 'foo',
-			b: 42
-		} );
-
-		class T extends View {
-			constructor() {
-				super();
-				this.template = { tag: 'a' };
-			}
-		}
-
-		TestView = T;
-	} );
-
+describe( 'constructor', function() {
 	it( 'accepts the model', function() {
 		expect( view.model ).to.be.an.instanceof( modules.model );
 
 		expect( view ).to.have.deep.property( 'model.a', 'foo' );
 		expect( view ).to.have.deep.property( 'model.b', 42 );
 	} );
+} );
 
+describe( 'instance', function() {
 	it( 'has no default element', function() {
 		expect( () => view.el ).to.throw( modules.ckeditorerror );
 	} );
@@ -53,9 +36,11 @@ describe( 'View', function() {
 	it( 'has no default regions', function() {
 		expect( view.regions ).to.have.length( 0 );
 	} );
+} );
 
-	it( 'provides binding to the model', function() {
-		var spy = sinon.spy();
+describe( 'bind', function() {
+	it( 'returns a function that passes arguments', function() {
+		var spy = bender.sinon.spy();
 		var callback = view.bind( 'a', spy );
 
 		expect( spy.called ).to.be.false;
@@ -64,20 +49,125 @@ describe( 'View', function() {
 		sinon.assert.calledOnce( spy );
 		sinon.assert.calledWithExactly( spy, 'el', 'foo' );
 
-		spy.reset();
 		view.model.a = 'bar';
-		sinon.assert.calledOnce( spy );
-		sinon.assert.calledWithExactly( spy, 'el', 'bar' );
+		sinon.assert.calledTwice( spy );
+		expect( spy.secondCall.calledWithExactly( 'el', 'bar' ) ).to.be.true;
+	} );
+
+	it( 'allows binding attribute to the model', function() {
+		class TestView extends View {
+			constructor( model ) {
+				super( model );
+
+				this.template = {
+					tag: 'p',
+					attributes: {
+						'class': this.bind( 'foo' )
+					},
+					text: 'abc'
+				};
+			}
+		}
+
+		view = new TestView( { foo: 'bar' } );
+		expect( view.el.outerHTML ).to.be.equal( '<p class="bar">abc</p>' );
+
+		view.model.foo = 'baz';
+		expect( view.el.outerHTML ).to.be.equal( '<p class="baz">abc</p>' );
+	} );
+
+	it( 'allows binding "text" to the model', function() {
+		class TestView extends View {
+			constructor( model ) {
+				super( model );
+
+				this.template = {
+					tag: 'p',
+					children: [
+						{
+							tag: 'b',
+							text: 'baz'
+						}
+					],
+					text: this.bind( 'foo' )
+				};
+			}
+		}
+
+		view = new TestView( { foo: 'bar' } );
+
+		expect( view.el.outerHTML ).to.be.equal( '<p>bar<b>baz</b></p>' );
+
+		// TODO: A solution to avoid nuking the children?
+		view.model.foo = 'qux';
+		expect( view.el.outerHTML ).to.be.equal( '<p>qux</p>' );
 	} );
 
-	it( 'renders element from template', function() {
+	it( 'allows binding to the model with value processing', function() {
+		class TestView extends View {
+			constructor( model ) {
+				super( model );
+
+				var callback = ( el, value ) =>
+					( value > 0 ? 'positive' : 'negative' );
+
+				this.template = {
+					tag: 'p',
+					attributes: {
+						'class': this.bind( 'foo', callback )
+					},
+					text: this.bind( 'foo', callback )
+				};
+			}
+		}
+
+		view = new TestView( { foo: 3 } );
+		expect( view.el.outerHTML ).to.be.equal( '<p class="positive">positive</p>' );
+
+		view.model.foo = -7;
+		expect( view.el.outerHTML ).to.be.equal( '<p class="negative">negative</p>' );
+	} );
+
+	it( 'allows binding to the model with custom callback', function() {
+		class TestView extends View {
+			constructor( model ) {
+				super( model );
+
+				this.template = {
+					tag: 'p',
+					attributes: {
+						'class': this.bind( 'foo', ( el, value ) => {
+							el.innerHTML = value;
+
+							if ( value == 'changed' ) {
+								return value;
+							}
+						} )
+					},
+					text: 'bar'
+				};
+			}
+		}
+
+		view = new TestView( { foo: 'moo' } );
+		expect( view.el.outerHTML ).to.be.equal( '<p>moo</p>' );
+
+		view.model.foo = 'changed';
+		expect( view.el.outerHTML ).to.be.equal( '<p class="changed">changed</p>' );
+	} );
+} );
+
+describe( 'render', function() {
+	it( 'creates an element from template', function() {
 		view = new TestView( { a: 1 } );
 
 		expect( view.el ).to.be.an.instanceof( HTMLElement );
 		expect( view.el.nodeName ).to.be.equal( 'A' );
 	} );
+} );
 
-	it( 'destroys properly', function() {
+describe( 'destroys', function() {
+	it( 'destroys the view', function() {
 		view = new TestView( { a: 1 } );
 
 		// Append the views's element to some container.
@@ -108,3 +198,17 @@ describe( 'View', function() {
 		expect( spy.calledOnce ).to.be.true;
 	} );
 } );
+
+function createViewInstance() {
+	View = modules[ 'ui/view' ];
+	view = new View( { a: 'foo', b: 42 } );
+
+	class T extends View {
+		constructor() {
+			super();
+			this.template = { tag: 'a' };
+		}
+	}
+
+	TestView = T;
+}