Selaa lähdekoodia

Extracted View's templating API to ui/template module.

Aleksander Nowodzinski 10 vuotta sitten
vanhempi
sitoutus
f409dcf1dd

+ 85 - 0
packages/ckeditor5-engine/src/ui/template.js

@@ -0,0 +1,85 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+'use strict';
+
+CKEDITOR.define( function() {
+	class Template {
+		/**
+		 * Creates an instance of the {@link Template} class.
+		 *
+		 * @param {Model} mode (View)Model of this Template.
+		 * @constructor
+		 */
+		constructor( def ) {
+			/**
+			 * Definition of this Template.
+			 */
+			this.def = def;
+		}
+
+		/**
+		 * Renders HTMLElement using {@link def}.
+		 *
+		 * @param {Object} [def] Template definition to be rendered.
+		 * @returns {HTMLElement}
+		 */
+		render( def ) {
+			// TODO: Use ES6 default arguments syntax.
+			def = def || this.def;
+
+			if ( !def ) {
+				return null;
+			}
+
+			var el = document.createElement( def.tag );
+
+			// Set the text first.
+			if ( def.text ) {
+				if ( typeof def.text == 'function' ) {
+					def.text( el, textUpdater() );
+				} else {
+					el.innerHTML = def.text;
+				}
+			}
+
+			// Set attributes.
+			for ( let attr in def.attributes ) {
+				let value = def.attributes[ attr ];
+
+				// Attribute bound directly to the model.
+				if ( typeof value == 'function' ) {
+					value( el, attributeUpdater( attr ) );
+				}
+
+				// Explicit attribute definition (string).
+				else {
+					// Attribute can be an array, i.e. classes.
+					if ( Array.isArray( value ) ) {
+						value = value.join( ' ' );
+					}
+
+					attributeUpdater( attr )( el, value );
+				}
+			}
+
+			// Invoke children recursively.
+			if ( def.children ) {
+				for ( let child of def.children ) {
+					el.appendChild( this.render( child ) );
+				}
+			}
+
+			return el;
+		}
+	}
+
+	var textUpdater = () => ( el, value ) => el.innerHTML = value;
+	var attributeUpdater = ( attr ) => ( el, value ) => el.setAttribute( attr, value );
+
+	return Template;
+} );

+ 7 - 64
packages/ckeditor5-engine/src/ui/view.js

@@ -3,11 +3,9 @@
  * For licensing, see LICENSE.md.
  */
 
-/* global document */
-
 'use strict';
 
