Przeglądaj źródła

Replaced View#element on-demand rendering with the #render method. Renamed addChildren(), removeChildren() -> registerChildren(), deregisterChildren().

Aleksander Nowodzinski 8 lat temu
rodzic
commit
945d718ca5
2 zmienionych plików z 148 dodań i 169 usunięć
  1. 106 136
      packages/ckeditor5-ui/src/view.js
  2. 42 33
      packages/ckeditor5-ui/tests/view.js

+ 106 - 136
packages/ckeditor5-ui/src/view.js

@@ -21,9 +21,9 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  * {@link module:ui/view~View#template}. Views are building blocks of the user interface and handle
  * interaction
  *
- * Views {@link module:ui/view~View#addChildren aggregate} children in
+ * Views {@link module:ui/view~View#registerChildren aggregate} children in
  * {@link module:ui/view~View#createCollection collections} and manage the life cycle of DOM
- * listeners e.g. by handling initialization and destruction.
+ * listeners e.g. by handling rendering and destruction.
  *
  * See the {@link module:ui/template~TemplateDefinition} syntax to learn more about shaping view
  * elements, attributes and listeners.
@@ -66,8 +66,7 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  *
  *		const view = new SampleView( locale );
  *
- *		// Each view must be first initialized.
- *		view.init();
+ *		view.render();
  *
  *		// Append <p class="foo bar">Hello<b>world</b></p> to the <body>
  *		document.body.appendChild( view.element );
@@ -86,12 +85,53 @@ export default class View {
 	/**
 	 * Creates an instance of the {@link module:ui/view~View} class.
 	 *
-	 * Also see {@link #init}.
+	 * Also see {@link #render}.
 	 *
 	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
 	 */
 	constructor( locale ) {
 		/**
+		 * An HTML element of the view. `null` until {@link #render rendered}
+		 * from the {@link #template}.
+		 *
+		 *		class SampleView extends View {
+		 *			constructor() {
+		 *				super();
+		 *
+		 *				// A template instance the #element will be created from.
+		 *				this.template = new Template( {
+		 *					tag: 'p'
+		 *
+		 *					// ...
+		 *				} );
+		 *			}
+		 *		}
+		 *
+		 *		const view = new SampleView();
+		 *
+		 *		// Renders the #template
+		 *		view.render();
+		 *
+		 *		// Append the HTML element of the view to <body>.
+		 *		document.body.appendChild( view.element );
+		 *
+		 * **Note**: The element of the view can also be assigned directly:
+		 *
+		 *		view.element = document.querySelector( '#my-container' );
+		 *
+		 * @member {HTMLElement}
+		 */
+		this.element = null;
+
+		/**
+		 * Set `true` when the view has already been {@link module:ui/view~View#render rendered}.
+		 *
+		 * @readonly
+		 * @member {Boolean} #isRendered
+		 */
+		this.isRendered = false;
+
+		/**
 		 * A set of tools to localize the user interface.
 		 *
 		 * Also see {@link module:core/editor/editor~Editor#locale}.
@@ -113,17 +153,6 @@ export default class View {
 		this.t = locale && locale.t;
 
 		/**
-		 * A flag set `true` after {@link #init initialization}. Because the process can be
-		 * asynchronous, this {@link module:utils/observablemixin~Observable observable} flag allows
-		 * deferring certain actions until it is finished.
-		 *
-		 * @readonly
-		 * @observable
-		 * @member {Boolean} #ready
-		 */
-		this.set( 'ready', false );
-
-		/**
 		 * Collections registered with {@link #createCollection}.
 		 *
 		 * @protected
@@ -147,19 +176,12 @@ export default class View {
 
 		/**
 		 * Template of this view. It provides the {@link #element} representing
-		 * the view in DOM.
+		 * the view in DOM, which is {@link #render rendered}.
 		 *
 		 * @member {module:ui/template~Template} #template
 		 */
 
 		/**
-		 * An internal, cached element of this view. See {@link #element}.
-		 *
-		 * @private
-		 * @member {HTMLElement} #_element
-		 */
-
-		/**
 		 * Cached {@link @link module:ui/template~BindChain bind chain} object created by the
 		 * {@link #template}. See {@link #bindTemplate}.
 		 *
@@ -169,54 +191,6 @@ export default class View {
 	}
 
 	/**
-	 * An HTML element of this view. The element is rendered on first reference
-	 * by the {@link #template}:
-	 *
-	 *		class SampleView extends View {
-	 *			constructor() {
-	 *				super();
-	 *
-	 *				// A template instance the #element will be created from.
-	 *				this.template = new Template( {
-	 *					tag: 'p'
-	 *
-	 *					// ...
-	 *				} );
-	 *			}
-	 *		}
-	 *
-	 *		const view = new SampleView();
-	 *		view.init();
-	 *
-	 *		// Renders the #template and appends the output to <body>.
-	 *		document.body.appendChild( view.element );
-	 *
-	 * The element of the view can also be assigned directly:
-	 *
-	 *		view.element = document.querySelector( '#my-container' );
-	 *
-	 * @type {HTMLElement}
-	 */
-	get element() {
-		if ( this._element ) {
-			return this._element;
-		}
-
-		// No template means no element (a virtual view).
-		if ( !this.template ) {
-			return null;
-		}
-
-		this._addTemplateChildren();
-
-		return ( this._element = this.template.render() );
-	}
-
-	set element( el ) {
-		this._element = el;
-	}
-
-	/**
 	 * Shorthand for {@link module:ui/template~Template.bind}, a binding
 	 * {@link module:ui/template~BindChain interface} pre–configured for the view instance.
 	 *
@@ -289,7 +263,7 @@ export default class View {
 	 *		const view = new SampleView( locale );
 	 *		const child = new ChildView( locale );
 	 *
-	 *		view.init();
+	 *		view.render();
 	 *
 	 *		// It will append <p></p> to the <body>.
 	 *		document.body.appendChild( view.element );
@@ -310,7 +284,7 @@ export default class View {
 
 	/**
 	 * Registers a new child view under the view instance. Once registered, a child
-	 * view is managed by its parent, including {@link #init initization}
+	 * view is managed by its parent, including {@link #render rendering}
 	 * and {@link #destroy destruction}.
 	 *
 	 * To revert this, use {@link #removeChildren}.
@@ -325,20 +299,20 @@ export default class View {
 	 *				this.template = new Template( { tag: 'p' } );
 	 *
 	 *				// Register the children.
-	 *				this.addChildren( [ this.childA, this.childB ] );
+	 *				this.registerChildren( [ this.childA, this.childB ] );
 	 *			}
 	 *
-	 *			init() {
+	 *			render() {
+	 *				super.render();
+	 *
 	 *				this.element.appendChild( this.childA.element );
 	 *				this.element.appendChild( this.childB.element );
-	 *
-	 *				return super.init();
 	 *			}
 	 *		}
 	 *
 	 *		const view = new SampleView( locale );
 	 *
-	 *		view.init();
+	 *		view.render();
 	 *
 	 *		// Will append <p><childA#element><b></b><childB#element></p>.
 	 *		document.body.appendChild( view.element );
@@ -357,7 +331,7 @@ export default class View {
 	 *					tag: 'p',
 	 *
  	 *					// These children will be added automatically. There's no
- 	 *					// need to call {@link #addChildren} for any of them.
+ 	 *					// need to call {@link #registerChildren} for any of them.
 	 *					children: [ this.childA, this.childB ]
 	 *				} );
 	 *			}
@@ -367,39 +341,53 @@ export default class View {
 	 *
 	 * @param {module:ui/view~View|Iterable.<module:ui/view~View>} children Children views to be registered.
 	 */
-	addChildren( children ) {
+	registerChildren( children ) {
 		if ( !isIterable( children ) ) {
 			children = [ children ];
 		}
 
-		children.map( c => this._unboundChildren.add( c ) );
+		for ( const child of children ) {
+			this._unboundChildren.add( child );
+		}
 	}
 
 	/**
-	 * The opposite of {@link #addChildren}. Removes a child view from this view instance.
+	 * The opposite of {@link #registerChildren}. Removes a child view from this view instance.
 	 * Once removed, the child is no longer managed by its parent, e.g. it can safely
 	 * become a child of another parent view.
 	 *
-	 * @see #addChildren
+	 * @see #registerChildren
 	 * @param {module:ui/view~View|Iterable.<module:ui/view~View>} children Child views to be removed.
 	 */
-	removeChildren( children ) {
+	deregisterChildren( children ) {
 		if ( !isIterable( children ) ) {
 			children = [ children ];
 		}
 
-		children.map( c => this._unboundChildren.remove( c ) );
+		for ( const child of children ) {
+			this._unboundChildren.remove( child );
+		}
 	}
 
 	/**
-	 * Initializes the view and its children added by {@link #addChildren} and residing in collections
-	 * created by the {@link #createCollection} method.
+	 * Recursively renders the view.
+	 *
+	 * Once the view is rendered:
+	 * * the {@link #element} becomes an HTML element out of {@link #template},
+	 * * the {@link #isRendered} flag is set `true`.
+	 *
+	 * **Note**: The children of the view:
+	 * * defined directly in the {@link #template}
+	 * * residing in collections created by the {@link #createCollection} method,
+	 * * and added by {@link #registerChildren}
+	 * are also rendered in the process.
+	 *
+	 * In general, `render()` method is the right place to keep the code which refers to the
+	 * {@link #element} and should be executed at the very beginning of the view's life cycle.
 	 *
-	 * In general, `init()` is the right place to keep the code which refers to the {@link #element}
-	 * and should be executed at the very beginning of the view's life cycle. It is possible to
-	 * {@link module:ui/template~Template.extend} the {@link #template} before the first reference of
-	 * the {@link #element}. To allow an early customization of the view (e.g. by its parent),
-	 * such references should be done in `init()`.
+	 * It is possible to {@link module:ui/template~Template.extend} the {@link #template} before
+	 * the view is rendered. To allow an early customization of the view (e.g. by its parent),
+	 * such references should be done in `render()`.
 	 *
 	 *		class SampleView extends View {
 	 *			constructor() {
@@ -408,26 +396,25 @@ export default class View {
 	 *				} );
 	 *			},
 	 *
-	 *			init() {
-	 *				super.init();
+	 *			render() {
+	 *				// View#element becomes available.
+	 *				super.render();
 	 *
-	 *				function scroll() {
+	 *				// The "scroll" listener depends on #element.
+	 *				this.listenTo( window, 'scroll', () => {
 	 *					// A reference to #element would render the #template and make it non-extendable.
 	 *					if ( window.scrollY > 0 ) {
 	 *						this.element.scrollLeft = 100;
 	 *					} else {
 	 *						this.element.scrollLeft = 0;
 	 *					}
-	 *				}
-	 *
-	 *				this.listenTo( window, 'scroll', () => {
-	 *					scroll();
 	 *				} );
 	 *			}
 	 *		}
 	 *
 	 *		const view = new SampleView();
 	 *
+	 *		// Let's customize the view before it gets rendered.
 	 *		Template.extend( view.template, {
 	 *			attributes: {
 	 *				class: [
@@ -436,30 +423,35 @@ export default class View {
 	 *			}
 	 *		} );
 	 *
-	 *		// Late initialization allows customization of the view.
-	 *		view.init();
-	 *
-	 * Once initialized, the view becomes {@link #ready}.
+	 *		// Late rendering allows customization of the view.
+	 *		view.render();
 	 */
-	init() {
-		if ( this.ready ) {
+	render() {
+		if ( this.isRendered ) {
 			/**
-			 * This View has already been initialized.
+			 * This View has already been rendered.
 			 *
-			 * @error ui-view-init-reinit
+			 * @error ui-view-render-rendered
 			 */
-			throw new CKEditorError( 'ui-view-init-reinit: This View has already been initialized.' );
+			throw new CKEditorError( 'ui-view-render-already-rendered: This View has already been rendered.' );
 		}
 
-		// Initialize collections in #_viewCollections.
-		this._viewCollections.map( c => c.init() );
+		// Render collections in #_viewCollections.
+		this._viewCollections.map( col => col.render() );
 
-		// Spread the word that this view is ready!
-		this.ready = true;
+		// Render #element of the view.
+		if ( this.template ) {
+			this.element = this.template.render();
+
+			// Auto–register view children from #template.
+			this.registerChildren( this.template.getViews() );
+		}
+
+		this.isRendered = true;
 	}
 
 	/**
-	 * Recursively destroys the view instance and child views added by {@link #addChildren} and
+	 * Recursively destroys the view instance and child views added by {@link #registerChildren} and
 	 * residing in collections created by the {@link #createCollection}.
 	 *
 	 * Destruction disables all event listeners:
@@ -471,28 +463,6 @@ export default class View {
 
 		this._viewCollections.map( c => c.destroy() );
 	}
-
-	/**
-	 * Recursively traverses {@link #template} in search of {@link module:ui/view~View}
-	 * instances and automatically registers them using {@link #addChildren} method.
-	 *
-	 * @protected
-	 */
-	_addTemplateChildren() {
-		const search = def => {
-			if ( def.children ) {
-				for ( const defOrView of def.children ) {
-					if ( defOrView instanceof View ) {
-						this.addChildren( defOrView );
-					} else {
-						search( defOrView );
-					}
-				}
-			}
-		};
-
-		search( this.template );
-	}
 }
 
 mix( View, DomEmitterMixin );

+ 42 - 33
packages/ckeditor5-ui/tests/view.js

@@ -28,7 +28,7 @@ describe( 'View', () => {
 
 			expect( view.t ).to.be.undefined;
 			expect( view.locale ).to.be.undefined;
-			expect( view.ready ).to.be.false;
+			expect( view.isRendered ).to.be.false;
 			expect( view.template ).to.be.undefined;
 			expect( view._viewCollections ).to.be.instanceOf( Collection );
 			expect( view._unboundChildren ).to.be.instanceOf( ViewCollection );
@@ -81,7 +81,7 @@ describe( 'View', () => {
 		} );
 	} );
 
-	describe( 'addChildren()', () => {
+	describe( 'registerChildren()', () => {
 		beforeEach( () => {
 			setTestViewClass();
 			setTestViewInstance();
@@ -90,9 +90,9 @@ describe( 'View', () => {
 		it( 'should add a single view to #_unboundChildren', () => {
 			expect( view._unboundChildren ).to.have.length( 0 );
 
-			const child = {};
+			const child = new View();
 
-			view.addChildren( child );
+			view.registerChildren( child );
 			expect( view._unboundChildren ).to.have.length( 1 );
 			expect( view._unboundChildren.get( 0 ) ).to.equal( child );
 		} );
@@ -100,74 +100,82 @@ describe( 'View', () => {
 		it( 'should support iterables', () => {
 			expect( view._unboundChildren ).to.have.length( 0 );
 
-			view.addChildren( [ {}, {}, {} ] );
+			view.registerChildren( [ new View(), new View(), new View() ] );
 			expect( view._unboundChildren ).to.have.length( 3 );
 		} );
 	} );
 
-	describe( 'removeChildren()', () => {
+	describe( 'deregisterChildren()', () => {
 		beforeEach( () => {
 			setTestViewClass();
 			setTestViewInstance();
 		} );
 
 		it( 'should remove a single view from #_unboundChildren', () => {
-			const child1 = {};
-			const child2 = {};
+			const child1 = new View();
+			const child2 = new View();
 
-			view.addChildren( child1 );
-			view.addChildren( child2 );
+			view.registerChildren( child1 );
+			view.registerChildren( child2 );
 			expect( view._unboundChildren ).to.have.length( 2 );
 
-			view.removeChildren( child2 );
+			view.deregisterChildren( child2 );
 			expect( view._unboundChildren ).to.have.length( 1 );
 			expect( view._unboundChildren.get( 0 ) ).to.equal( child1 );
 		} );
 
 		it( 'should support iterables', () => {
-			const child1 = {};
-			const child2 = {};
-			const child3 = {};
+			const child1 = new View();
+			const child2 = new View();
+			const child3 = new View();
 
-			view.addChildren( [ child1, child2, child3 ] );
+			view.registerChildren( [ child1, child2, child3 ] );
 			expect( view._unboundChildren ).to.have.length( 3 );
 
-			view.removeChildren( [ child2, child3 ] );
+			view.deregisterChildren( [ child2, child3 ] );
 			expect( view._unboundChildren ).to.have.length( 1 );
 			expect( view._unboundChildren.get( 0 ) ).to.equal( child1 );
 		} );
 	} );
 
-	describe( 'init()', () => {
-		beforeEach( createViewWithChildren );
+	describe( 'render()', () => {
+		it( 'should throw if already rendered', () => {
+			const view = new View();
 
-		it( 'should throw if already initialized', () => {
-			view.init();
+			view.render();
 
 			try {
-				view.init();
+				view.render();
 				throw new Error( 'This should not be executed.' );
 			} catch ( err ) {
 				expect( err ).to.be.instanceof( CKEditorError );
-				expect( err.message ).to.match( /ui-view-init-re/ );
+				expect( err.message ).to.match( /^ui-view-render-already-rendered:/ );
 			}
 		} );
 
-		it( 'should set view#ready', () => {
-			expect( view.ready ).to.be.false;
+		it( 'should set view#isRendered', () => {
+			const view = new View();
+
+			view.template = new Template( {
+				tag: 'div'
+			} );
+
+			expect( view.isRendered ).to.be.false;
 
-			view.init();
-			expect( view.ready ).to.be.true;
+			view.render();
+			expect( view.isRendered ).to.be.true;
 		} );
 
-		it( 'calls init() on all view#_viewCollections', () => {
+		it( 'calls render() on all view#_viewCollections', () => {
+			const view = new View();
+
 			const collectionA = view.createCollection();
 			const collectionB = view.createCollection();
 
-			const spyA = testUtils.sinon.spy( collectionA, 'init' );
-			const spyB = testUtils.sinon.spy( collectionB, 'init' );
+			const spyA = testUtils.sinon.spy( collectionA, 'render' );
+			const spyB = testUtils.sinon.spy( collectionB, 'render' );
 
-			view.init();
+			view.render();
 			sinon.assert.calledOnce( spyA );
 			sinon.assert.calledOnce( spyB );
 			sinon.assert.callOrder( spyA, spyB );
@@ -248,8 +256,7 @@ describe( 'View', () => {
 
 			expect( view._unboundChildren ).to.have.length( 0 );
 
-			// Render the view.
-			view.element;
+			view.render();
 
 			expect( view._unboundChildren ).to.have.length( 3 );
 			expect( view._unboundChildren.get( 0 ) ).to.equal( viewA );
@@ -282,7 +289,7 @@ describe( 'View', () => {
 		it( 'should not clear the #_unboundChildren', () => {
 			const cached = view._unboundChildren;
 
-			view.addChildren( [ new View(), new View() ] );
+			view.registerChildren( [ new View(), new View() ] );
 			expect( cached ).to.have.length( 4 );
 
 			view.destroy();
@@ -354,6 +361,8 @@ function setTestViewInstance() {
 	view = new TestView();
 
 	if ( view.template ) {
+		view.render();
+
 		document.body.appendChild( view.element );
 	}
 }