-CKEDITOR.define( [ 'Collection', 'Model' ], function( Collection, Model ) {
+CKEDITOR.define( [ 'collection', 'model', 'ui/template' ], function( Collection, Model, Template ) {
 	class View extends Model {
 		/**
 		 * Creates an instance of the {@link View} class.
@@ -47,7 +45,6 @@ CKEDITOR.define( [ 'Collection', 'Model' ], function( Collection, Model ) {
 			}
 
 			this._el = this.render();
-
 			this.attachListeners();
 
 			return this._el;
@@ -62,7 +59,7 @@ CKEDITOR.define( [ 'Collection', 'Model' ], function( Collection, Model ) {
 		 * @param {Function} [callback] Callback function executed on property change in model.
 		 * @constructor
 		 */
-		bindModel( property, callback ) {
+		bind( property, callback ) {
 			var model = this.model;
 
 			return function( el, updater ) {
@@ -104,68 +101,14 @@ CKEDITOR.define( [ 'Collection', 'Model' ], function( Collection, Model ) {
 		}
 
 		/**
-		 * Renders {@link el} using {@link template}.
+		 * Renders View's {@link el}.
 		 *
-		 * @param {Object} [def] Template definition to be rendered.
-		 * @returns HTMLElement {@link el} ready to be injected into DOM.
+		 * @returns {HTMLElement}
 		 */
-		render( template ) {
-			// TODO: Use ES6 default arguments syntax.
-			template = template || this.template;
-
-			if ( !template ) {
-				return null;
-			}
-
-			var el = document.createElement( template.tag );
-			var attr;
-			var value;
-
-			var textUpdater = () => {
-				return ( el, value ) => el.innerHTML = value;
-			};
-
-			var attributeUpdater = ( attr ) => {
-				return ( el, value ) => el.setAttribute( attr, value );
-			};
-
-			// Set the text first.
-			if ( template.text ) {
-				if ( typeof template.text == 'function' ) {
-					template.text( el, textUpdater() );
-				} else {
-					el.innerHTML = template.text;
-				}
-			}
-
-			// Set attributes.
-			for ( attr in template.attributes ) {
-				value = template.attributes[ attr ];
-
-				// Attribute bound directly to the model.
-				if ( typeof value == 'function' ) {
-					value( el, attributeUpdater( attr ) );
-				}
-
-				// Explicit attribute definition (string).
-				else {
-					// Attribute can be an array, i.e. classes.
-					if ( Array.isArray( value ) ) {
-						value = value.join( ' ' );
-					}
-
-					el.setAttribute( attr, value );
-				}
-			}
-
-			// Invoke children recursively.
-			if ( template.children ) {
-				for ( let child of template.children ) {
-					el.appendChild( this.render( child ) );
-				}
-			}
+		render() {
+			this._template = new Template( this.template );
 
-			return el;
+			return this._template.render();
 		}
 
 		destroy() {}

+ 0 - 121
packages/ckeditor5-engine/tests/ui/view.js

@@ -1,121 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* global document, HTMLElement */
-
-'use strict';
-
-var modules = bender.amd.require( 'ckeditor', 'ui/view' );
-var view;
-
-describe( 'plain view', function() {
-	beforeEach( 'Create a test view instance', function() {
-		var View = modules[ 'ui/view' ];
-
-		view = new View( {
-			a: 'foo',
-			b: 42
-		} );
-	} );
-
-	it( 'accepts the model', function() {
-		expect( view ).to.have.deep.property( 'model.a', 'foo' );
-		expect( view ).to.have.deep.property( 'model.b', 42 );
-	} );
-
-	it( 'has no default element', function() {
-		expect( view.el ).to.be.null();
-	} );
-
-	it( 'has no default template', function() {
-		expect( view.template ).to.be.undefined();
-	} );
-
-	it( 'has no default regions', function() {
-		expect( view.regions.length ).to.be.equal( 0 );
-	} );
-} );
-
-describe( 'rich view', function() {
-	beforeEach( 'Create a test view instance', function() {
-		var View = modules[ 'ui/view' ];
-		var Component = class extends View {
-				constructor( model ) {
-					super( model );
-
-					this.template = {
-						tag: 'p',
-						attributes: {
-							'class': 'a',
-							x: this.bindModel( 'plain' )
-						},
-						text: this.bindModel( 'text' ),
-						children: [
-							{
-								tag: 'b',
-								text: 'c',
-								attributes: {
-									y: this.bindModel( 'augmented', function( el, value ) {
-										return ( value > 0 ? 'positive' : 'negative' );
-									} )
-								}
-							},
-							{
-								tag: 'i',
-								text: 'd',
-								attributes: {
-									z: this.bindModel( 'custom', function( el, value ) {
-										el.innerHTML = value;
-
-										if ( value == 'foo' ) {
-											return value;
-										}
-									} )
-								}
-							}
-						]
-					};
-				}
-			};
-
-		view = new Component( {
-			plain: 'z',
-			augmented: 7,
-			custom: 'moo',
-			text: 'bar'
-		} );
-	} );
-
-	it( 'renders the element', function() {
-		expect( view.el ).to.be.instanceof( HTMLElement );
-		expect( view.el.parentNode ).to.be.null();
-
-		expect( getOuterHtml( view.el ) ).to.be.equal( '<p class="a" x="z">bar<b y="positive">c</b><i>moo</i></p>' );
-	} );
-
-	it( 'reacts to changes in model', function() {
-		view.model.plain = 'x';
-		expect( getOuterHtml( view.el ) ).to.be.equal( '<p class="a" x="x">bar<b y="positive">c</b><i>moo</i></p>' );
-
-		view.model.augmented = 2;
-		expect( getOuterHtml( view.el ) ).to.be.equal( '<p class="a" x="x">bar<b y="positive">c</b><i>moo</i></p>' );
-
-		view.model.augmented = -2;
-		expect( getOuterHtml( view.el ) ).to.be.equal( '<p class="a" x="x">bar<b y="negative">c</b><i>moo</i></p>' );
-
-		view.model.custom = 'foo';
-		expect( getOuterHtml( view.el ) ).to.be.equal( '<p class="a" x="x">bar<b y="negative">c</b><i z="foo">foo</i></p>' );
-
-		view.model.text = 'baz';
-		expect( getOuterHtml( view.el ) ).to.be.equal( '<p class="a" x="x">baz</p>' );
-	} );
-} );
-
-function getOuterHtml( el ) {
-	var container = document.createElement( 'div' );
-	container.appendChild( el.cloneNode( 1 ) );
-
-	return container.innerHTML;
-}

+ 169 - 0
packages/ckeditor5-engine/tests/ui/view/template.js

@@ -0,0 +1,169 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: core, ui */
+/* global document, HTMLElement */
+
+'use strict';
+
+var modules = bender.amd.require( 'ckeditor', 'ui/view', 'ui/template' );
+
+describe( 'Template', function() {
+	var View;
+	var Template;
+
+	beforeEach( 'Create a test view instance', function() {
+		View = modules[ 'ui/view' ];
+		Template = modules[ 'ui/template' ];
+	} );
+
+	it( 'accepts the definition', function() {
+		var def = {
+			tag: 'p'
+		};
+
+		expect( new Template( def ).def ).to.equal( def );
+	} );
+
+	it( 'renders the element', function() {
+		var el = new Template( {
+			tag: 'p',
+			attributes: {
+				'class': [ 'a', 'b' ],
+				x: 'bar'
+			},
+			text: 'foo'
+		} ).render();
+
+		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>' );
+	} );
+
+	it( 'renders element\'s children', function() {
+		var el = new Template( {
+			tag: 'p',
+			attributes: {
+				a: 'A'
+			},
+			children: [
+				{
+					tag: 'b',
+					text: 'B'
+				},
+				{
+					tag: 'i',
+					text: 'C',
+					children: [
+						{
+							tag: 'b',
+							text: 'D'
+						}
+					]
+				}
+			]
+		} ).render();
+
+		expect( getOuterHtml( el ) ).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'
+		} );
+
+		var el = new Template( {
+			tag: 'p',
+			attributes: {
+				'class': view.bind( 'foo' )
+			},
+			text: view.bind( 'foo' )
+		} ).render();
+
+		expect( getOuterHtml( el ) ).to.be.equal( '<p class="bar">bar</p>' );
+
+		view.model.foo = 'baz';
+		expect( getOuterHtml( el ) ).to.be.equal( '<p class="baz">baz</p>' );
+	} );
+
+	it( 'binds to the model and processes the property', function() {
+		var view = new View( {
+			foo: 7
+		} );
+
+		var callback = ( el, value ) => ( value > 0 ? 'positive' : 'negative' );
+
+		var el = new Template( {
+			tag: 'p',
+			attributes: {
+				'class': view.bind( 'foo', callback )
+			},
+			text: view.bind( 'foo', callback )
+		} ).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'
+		} );
+
+		var callback = ( el, value ) => {
+			el.innerHTML = value;
+
+			if ( value == 'changed' ) {
+				return value;
+			}
+		};
+
+		var el = new Template( {
+			tag: 'p',
+			attributes: {
+				'class': view.bind( 'foo', callback )
+			},
+			text: 'bar'
+		} ).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>' );
+	} );
+
+	it( 'binds to the model and updates element\'s text', function() {
+		var view = new View( {
+			foo: 'bar'
+		} );
+
+		var el = new Template( {
+			tag: 'p',
+			children: [
+				{
+					tag: 'b',
+					text: 'baz'
+				}
+			],
+			text: view.bind( 'foo' )
+		} ).render();
+
+		expect( getOuterHtml( el ) ).to.be.equal( '<p>bar<b>baz</b></p>' );
+
+		// TODO: A solution to avoid nuking the children?
+		view.model.foo = 'qux';
+		expect( getOuterHtml( el ) ).to.be.equal( '<p>qux</p>' );
+	} );
+} );
+
+function getOuterHtml( el ) {
+	var container = document.createElement( 'div' );
+	container.appendChild( el.cloneNode( 1 ) );
+
+	return container.innerHTML;
+}

+ 56 - 0
packages/ckeditor5-engine/tests/ui/view/view.js

@@ -0,0 +1,56 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: core, ui */
+
+'use strict';
+
+var modules = bender.amd.require( 'ckeditor', 'ui/view' );
+
+describe( 'View', function() {
+	var view;
+
+	beforeEach( 'Create a test view instance', function() {
+		var View = modules[ 'ui/view' ];
+
+		view = new View( {
+			a: 'foo',
+			b: 42
+		} );
+	} );
+
+	it( 'accepts the model', function() {
+		expect( view ).to.have.deep.property( 'model.a', 'foo' );
+		expect( view ).to.have.deep.property( 'model.b', 42 );
+	} );
+
+	it( 'has no default element', function() {
+		expect( view.el ).to.be.null();
+	} );
+
+	it( 'has no default template', function() {
+		expect( view.template ).to.be.undefined();
+	} );
+
+	it( 'has no default regions', function() {
+		expect( view.regions.length ).to.be.equal( 0 );
+	} );
+
+	it( 'provides binding to the model', function() {
+		var spy = sinon.spy();
+		var callback = view.bind( 'a', spy );
+
+		expect( spy.called ).to.be.false;
+
+		callback( 'el', 'updater' );
+		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' );
+	} );
+} );