8
0
Просмотр исходного кода

Merge pull request #332 from ckeditor/t/262

Other: Implemented `View#render` method which replaces the `#element` rendering upon the first reference and incorporates the `#init` method functionality. Closes #262. Closes #302.

From now on `View#setTemplate` and `View#extendTemplate` methods are recommended as a shorthand for `view.template = new Template( { ... } )` and `Template.extend( view.template )`.

BREAKING CHANGE: The `init()` method in various UI components has been renamed to `render()`. Please refer to the documentation to learn more.

BREAKING CHANGE: The `View#element` is no longer a getter which renders an element when first referenced. Use the `View#render()` method instead.

BREAKING CHANGE: `Template#children` property became an `Array` (previously `ViewCollection`).

BREAKING CHANGE: `View#addChildren` and `View#removeChildren` methods became `#registerChildren` and `#deregisterChildren`.

BREAKING CHANGE:  The DOM structure of the `StickyPanelView` has changed along with the class names. Please refer to the component documentation to learn more.
Oskar Wróbel 8 лет назад
Родитель
Сommit
95ad993d05
69 измененных файлов с 979 добавлено и 923 удалено
  1. 2 2
      packages/ckeditor5-ui/docs/_snippets/examples/bootstrap-ui-inner.js
  2. 2 2
      packages/ckeditor5-ui/docs/framework/guides/external-ui.md
  3. 1 1
      packages/ckeditor5-ui/src/bindings/preventdefault.js
  4. 61 34
      packages/ckeditor5-ui/src/button/buttonview.js
  5. 1 1
      packages/ckeditor5-ui/src/dropdown/createdropdown.js
  6. 1 2
      packages/ckeditor5-ui/src/dropdown/dropdownpanelview.js
  7. 12 13
      packages/ckeditor5-ui/src/dropdown/dropdownview.js
  8. 1 3
      packages/ckeditor5-ui/src/dropdown/list/createlistdropdown.js
  9. 6 7
      packages/ckeditor5-ui/src/editableui/editableuiview.js
  10. 1 2
      packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.js
  11. 1 2
      packages/ckeditor5-ui/src/editorui/boxed/boxededitoruiview.js
  12. 3 2
      packages/ckeditor5-ui/src/editorui/editoruiview.js
  13. 25 6
      packages/ckeditor5-ui/src/icon/iconview.js
  14. 8 34
      packages/ckeditor5-ui/src/iframe/iframeview.js
  15. 8 2
      packages/ckeditor5-ui/src/inputtext/inputtextview.js
  16. 1 2
      packages/ckeditor5-ui/src/label/labelview.js
  17. 1 2
      packages/ckeditor5-ui/src/labeledinput/labeledinputview.js
  18. 4 3
      packages/ckeditor5-ui/src/list/listitemview.js
  19. 13 9
      packages/ckeditor5-ui/src/list/listview.js
  20. 4 5
      packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js
  21. 3 3
      packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js
  22. 64 48
      packages/ckeditor5-ui/src/panel/sticky/stickypanelview.js
  23. 70 13
      packages/ckeditor5-ui/src/template.js
  24. 3 2
      packages/ckeditor5-ui/src/toolbar/contextual/contextualtoolbar.js
  25. 1 2
      packages/ckeditor5-ui/src/toolbar/toolbarseparatorview.js
  26. 13 9
      packages/ckeditor5-ui/src/toolbar/toolbarview.js
  27. 1 2
      packages/ckeditor5-ui/src/tooltip/tooltipview.js
  28. 141 144
      packages/ckeditor5-ui/src/view.js
  29. 8 71
      packages/ckeditor5-ui/src/viewcollection.js
  30. 2 2
      packages/ckeditor5-ui/tests/_utils-tests/utils.js
  31. 10 7
      packages/ckeditor5-ui/tests/_utils/utils.js
  32. 5 6
      packages/ckeditor5-ui/tests/bindings/preventdefault.js
  33. 21 18
      packages/ckeditor5-ui/tests/button/buttonview.js
  34. 2 1
      packages/ckeditor5-ui/tests/dropdown/dropdownpanelview.js
  35. 5 4
      packages/ckeditor5-ui/tests/dropdown/dropdownview.js
  36. 5 1
      packages/ckeditor5-ui/tests/dropdown/list/createlistdropdown.js
  37. 14 11
      packages/ckeditor5-ui/tests/dropdown/manual/dropdown.js
  38. 8 5
      packages/ckeditor5-ui/tests/editableui/editableuiview.js
  39. 2 1
      packages/ckeditor5-ui/tests/editableui/inline/inlineeditableuiview.js
  40. 4 1
      packages/ckeditor5-ui/tests/editorui/boxed/boxededitoruiview.js
  41. 6 2
      packages/ckeditor5-ui/tests/editorui/editoruiview.js
  42. 15 2
      packages/ckeditor5-ui/tests/icon/iconview.js
  43. 18 8
      packages/ckeditor5-ui/tests/iframe/iframeview.js
  44. 1 1
      packages/ckeditor5-ui/tests/inputtext/inputtextview.js
  45. 1 1
      packages/ckeditor5-ui/tests/label/labelview.js
  46. 3 3
      packages/ckeditor5-ui/tests/labeledinput/labeledinputview.js
  47. 5 3
      packages/ckeditor5-ui/tests/list/listitemview.js
  48. 19 8
      packages/ckeditor5-ui/tests/list/listview.js
  49. 1 1
      packages/ckeditor5-ui/tests/manual/icon/icon.js
  50. 0 33
      packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.html
  51. 0 98
      packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.js
  52. 0 6
      packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.md
  53. BIN
      packages/ckeditor5-ui/tests/manual/imagetoolbar/sample.jpg
  54. 1 0
      packages/ckeditor5-ui/tests/manual/panel/balloon/balloonpanelview.js
  55. 2 2
      packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.html
  56. 1 1
      packages/ckeditor5-ui/tests/manual/tickets/126/1.js
  57. 2 2
      packages/ckeditor5-ui/tests/manual/tickets/170/1.js
  58. 2 3
      packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js
  59. 2 1
      packages/ckeditor5-ui/tests/panel/balloon/contextualballoon.js
  60. 90 96
      packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js
  61. 125 26
      packages/ckeditor5-ui/tests/template.js
  62. 0 2
      packages/ckeditor5-ui/tests/toolbar/contextual/contextualtoolbar.js
  63. 4 4
      packages/ckeditor5-ui/tests/toolbar/enabletoolbarkeyboardfocus.js
  64. 1 1
      packages/ckeditor5-ui/tests/toolbar/toolbarseparatorview.js
  65. 28 18
      packages/ckeditor5-ui/tests/toolbar/toolbarview.js
  66. 1 0
      packages/ckeditor5-ui/tests/tooltip/tooltipview.js
  67. 103 51
      packages/ckeditor5-ui/tests/view.js
  68. 8 63
      packages/ckeditor5-ui/tests/viewcollection.js
  69. 2 2
      packages/ckeditor5-ui/theme/components/panel/stickypanel.scss

+ 2 - 2
packages/ckeditor5-ui/docs/_snippets/examples/bootstrap-ui-inner.js

@@ -59,8 +59,8 @@ export default class BootstrapEditor extends StandardEditor {
 
 			resolve(
 				editor.initPlugins()
-					// Initialize the editable view in DOM first.
-					.then( () => editable.init() )
+					// Render the editable view in DOM first.
+					.then( () => editable.render() )
 					// Replace the editor#element with editor.editable#element.
 					.then( () => editor._elementReplacer.replace( element, editable.element ) )
 					// Handle the UI of the editor.

+ 2 - 2
packages/ckeditor5-ui/docs/framework/guides/external-ui.md

@@ -83,8 +83,8 @@ export default class BootstrapEditor extends StandardEditor {
 
 			resolve(
 				editor.initPlugins()
-					// Initialize the editable view in DOM first.
-					.then( () => editable.init() )
+					// Render the editable view in DOM first.
+					.then( () => editable.render() )
 					// Replace the editor#element with editor.editable#element.
 					.then( () => editor._elementReplacer.replace( element, editable.element ) )
 					// Handle the UI of the editor.

+ 1 - 1
packages/ckeditor5-ui/src/bindings/preventdefault.js

@@ -17,7 +17,7 @@
  *
  *		// ...
  *
- *		this.template = new Template( {
+ *		this.setTemplate( {
  *			tag: 'div',
  *
  *			on: {

+ 61 - 34
packages/ckeditor5-ui/src/button/buttonview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 import IconView from '../icon/iconview';
 import TooltipView from '../tooltip/tooltipview';
 
@@ -26,7 +25,7 @@ import { getEnvKeystrokeText } from '@ckeditor/ckeditor5-utils/src/keyboard';
  *			withText: true
  *		} );
  *
- *		view.init();
+ *		view.render();
  *
  *		document.body.append( view.element );
  *
@@ -39,6 +38,8 @@ export default class ButtonView extends View {
 	constructor( locale ) {
 		super( locale );
 
+		const bind = this.bindTemplate;
+
 		/**
 		 * The label of the button view visible to the user when {@link #withText} is `true`.
 		 * It can also be used to create a {@link #tooltip}.
@@ -151,6 +152,30 @@ export default class ButtonView extends View {
 		 */
 		this.set( 'tabindex', -1 );
 
+		/**
+		 * Collection of the child views inside of the button {@link #element}.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.children = this.createCollection();
+
+		/**
+		 * Tooltip of the button view. It is configurable using the {@link #tooltip tooltip attribute}.
+		 *
+		 * @readonly
+		 * @member {module:ui/tooltip/tooltipview~TooltipView} #tooltipView
+		 */
+		this.tooltipView = this._createTooltipView();
+
+		/**
+		 * Label of the button view. It is configurable using the {@link #label label attribute}.
+		 *
+		 * @readonly
+		 * @member {module:ui/view~View} #labelView
+		 */
+		this.labelView = this._createLabelView();
+
 		/**
 		 * Tooltip of the button bound to the template.
 		 *
@@ -167,14 +192,6 @@ export default class ButtonView extends View {
 			this._getTooltipString.bind( this )
 		);
 
-		/**
-		 * Tooltip of the button view. It is configurable using the {@link #tooltip tooltip attribute}.
-		 *
-		 * @readonly
-		 * @member {module:ui/tooltip/tooltipview~TooltipView} #tooltipView
-		 */
-		this.tooltipView = this._createTooltipView();
-
 		/**
 		 * (Optional) The icon view of the button. Only present when the {@link #icon icon attribute} is defined.
 		 *
@@ -182,9 +199,7 @@ export default class ButtonView extends View {
 		 * @member {module:ui/icon/iconview~IconView} #iconView
 		 */
 
-		const bind = this.bindTemplate;
-
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'button',
 
 			attributes: {
@@ -199,22 +214,7 @@ export default class ButtonView extends View {
 				tabindex: bind.to( 'tabindex' )
 			},
 
-			children: [
-				{
-					tag: 'span',
-
-					attributes: {
-						class: [ 'ck-button__label' ]
-					},
-
-					children: [
-						{
-							text: bind.to( 'label' )
-						}
-					]
-				},
-				this.tooltipView
-			],
+			children: this.children,
 
 			on: {
 				mousedown: bind.to( evt => {
@@ -246,18 +246,19 @@ export default class ButtonView extends View {
 	/**
 	 * @inheritDoc
 	 */
-	init() {
+	render() {
+		super.render();
+
 		if ( this.icon ) {
 			const iconView = this.iconView = new IconView();
 
 			iconView.bind( 'content' ).to( this, 'icon' );
-			this.element.insertBefore( iconView.element, this.element.firstChild );
 
-			// Make sure the icon will be destroyed along with the button.
-			this.addChildren( iconView );
+			this.children.add( iconView );
 		}
 
-		super.init();
+		this.children.add( this.tooltipView );
+		this.children.add( this.labelView );
 	}
 
 	/**
@@ -283,6 +284,32 @@ export default class ButtonView extends View {
 		return tooltipView;
 	}
 
+	/**
+	 * Creates a label view instance and binds it with button attributes.
+	 *
+	 * @private
+	 * @returns {module:ui/view~View}
+	 */
+	_createLabelView() {
+		const labelView = new View();
+
+		labelView.setTemplate( {
+			tag: 'span',
+
+			attributes: {
+				class: [ 'ck-button__label' ]
+			},
+
+			children: [
+				{
+					text: this.bindTemplate.to( 'label' )
+				}
+			]
+		} );
+
+		return labelView;
+	}
+
 	/**
 	 * Gets the text for the {@link #tooltipView} from the combination of
 	 * {@link #tooltip}, {@link #label} and {@link #keystroke} attributes.

+ 1 - 1
packages/ckeditor5-ui/src/dropdown/createdropdown.js

@@ -24,7 +24,7 @@ import DropdownPanelView from './dropdownpanelview';
  *
  *		const dropdown = createDropdown( model );
  *
- *		dropdown.init();
+ *		dropdown.render();
  *
  *		// Will render a dropdown labeled "A dropdown" with an empty panel.
  *		document.body.appendChild( dropdown.element );

+ 1 - 2
packages/ckeditor5-ui/src/dropdown/dropdownpanelview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 
 /**
  * The dropdown panel view class.
@@ -46,7 +45,7 @@ export default class DropdownPanelView extends View {
 		 */
 		this.children = this.createCollection();
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'div',
 
 			attributes: {

+ 12 - 13
packages/ckeditor5-ui/src/dropdown/dropdownview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
 
@@ -25,7 +24,7 @@ import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
  *			withText: true
  *		} );
  *
- *		dropdown.init();
+ *		dropdown.render();
  *
  *		// Will render a dropdown with a panel containing a "Content of the panel" text.
  *		document.body.appendChild( dropdown.element );
@@ -46,7 +45,7 @@ export default class DropdownView extends View {
 		// Extend button's template before it's registered as a child of the dropdown because
 		// by doing so, its #element is rendered and any post–render template extension will
 		// not be reflected in DOM.
-		Template.extend( buttonView.template, {
+		buttonView.extendTemplate( {
 			attributes: {
 				class: [
 					'ck-dropdown__button'
@@ -106,7 +105,7 @@ export default class DropdownView extends View {
 		 */
 		this.keystrokes = new KeystrokeHandler();
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'div',
 
 			attributes: {
@@ -120,20 +119,22 @@ export default class DropdownView extends View {
 				panelView
 			]
 		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
 
 		// Toggle the the dropdown when it's button has been clicked.
-		this.listenTo( buttonView, 'execute', () => {
+		this.listenTo( this.buttonView, 'execute', () => {
 			this.isOpen = !this.isOpen;
 		} );
 
 		// Toggle the visibility of the panel when the dropdown becomes open.
-		panelView.bind( 'isVisible' ).to( this, 'isOpen' );
-	}
+		this.panelView.bind( 'isVisible' ).to( this, 'isOpen' );
 
-	/**
-	 * @inheritDoc
-	 */
-	init() {
 		// Listen for keystrokes coming from within #element.
 		this.keystrokes.listenTo( this.element );
 
@@ -167,8 +168,6 @@ export default class DropdownView extends View {
 		// Close the dropdown using the arrow left/escape key.
 		this.keystrokes.set( 'arrowleft', closeDropdown );
 		this.keystrokes.set( 'esc', closeDropdown );
-
-		super.init();
 	}
 
 	/**

+ 1 - 3
packages/ckeditor5-ui/src/dropdown/list/createlistdropdown.js

@@ -31,10 +31,9 @@ import createDropdown from '../createdropdown';
  *
  *		const dropdown = createListDropdown( model, locale );
  *
- *		dropdown.init();
- *
  *		// Will render a dropdown labeled "A dropdown" with a list in the panel
  *		// containing two items.
+ *		dropdown.render()
  *		document.body.appendChild( dropdown.element );
  *
  * The model instance remains in control of the dropdown after it has been created. E.g. changes to the
@@ -54,7 +53,6 @@ import createDropdown from '../createdropdown';
  */
 export default function createListDropdown( model, locale ) {
 	const dropdownView = createDropdown( model, locale );
-
 	const listView = dropdownView.listView = new ListView( locale );
 
 	listView.items.bindTo( model.items ).using( itemModel => {

+ 6 - 7
packages/ckeditor5-ui/src/editableui/editableuiview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 
 /**
  * The editable UI view class.
@@ -32,7 +31,7 @@ export default class EditableUIView extends View {
 			this.element = this.editableElement = editableElement;
 		}
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'div',
 			attributes: {
 				class: [
@@ -77,17 +76,17 @@ export default class EditableUIView extends View {
 	}
 
 	/**
-	 * Initializes the view by either applying the {@link #template} to the existing
+	 * Renders the view by either applying the {@link #template} to the existing
 	 * {@link #editableElement} or assigning {@link #element} as {@link #editableElement}.
 	 */
-	init() {
+	render() {
+		super.render();
+
 		if ( this.externalElement ) {
-			this.template.apply( this.externalElement );
+			this.template.apply( this.element = this.externalElement );
 		} else {
 			this.editableElement = this.element;
 		}
-
-		super.init();
 	}
 
 	/**

+ 1 - 2
packages/ckeditor5-ui/src/editableui/inline/inlineeditableuiview.js

@@ -8,7 +8,6 @@
  */
 
 import EditableUIView from '../../editableui/editableuiview';
-import Template from '../../template';
 
 /**
  * The inline editable UI class implementing an inline {@link module:ui/editableui/editableuiview~EditableUIView}.
@@ -42,7 +41,7 @@ export default class InlineEditableUIView extends EditableUIView {
 			return t( 'Rich Text Editor, %0', [ value ] );
 		};
 
-		Template.extend( this.template, {
+		this.extendTemplate( {
 			attributes: {
 				role: 'textbox',
 				'aria-label': bind.to( 'name', getLabel ),

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

@@ -9,7 +9,6 @@
 
 import EditorUIView from '../../editorui/editoruiview';
 import uid from '@ckeditor/ckeditor5-utils/src/uid';
-import Template from '../../template';
 
 /**
  * The boxed editor UI view class. This class represents an editor interface
@@ -47,7 +46,7 @@ export default class BoxedEditorUIView extends EditorUIView {
 		 */
 		this.main = this.createCollection();
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'div',
 
 			attributes: {

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

@@ -46,9 +46,10 @@ export default class EditorUIView extends View {
 	/**
 	 * @inheritDoc
 	 */
-	init() {
+	render() {
+		super.render();
+
 		this._renderBodyCollection();
-		super.init();
 	}
 
 	/**

+ 25 - 6
packages/ckeditor5-ui/src/icon/iconview.js

@@ -10,7 +10,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 
 /**
  * The icon view class.
@@ -32,7 +31,7 @@ export default class IconView extends View {
 		 * @observable
 		 * @member {String} #content
 		 */
-		this.set( 'content' );
+		this.set( 'content', '' );
 
 		/**
 		 * This attribute specifies the boundaries to which the
@@ -44,7 +43,7 @@ export default class IconView extends View {
 		 */
 		this.set( 'viewBox', '0 0 20 20' );
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'svg',
 			ns: 'http://www.w3.org/2000/svg',
 			attributes: {
@@ -52,17 +51,37 @@ export default class IconView extends View {
 				viewBox: bind.to( 'viewBox' )
 			}
 		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		this._updateXMLContent();
 
 		// This is a hack for lack of innerHTML binding.
 		// See: https://github.com/ckeditor/ckeditor5-ui/issues/99.
-		this.on( 'change:content', ( evt, name, value ) => {
+		this.on( 'change:content', () => this._updateXMLContent() );
+	}
+
+	/**
+	 * Updates the {@link #element} with the value of {@link #content}.
+	 *
+	 * @private
+	 */
+	_updateXMLContent() {
+		if ( this.content ) {
 			const svg = new DOMParser()
-				.parseFromString( value.trim(), 'image/svg+xml' )
+				.parseFromString( this.content.trim(), 'image/svg+xml' )
 				.firstChild;
 
+			this.element.innerHTML = '';
+
 			while ( svg.childNodes.length > 0 ) {
 				this.element.appendChild( svg.childNodes[ 0 ] );
 			}
-		} );
+		}
 	}
 }

+ 8 - 34
packages/ckeditor5-ui/src/iframe/iframeview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 
 /**
  * The iframe view class.
@@ -26,7 +25,7 @@ export default class IframeView extends View {
 
 		const bind = this.bindTemplate;
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'iframe',
 			attributes: {
 				class: [ 'ck-reset_all' ],
@@ -38,45 +37,20 @@ export default class IframeView extends View {
 				load: bind.to( 'loaded' )
 			}
 		} );
-
-		/**
-		 * A promise returned by {@link #init} since iframe loading may be asynchronous.
-		 *
-		 * **Note**: Listening to `load` in {@link #init} makes no sense because at this point
-		 * the element is already in the DOM and the `load` event might already be fired.
-		 *
-		 * See {@link #_iframeDeferred}.
-		 *
-		 * @private
-		 * @member {Promise}
-		 */
-		this._iframePromise = new Promise( ( resolve, reject ) => {
-			/**
-			 * A deferred object used to resolve the iframe promise associated with
-			 * asynchronous loading of `contentDocument`. See {@link #_iframePromise}.
-			 *
-			 * @private
-			 * @member {Object}
-			 */
-			this._iframeDeferred = { resolve, reject };
-		} );
-
-		this.on( 'loaded', () => {
-			this._iframeDeferred.resolve();
-		} );
 	}
 
 	/**
-	 * Initializes iframe {@link #element} and returns a `Promise` for asynchronous
-	 * child `contentDocument` loading process. See {@link #_iframePromise}.
+	 * Renders the iframe's {@link #element} and returns a `Promise` for asynchronous
+	 * child `contentDocument` loading process.
 	 *
 	 * @returns {Promise} A promise which resolves once the iframe `contentDocument` has
 	 * been {@link #event:loaded}.
 	 */
-	init() {
-		super.init();
-
-		return this._iframePromise;
+	render() {
+		return new Promise( resolve => {
+			this.on( 'loaded', resolve );
+			super.render();
+		} );
 	}
 }
 

+ 8 - 2
packages/ckeditor5-ui/src/inputtext/inputtextview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 
 /**
  * The text input view class.
@@ -56,7 +55,7 @@ export default class InputTextView extends View {
 
 		const bind = this.bindTemplate;
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'input',
 			attributes: {
 				type: 'text',
@@ -69,6 +68,13 @@ export default class InputTextView extends View {
 				readonly: bind.to( 'isReadOnly' )
 			}
 		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
 
 		// Note: `value` cannot be an HTML attribute, because it doesn't change HTMLInputElement value once changed.
 		this.on( 'change:value', ( evt, propertyName, value ) => {

+ 1 - 2
packages/ckeditor5-ui/src/label/labelview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 
 /**
  * The label view class.
@@ -40,7 +39,7 @@ export default class LabelView extends View {
 
 		const bind = this.bindTemplate;
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'label',
 			attributes: {
 				class: [

+ 1 - 2
packages/ckeditor5-ui/src/labeledinput/labeledinputview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 import uid from '@ckeditor/ckeditor5-utils/src/uid';
 
 import LabelView from '../label/labelview';
@@ -70,7 +69,7 @@ export default class LabeledInputView extends View {
 
 		const bind = this.bindTemplate;
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'div',
 			attributes: {
 				class: [

+ 4 - 3
packages/ckeditor5-ui/src/list/listitemview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
 
 /**
@@ -42,7 +41,7 @@ export default class ListItemView extends View {
 
 		const bind = this.bindTemplate;
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'li',
 
 			attributes: {
@@ -104,7 +103,9 @@ export default class ListItemView extends View {
 	/**
 	 * @inheritDoc
 	 */
-	init() {
+	render() {
+		super.render();
+
 		const onKeystrokePress = ( data, cancel ) => {
 			this.fire( 'execute' );
 			cancel();

+ 13 - 9
packages/ckeditor5-ui/src/list/listview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import FocusCycler from '../focuscycler';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
@@ -69,7 +68,7 @@ export default class ListView extends View {
 			}
 		} );
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'ul',
 
 			attributes: {
@@ -81,6 +80,18 @@ export default class ListView extends View {
 
 			children: this.items
 		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		// Items added before rendering should be known to the #focusTracker.
+		for ( const item of this.items ) {
+			this.focusTracker.add( item.element );
+		}
 
 		this.items.on( 'add', ( evt, item ) => {
 			this.focusTracker.add( item.element );
@@ -89,16 +100,9 @@ export default class ListView extends View {
 		this.items.on( 'remove', ( evt, item ) => {
 			this.focusTracker.remove( item.element );
 		} );
-	}
 
-	/**
-	 * @inheritDoc
-	 */
-	init() {
 		// Start listening for the keystrokes coming from #element.
 		this.keystrokes.listenTo( this.element );
-
-		super.init();
 	}
 
 	/**

+ 4 - 5
packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../../view';
-import Template from '../../template';
 import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
 import isRange from '@ckeditor/ckeditor5-utils/src/dom/isrange';
 import isElement from '@ckeditor/ckeditor5-utils/src/lib/lodash/isElement';
@@ -40,7 +39,7 @@ const defaultLimiterElement = global.document.body;
  *		const childView = new ChildView();
  *		const positions = BalloonPanelView.defaultPositions;
  *
- *		panel.init();
+ *		panel.render();
  *
  *		// Add a child view to the panel's content collection.
  *		panel.content.add( childView );
@@ -144,7 +143,7 @@ export default class BalloonPanelView extends View {
 		 */
 		this.content = this.createCollection();
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'div',
 			attributes: {
 				class: [
@@ -194,7 +193,7 @@ export default class BalloonPanelView extends View {
 	 *		const panel = new BalloonPanelView( locale );
 	 *		const positions = BalloonPanelView.defaultPositions;
 	 *
-	 *		panel.init();
+	 *		panel.render();
 	 *
 	 *		// Attach the panel to an element with the "target" id DOM.
 	 *		panel.attachTo( {
@@ -249,7 +248,7 @@ export default class BalloonPanelView extends View {
 	 *		const panel = new BalloonPanelView( locale );
 	 *		const positions = BalloonPanelView.defaultPositions;
 	 *
-	 *		panel.init();
+	 *		panel.render();
 	 *
 	 *		// Pin the panel to an element with the "target" id DOM.
 	 *		panel.pin( {

+ 3 - 3
packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js

@@ -79,11 +79,11 @@ export default class ContextualBalloon extends Plugin {
 		 */
 		this._stack = new Map();
 
-		// Editor should be focused when contextual balloon is focused.
-		this.editor.ui.focusTracker.add( this.view.element );
-
 		// Add balloon panel view to editor `body` collection and wait until view will be ready.
 		this.editor.ui.view.body.add( this.view );
+
+		// Editor should be focused when contextual balloon is focused.
+		this.editor.ui.focusTracker.add( this.view.element );
 	}
 
 	/**

+ 64 - 48
packages/ckeditor5-ui/src/panel/sticky/stickypanelview.js

@@ -141,19 +141,48 @@ export default class StickyPanelView extends View {
 		 * @member {Object} #_limiterRect
 		 */
 
-		this.template = new Template( {
+		/**
+		 * A dummy element which visually fills the space as long as the
+		 * actual panel is sticky. It prevents flickering of the UI.
+		 *
+		 * @protected
+		 * @property {HTMLElement}
+		 */
+		this._contentPanelPlaceholder = new Template( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-sticky-panel__placeholder'
+				],
+				style: {
+					display: bind.to( 'isSticky', isSticky => isSticky ? 'block' : 'none' ),
+					height: bind.to( 'isSticky', isSticky => {
+						return isSticky ? toPx( this._panelRect.height ) : null;
+					} )
+				}
+			}
+		} ).render();
+
+		/**
+		 * The panel which accepts children into {@link #content} collection.
+		 * Also an element which is positioned when {@link #isSticky}.
+		 *
+		 * @protected
+		 * @property {HTMLElement}
+		 */
+		this._contentPanel = new Template( {
 			tag: 'div',
 
 			attributes: {
 				class: [
-					'ck-sticky-panel',
+					'ck-sticky-panel__content',
 					// Toggle class of the panel when "sticky" state changes in the view.
-					bind.if( 'isSticky', 'ck-sticky-panel_sticky' ),
-					bind.if( '_isStickyToTheLimiter', 'ck-sticky-panel_sticky_bottom-limit' ),
+					bind.if( 'isSticky', 'ck-sticky-panel__content_sticky' ),
+					bind.if( '_isStickyToTheLimiter', 'ck-sticky-panel__content_sticky_bottom-limit' ),
 				],
 				style: {
 					width: bind.to( 'isSticky', isSticky => {
-						return isSticky ? toPx( this._elementPlaceholder.getBoundingClientRect().width ) : null;
+						return isSticky ? toPx( this._contentPanelPlaceholder.getBoundingClientRect().width ) : null;
 					} ),
 
 					top: bind.to( '_hasViewportTopOffset', _hasViewportTopOffset => {
@@ -169,38 +198,27 @@ export default class StickyPanelView extends View {
 			},
 
 			children: this.content
-		} );
+		} ).render();
 
-		/**
-		 * A dummy element which visually fills the space as long as the
-		 * actual panel is sticky. It prevents flickering of the UI.
-		 *
-		 * @private
-		 * @property {HTMLElement}
-		 */
-		this._elementPlaceholder = new Template( {
+		this.setTemplate( {
 			tag: 'div',
 			attributes: {
 				class: [
-					'ck-sticky-panel__placeholder'
-				],
-				style: {
-					display: bind.to( 'isSticky', isSticky => isSticky ? 'block' : 'none' ),
-					height: bind.to( 'isSticky', isSticky => {
-						return isSticky ? toPx( this._panelRect.height ) : null;
-					} )
-				}
-			}
-		} ).render();
+					'ck-sticky-panel'
+				]
+			},
+			children: [
+				this._contentPanelPlaceholder,
+				this._contentPanel
+			]
+		} );
 	}
 
 	/**
 	 * @inheritDoc
 	 */
-	init() {
-		super.init();
-
-		this.element.parentNode.insertBefore( this._elementPlaceholder, this.element );
+	render() {
+		super.render();
 
 		// Check if the panel should go into the sticky state immediately.
 		this._checkIfShouldBeSticky();
@@ -216,14 +234,6 @@ export default class StickyPanelView extends View {
 		} );
 	}
 
-	/**
-	 * Destroys the panel and removes the {@link #_elementPlaceholder}.
-	 */
-	destroy() {
-		super.destroy();
-		this._elementPlaceholder.remove();
-	}
-
 	/**
 	 * Analyzes the environment to decide whether the panel should
 	 * be sticky or not.
@@ -231,18 +241,24 @@ export default class StickyPanelView extends View {
 	 * @protected
 	 */
 	_checkIfShouldBeSticky() {
-		const limiterRect = this._limiterRect = this.limiterElement.getBoundingClientRect();
-		const panelRect = this._panelRect = this.element.getBoundingClientRect();
-
-		// The panel must be active to become sticky.
-		this.isSticky = this.isActive &&
-			// The limiter's top edge must be beyond the upper edge of the visible viewport (+the viewportTopOffset).
-			limiterRect.top < this.viewportTopOffset &&
-			// The model#limiterElement's height mustn't be smaller than the panel's height and model#limiterBottomOffset.
-			// There's no point in entering the sticky mode if the model#limiterElement is very, very small, because
-			// it would immediately set model#_isStickyToTheLimiter true and, given model#limiterBottomOffset, the panel
-			// would be positioned before the model#limiterElement.
-			this._panelRect.height + this.limiterBottomOffset < limiterRect.height;
+		const panelRect = this._panelRect = this._contentPanel.getBoundingClientRect();
+		let limiterRect;
+
+		if ( !this.limiterElement ) {
+			this.isSticky = false;
+		} else {
+			limiterRect = this._limiterRect = this.limiterElement.getBoundingClientRect();
+
+			// The panel must be active to become sticky.
+			this.isSticky = this.isActive &&
+				// The limiter's top edge must be beyond the upper edge of the visible viewport (+the viewportTopOffset).
+				limiterRect.top < this.viewportTopOffset &&
+				// The model#limiterElement's height mustn't be smaller than the panel's height and model#limiterBottomOffset.
+				// There's no point in entering the sticky mode if the model#limiterElement is very, very small, because
+				// it would immediately set model#_isStickyToTheLimiter true and, given model#limiterBottomOffset, the panel
+				// would be positioned before the model#limiterElement.
+				this._panelRect.height + this.limiterBottomOffset < limiterRect.height;
+		}
 
 		// Stick the panel to the top edge of the viewport simulating CSS position:sticky.
 		// TODO: Possibly replaced by CSS in the future http://caniuse.com/#feat=css-sticky

+ 70 - 13
packages/ckeditor5-ui/src/template.js

@@ -16,6 +16,7 @@ import View from './view';
 import ViewCollection from './viewcollection';
 import cloneDeepWith from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeepWith';
 import isObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isObject';
+import isDomNode from '@ckeditor/ckeditor5-utils/src/dom/isdomnode';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 
 const xhtmlNs = 'http://www.w3.org/1999/xhtml';
@@ -98,12 +99,13 @@ export default class Template {
 		 */
 
 		/**
-		 * Children of the template, sub–templates. Each one is an independent
-		 * instance of {@link ~Template}.
+		 * Children of the template. They can be either:
+		 * * an independent instances of {@link ~Template} (sub–templates),
+		 * * native DOM Nodes.
 		 *
 		 * **Note**: This property only makes sense when {@link #tag} is defined.
 		 *
-		 * @member {module:utils/collection~Collection.<module:ui/template~Template>} #children
+		 * @member {Array.<module:ui/template~Template|Node>} #children
 		 */
 
 		/**
@@ -209,6 +211,50 @@ export default class Template {
 		this._revertTemplateFromNode( node, this._revertData );
 	}
 
+	/**
+	 * Returns an iterator which traverses the template in search of {@link module:ui/view~View}
+	 * instances and returns them one by one.
+	 *
+	 *		const viewFoo = new View();
+	 *		const viewBar = new View();
+	 *		const viewBaz = new View();
+	 *		const template = new Template( {
+	 *			tag: 'div',
+	 *			children: [
+	 *				viewFoo,
+	 *				{
+	 *					tag: 'div',
+	 *					children: [
+	 *						viewBar
+	 *					]
+	 *				},
+	 *				viewBaz
+	 *			]
+	 *		} );
+	 *
+	 *		// Logs: viewFoo, viewBar, viewBaz
+	 *		for ( const view of template.getViews() ) {
+	 *			console.log( view );
+	 *		}
+	 *
+	 * @returns {Iterator.<module:ui/view~View>}
+	 */
+	* getViews() {
+		function* search( def ) {
+			if ( def.children ) {
+				for ( const child of def.children ) {
+					if ( isView( child ) ) {
+						yield child;
+					} else {
+						yield* search( child );
+					}
+				}
+			}
+		}
+
+		yield* search( this );
+	}
+
 	/**
 	 * An entry point to the interface which binds DOM nodes to
 	 * {@link module:utils/observablemixin~Observable observables}.
@@ -305,7 +351,7 @@ export default class Template {
 	 *		} );
 	 *
 	 *		// Child extension.
-	 *		Template.extend( template.children.get( 0 ), {
+	 *		Template.extend( template.children[ 0 ], {
 	 *			attributes: {
 	 *				class: 'd'
 	 *			}
@@ -644,14 +690,21 @@ export default class Template {
 				if ( !isApplying ) {
 					child.setParent( node );
 
+					// Note: ViewCollection renders its children.
 					for ( const view of child ) {
 						container.appendChild( view.element );
 					}
 				}
 			} else if ( isView( child ) ) {
 				if ( !isApplying ) {
+					if ( !child.isRendered ) {
+						child.render();
+					}
+
 					container.appendChild( child.element );
 				}
+			} else if ( isDomNode( child ) ) {
+				container.appendChild( child );
 			} else {
 				if ( isApplying ) {
 					const revertData = data.revertData;
@@ -1117,17 +1170,17 @@ function normalize( def ) {
 			normalizeAttributes( def.attributes );
 		}
 
-		const children = new ViewCollection();
+		const children = [];
 
 		if ( def.children ) {
 			if ( isViewCollection( def.children ) ) {
-				children.add( def.children );
+				children.push( def.children );
 			} else {
 				for ( const child of def.children ) {
-					if ( isTemplate( child ) || isView( child ) ) {
-						children.add( child );
+					if ( isTemplate( child ) || isView( child ) || isDomNode( child ) ) {
+						children.push( child );
 					} else {
-						children.add( new Template( child ) );
+						children.push( new Template( child ) );
 					}
 				}
 			}
@@ -1336,7 +1389,7 @@ function extendTemplate( template, def ) {
 		let childIndex = 0;
 
 		for ( const childDef of def.children ) {
-			extendTemplate( template.children.get( childIndex++ ), childDef );
+			extendTemplate( template.children[ childIndex++ ], childDef );
 		}
 	}
 }
@@ -1436,11 +1489,12 @@ function shouldExtend( attrName ) {
  *			}
  *		} );
  *
- * A {@link module:ui/view~View} or another {@link module:ui/template~Template} can also become a child
- * of a template. In case of a view {@link module:ui/view~View#element} is used:
+ * A {@link module:ui/view~View}, another {@link module:ui/template~Template} or a native DOM node
+ * can also become a child of a template. When a view is passed, its {@link module:ui/view~View#element} is used:
  *
  *		const view = new SomeView();
  *		const childTemplate = new Template( { ... } );
+ *		const childNode = document.createElement( 'b' );
  *
  *		new Template( {
  *			tag: 'p',
@@ -1450,7 +1504,10 @@ function shouldExtend( attrName ) {
  *				view,
  *
  * 				// The output of childTemplate.render() will be added here.
- *				childTemplate
+ *				childTemplate,
+ *
+ *				// Native DOM nodes are included directly in the rendered output.
+ *				childNode
  *			]
  *		} );
  *

+ 3 - 2
packages/ckeditor5-ui/src/toolbar/contextual/contextualtoolbar.js

@@ -7,7 +7,6 @@
  * @module ui/toolbar/contextual/contextualtoolbar
  */
 
-import Template from '../../template';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ContextualBalloon from '../../panel/balloon/contextualballoon';
 import ToolbarView from '../toolbarview';
@@ -51,7 +50,7 @@ export default class ContextualToolbar extends Plugin {
 		 */
 		this.toolbarView = new ToolbarView( editor.locale );
 
-		Template.extend( this.toolbarView.template, {
+		this.toolbarView.extendTemplate( {
 			attributes: {
 				class: [
 					'ck-editor-toolbar',
@@ -60,6 +59,8 @@ export default class ContextualToolbar extends Plugin {
 			}
 		} );
 
+		this.toolbarView.render();
+
 		/**
 		 * The contextual balloon plugin instance.
 		 *

+ 1 - 2
packages/ckeditor5-ui/src/toolbar/toolbarseparatorview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 
 /**
  * The toolbar separator view class.
@@ -22,7 +21,7 @@ export default class ToolbarSeparatorView extends View {
 	constructor( locale ) {
 		super( locale );
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'span',
 			attributes: {
 				class: [

+ 13 - 9
packages/ckeditor5-ui/src/toolbar/toolbarview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import FocusCycler from '../focuscycler';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
@@ -72,7 +71,7 @@ export default class ToolbarView extends View {
 			}
 		} );
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'div',
 			attributes: {
 				class: [
@@ -87,6 +86,18 @@ export default class ToolbarView extends View {
 				mousedown: preventDefault( this )
 			}
 		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		// Items added before rendering should be known to the #focusTracker.
+		for ( const item of this.items ) {
+			this.focusTracker.add( item.element );
+		}
 
 		this.items.on( 'add', ( evt, item ) => {
 			this.focusTracker.add( item.element );
@@ -95,16 +106,9 @@ export default class ToolbarView extends View {
 		this.items.on( 'remove', ( evt, item ) => {
 			this.focusTracker.remove( item.element );
 		} );
-	}
 
-	/**
-	 * @inheritDoc
-	 */
-	init() {
 		// Start listening for the keystrokes coming from #element.
 		this.keystrokes.listenTo( this.element );
-
-		super.init();
 	}
 
 	/**

+ 1 - 2
packages/ckeditor5-ui/src/tooltip/tooltipview.js

@@ -8,7 +8,6 @@
  */
 
 import View from '../view';
-import Template from '../template';
 
 /**
  * The tooltip view class.
@@ -53,7 +52,7 @@ export default class TooltipView extends View {
 
 		const bind = this.bindTemplate;
 
-		this.template = new Template( {
+		this.setTemplate( {
 			tag: 'span',
 			attributes: {
 				class: [

+ 141 - 144
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.
@@ -37,7 +37,7 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  *				// Views define their interface (state) using observable attributes.
  *				this.set( 'elementClass', 'bar' );
  *
- *				this.template = new Template( {
+ *				this.setTemplate( {
  *					tag: 'p',
  *
  *					// The element of the view can be defined with its children.
@@ -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,11 +85,52 @@ 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.setTemplate( {
+		 *					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.
 		 *
@@ -112,17 +152,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}.
 		 *
@@ -147,18 +176,11 @@ 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}.
@@ -168,54 +190,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.
@@ -237,7 +211,7 @@ export default class View {
 	 *				 	isEnabled: true
 	 *				 } );
 	 *
-	 *				this.template = new Template( {
+	 *				this.setTemplate( {
 	 *					tag: 'p',
 	 *
 	 *					attributes: {
@@ -277,7 +251,7 @@ export default class View {
 	 *
 	 *				this.items = this.createCollection();
  	 *
-	 *				this.template = new Template( {
+	 *				this.setTemplate( {
 	 *					tag: 'p',
 	 *
 	 *					// `items` collection will render here.
@@ -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,10 +284,10 @@ 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}.
+	 * To revert this, use {@link #deregisterChildren}.
 	 *
 	 *		class SampleView extends View {
 	 *			constructor( locale ) {
@@ -322,23 +296,23 @@ export default class View {
 	 *				this.childA = new SomeChildView( locale );
 	 *				this.childB = new SomeChildView( locale );
 	 *
-	 *				this.template = new Template( { tag: 'p' } );
+	 *				this.setTemplate( { 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 );
@@ -353,11 +327,11 @@ export default class View {
 	 *				this.childA = new SomeChildView( locale );
 	 *				this.childB = new SomeChildView( locale );
 	 *
-	 *				this.template = new Template( {
+	 *				this.setTemplate( {
 	 *					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,68 +341,111 @@ 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 );
+		}
+	}
+
+	/**
+	 * Sets the {@link #template} of the view with with given definition.
+	 *
+	 * A shorthand for:
+	 *
+	 *		view.setTemplate( definition );
+	 *
+	 * @param {module:ui/template~TemplateDefinition} definition Definition of view's template.
+	 */
+	setTemplate( definition ) {
+		this.template = new Template( definition );
+	}
+
+	/**
+	 * {@link module:ui/template~Template.extend Extends} the {@link #template} of the view with
+	 * with given definition.
+	 *
+	 * A shorthand for:
+	 *
+	 *		Template.extend( view.template, definition );
+	 *
+	 * **Note**: Is requires the {@link #template} to be already set. See {@link #setTemplate}.
+	 *
+	 * @param {module:ui/template~TemplateDefinition} definition Definition which
+	 * extends the {@link #template}.
+	 */
+	extendTemplate( definition ) {
+		Template.extend( this.template, definition );
 	}
 
 	/**
-	 * 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, `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()`.
+	 * 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.
+	 *
+	 * 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() {
-	 *				this.template = new Template( {
+	 *				this.setTemplate( {
 	 *					// ...
 	 *				} );
 	 *			},
 	 *
-	 *			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();
 	 *
-	 *		Template.extend( view.template, {
+	 *		// Let's customize the view before it gets rendered.
+	 *		view.extendTemplate( {
 	 *			attributes: {
 	 *				class: [
 	 *					'additional-class'
@@ -436,30 +453,32 @@ 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 #element of the view.
+		if ( this.template ) {
+			this.element = this.template.render();
 
-		// Spread the word that this view is ready!
-		this.ready = true;
+			// 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 +490,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 );

+ 8 - 71
packages/ckeditor5-ui/src/viewcollection.js

@@ -23,16 +23,13 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  *		const viewA = new ChildView( locale );
  *		const viewB = new ChildView( locale );
  *
- * View collection manages view {@link module:ui/view~View#element elements}:
+ * View collection renders and manages view {@link module:ui/view~View#element elements}:
  *
- *		// parentView.element.children == [ viewA.element, vievB.element ]
  *		collection.add( viewA );
  *		collection.add( viewB );
  *
- * It handles initialization of the children:
- *
- *		// viewA.ready == viewB.ready == true;
- *		collection.init();
+ *		console.log( parentView.element.firsChild ); // -> viewA.element
+ *		console.log( parentView.element.lastChild ); // -> viewB.element
  *
  * It {@link module:ui/viewcollection~ViewCollection#delegate propagates} DOM events too:
  *
@@ -67,6 +64,10 @@ export default class ViewCollection extends Collection {
 
 		// Handle {@link module:ui/view~View#element} in DOM when a new view is added to the collection.
 		this.on( 'add', ( evt, view, index ) => {
+			if ( !view.isRendered ) {
+				view.render();
+			}
+
 			if ( view.element && this._parentElement ) {
 				this._parentElement.insertBefore( view.element, this._parentElement.children[ index ] );
 			}
@@ -87,16 +88,6 @@ export default class ViewCollection extends Collection {
 		 */
 		this.locale = locale;
 
-		/**
-		 * Set `true` when all views in the collection are {@link module:ui/view~View#ready}.
-		 * See the view {@link module:ui/view~View#init init} method.
-		 *
-		 * @readonly
-		 * @observable
-		 * @member {Boolean} #ready
-		 */
-		this.set( 'ready', false );
-
 		/**
 		 * A parent element within which child views are rendered and managed in DOM.
 		 *
@@ -106,66 +97,12 @@ export default class ViewCollection extends Collection {
 		this._parentElement = null;
 	}
 
-	/**
-	 * Initializes all child views in the collection by calling view {@link module:ui/view~View#init}
-	 * method.
-	 *
-	 * Once finished, sets {@link #ready} `true`.
-	 */
-	init() {
-		if ( this.ready ) {
-			/**
-			 * This ViewCollection has already been initialized.
-			 *
-			 * @error ui-viewcollection-init-reinit
-			 */
-			throw new CKEditorError( 'ui-viewcollection-init-reinit: This ViewCollection has already been initialized.' );
-		}
-
-		this.map( v => v.init() );
-
-		this.ready = true;
-	}
-
 	/**
 	 * Destroys the view collection along with child views.
 	 * See the view {@link module:ui/view~View#destroy} method.
 	 */
 	destroy() {
-		this.map( v => v.destroy() );
-	}
-
-	/**
-	 * Adds a new child view to the collection. If the collection is
-	 * {@link module:ui/viewcollection~ViewCollection#ready}, the child view is also
-	 * {@link module:ui/view~View#init initialized} when added.
-	 *
-	 * Additionally, if the {@link #setParent parent element} of the collection has been set, the
-	 * {@link module:ui/view~View#element element} of the view is also added in DOM,
-	 * reflecting the order of the collection.
-	 *
-	 *		const collection = new ViewCollection();
-	 *		const parentElement = document.querySelector( '#container' );
-	 *		collection.setParent( parentElement );
-	 *
-	 *		const viewA = new View();
-	 *		const viewB = new View();
-	 *
-	 *		// parentElement.children == [ viewA.element, vievB.element ]
-	 *		collection.add( viewA );
-	 *		collection.add( viewB );
-	 *
-	 * See the {@link #remove} method.
-	 *
-	 * @param {module:ui/view~View} view A child view.
-	 * @param {Number} [index] Index at which the child will be added to the collection.
-	 */
-	add( view, index ) {
-		super.add( view, index );
-
-		if ( this.ready && !view.ready ) {
-			view.init();
-		}
+		this.map( view => view.destroy() );
 	}
 
 	/**

+ 2 - 2
packages/ckeditor5-ui/tests/_utils-tests/utils.js

@@ -26,13 +26,13 @@ describe( 'utils', () => {
 				expect( view.bar._parentElement ).to.equal( document.body.lastChild );
 			} );
 
-			it( 'is ready', () => {
+			it( 'is rendered', () => {
 				const view = testUtils.createTestUIView( {
 					foo: el => el.firstChild,
 					bar: el => el.lastChild,
 				} );
 
-				expect( view.ready ).to.be.true;
+				expect( view.isRendered ).to.be.true;
 			} );
 		} );
 	} );

+ 10 - 7
packages/ckeditor5-ui/tests/_utils/utils.js

@@ -12,21 +12,24 @@ import View from '../../src/view';
  */
 const utils = {
 	/**
-	 * Returns UI controller for given region/DOM selector pairs, which {@link ui.Controller#view}
+	 * Returns a view for a given region/DOM selector pairs, which {@link module:ui/view~View#element}
 	 * is `document.body`. It is useful for manual tests which engage various UI components and/or
-	 * UI {@link ui.Controller} instances, where initialization and the process of insertion into
+	 * UI {@link module:ui/view~View} instances, where initialization and the process of insertion into
 	 * DOM could be problematic i.e. because of the number of instances.
 	 *
 	 * Usage:
 	 *
-	 *		// Get the controller.
-	 *		const controller = testUtils.createTestUIView();
+	 *		// Get the view.
+	 *		const view = testUtils.createTestUIView( {
+	 *			'some-collection': '#collection'
+	 *		} );
 	 *
 	 *		// Then use it to organize and initialize children.
-	 *		controller.add( 'some-collection', childControllerInstance );
+	 *		view.add( 'some-collection', childControllerInstance );
 	 *
 	 * @param {Object} regions An object literal with `regionName: [DOM Selector|callback]` pairs.
-	 * See {@link ui.View#register}.
+	 *
+	 * See {@link module:ui/view~View#createCollection}.
 	 */
 	createTestUIView( regions ) {
 		const TestUIView = class extends View {
@@ -50,7 +53,7 @@ const utils = {
 
 		const view = new TestUIView();
 
-		view.init();
+		view.render();
 
 		return view;
 	}

+ 5 - 6
packages/ckeditor5-ui/tests/bindings/preventdefault.js

@@ -7,13 +7,12 @@
 
 import preventDefault from '../../src/bindings/preventdefault';
 import View from '../../src/view';
-import Template from '../../src/template';
 
 describe( 'preventDefault', () => {
 	it( 'prevents default of a native DOM event', () => {
 		const view = new View();
 
-		view.template = new Template( {
+		view.setTemplate( {
 			tag: 'div',
 
 			on: {
@@ -25,7 +24,7 @@ describe( 'preventDefault', () => {
 		const spy = sinon.spy( evt, 'preventDefault' );
 
 		// Render to enable bubbling.
-		view.element;
+		view.render();
 
 		view.element.dispatchEvent( evt );
 		sinon.assert.calledOnce( spy );
@@ -35,11 +34,11 @@ describe( 'preventDefault', () => {
 		const view = new View();
 		const child = new View();
 
-		child.template = new Template( {
+		child.setTemplate( {
 			tag: 'a'
 		} );
 
-		view.template = new Template( {
+		view.setTemplate( {
 			tag: 'div',
 
 			on: {
@@ -55,7 +54,7 @@ describe( 'preventDefault', () => {
 		const spy = sinon.spy( evt, 'preventDefault' );
 
 		// Render to enable bubbling.
-		view.element;
+		view.render();
 
 		child.element.dispatchEvent( evt );
 		sinon.assert.notCalled( spy );

+ 21 - 18
packages/ckeditor5-ui/tests/button/buttonview.js

@@ -9,6 +9,8 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import ButtonView from '../../src/button/buttonview';
 import IconView from '../../src/icon/iconview';
 import TooltipView from '../../src/tooltip/tooltipview';
+import View from '../../src/view';
+import ViewCollection from '../../src/viewcollection';
 
 testUtils.createSinonSandbox();
 
@@ -18,7 +20,22 @@ describe( 'ButtonView', () => {
 	beforeEach( () => {
 		locale = { t() {} };
 
-		return ( view = new ButtonView( locale ) ).init();
+		view = new ButtonView( locale );
+		view.render();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'creates view#children collection', () => {
+			expect( view.children ).to.be.instanceOf( ViewCollection );
+		} );
+
+		it( 'creates #tooltipView', () => {
+			expect( view.tooltipView ).to.be.instanceOf( TooltipView );
+		} );
+
+		it( 'creates #labelView', () => {
+			expect( view.labelView ).to.be.instanceOf( View );
+		} );
 	} );
 
 	describe( '<button> bindings', () => {
@@ -76,19 +93,13 @@ describe( 'ButtonView', () => {
 		} );
 
 		describe( 'tooltip', () => {
-			beforeEach( () => {
-				view = new ButtonView( locale );
-			} );
-
 			it( 'is initially set', () => {
-				expect( view.tooltipView ).to.be.instanceof( TooltipView );
-				expect( Array.from( view.template.children ) ).to.include( view.tooltipView );
+				expect( view.children.getIndex( view.tooltipView ) ).to.equal( 0 );
 			} );
 
 			it( 'it reacts to #tooltipPosition attribute', () => {
 				view.tooltip = 'foo';
 				view.icon = 'bar';
-				view.init();
 
 				expect( view.tooltipPosition ).to.equal( 's' );
 				expect( view.tooltipView.position ).to.equal( 's' );
@@ -102,7 +113,6 @@ describe( 'ButtonView', () => {
 					view.tooltip = true;
 					view.label = 'bar';
 					view.keystroke = 'A';
-					view.init();
 
 					expect( view.tooltipView.text ).to.equal( 'bar (A)' );
 				} );
@@ -111,7 +121,6 @@ describe( 'ButtonView', () => {
 					view.tooltip = false;
 					view.label = 'bar';
 					view.keystroke = 'A';
-					view.init();
 
 					expect( view.tooltipView.text ).to.equal( '' );
 				} );
@@ -120,7 +129,6 @@ describe( 'ButtonView', () => {
 					view.tooltip = true;
 					view.label = 'foo';
 					view.keystroke = 'B';
-					view.init();
 
 					expect( view.tooltipView.text ).to.equal( 'foo (B)' );
 
@@ -136,14 +144,12 @@ describe( 'ButtonView', () => {
 					view.tooltip = 'bar';
 					view.label = 'foo';
 					view.keystroke = 'A';
-					view.init();
 
 					expect( view.tooltipView.text ).to.equal( 'bar' );
 				} );
 
 				it( 'reacts to changes of #tooltip', () => {
 					view.tooltip = 'bar';
-					view.init();
 
 					expect( view.tooltipView.text ).to.equal( 'bar' );
 
@@ -157,7 +163,6 @@ describe( 'ButtonView', () => {
 					view.tooltip = ( l, k ) => `${ l } - ${ k }`;
 					view.label = 'foo';
 					view.keystroke = 'A';
-					view.init();
 
 					expect( view.tooltipView.text ).to.equal( 'foo - A' );
 				} );
@@ -166,7 +171,6 @@ describe( 'ButtonView', () => {
 					view.tooltip = ( l, k ) => `${ l } - ${ k }`;
 					view.label = 'foo';
 					view.keystroke = 'A';
-					view.init();
 
 					expect( view.tooltipView.text ).to.equal( 'foo - A' );
 
@@ -237,8 +241,8 @@ describe( 'ButtonView', () => {
 		it( 'is set when view#icon is defined', () => {
 			view = new ButtonView( locale );
 			view.icon = 'foo';
+			view.render();
 
-			view.init();
 			expect( view.element.childNodes ).to.have.length( 3 );
 			expect( view.element.childNodes[ 0 ] ).to.equal( view.iconView.element );
 
@@ -252,8 +256,7 @@ describe( 'ButtonView', () => {
 		it( 'is destroyed with the view', () => {
 			view = new ButtonView( locale );
 			view.icon = 'foo';
-
-			view.init();
+			view.render();
 
 			const spy = sinon.spy( view.iconView, 'destroy' );
 

+ 2 - 1
packages/ckeditor5-ui/tests/dropdown/dropdownpanelview.js

@@ -14,7 +14,8 @@ describe( 'DropdownPanelView', () => {
 	beforeEach( () => {
 		locale = { t() {} };
 
-		return ( view = new DropdownPanelView( locale ) ).init();
+		view = new DropdownPanelView( locale );
+		view.render();
 	} );
 
 	describe( 'constructor()', () => {

+ 5 - 4
packages/ckeditor5-ui/tests/dropdown/dropdownview.js

@@ -19,7 +19,8 @@ describe( 'DropdownView', () => {
 		buttonView = new ButtonView( locale );
 		panelView = new DropdownPanelView( locale );
 
-		return ( view = new DropdownView( locale, buttonView, panelView ) ).init();
+		view = new DropdownView( locale, buttonView, panelView );
+		view.render();
 	} );
 
 	describe( 'constructor()', () => {
@@ -92,7 +93,7 @@ describe( 'DropdownView', () => {
 		} );
 	} );
 
-	describe( 'init()', () => {
+	describe( 'render()', () => {
 		it( 'starts listening for #keystrokes coming from #element', () => {
 			view = new DropdownView( locale,
 				new ButtonView( locale ),
@@ -100,7 +101,7 @@ describe( 'DropdownView', () => {
 
 			const spy = sinon.spy( view.keystrokes, 'listenTo' );
 
-			view.init();
+			view.render();
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledWithExactly( spy, view.element );
 		} );
@@ -112,7 +113,7 @@ describe( 'DropdownView', () => {
 
 			const spy = sinon.spy( view.focusTracker, 'add' );
 
-			view.init();
+			view.render();
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledWithExactly( spy, view.element );
 		} );

+ 5 - 1
packages/ckeditor5-ui/tests/dropdown/list/createlistdropdown.js

@@ -26,10 +26,14 @@ describe( 'createListDropdown', () => {
 		} );
 
 		view = createListDropdown( model, locale );
-		view.init();
+		view.render();
 		document.body.appendChild( view.element );
 	} );
 
+	afterEach( () => {
+		view.element.remove();
+	} );
+
 	describe( 'constructor()', () => {
 		it( 'sets view#locale', () => {
 			expect( view.locale ).to.equal( locale );

+ 14 - 11
packages/ckeditor5-ui/tests/dropdown/manual/dropdown.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals document, window */
+/* globals window */
 
 import Model from '../../../src/model';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
@@ -12,11 +12,14 @@ import createDropdown from '../../../src/dropdown/createdropdown';
 import createListDropdown from '../../../src/dropdown/list/createlistdropdown';
 
 import '@ckeditor/ckeditor5-theme-lark/theme/theme.scss';
+import testUtils from '@ckeditor/ckeditor5-ui/tests/_utils/utils';
 
-function renderInto( selector, view ) {
-	view.init();
-	document.querySelector( selector ).appendChild( view.element );
-}
+const ui = testUtils.createTestUIView( {
+	dropdown: '#dropdown',
+	listDropdown: '#list-dropdown',
+	dropdownShared: '#dropdown-shared',
+	dropdownLabel: '#dropdown-label'
+} );
 
 function testEmpty() {
 	const dropdownView = createDropdown( new Model( {
@@ -26,9 +29,9 @@ function testEmpty() {
 		withText: true
 	} ) );
 
-	dropdownView.panelView.element.innerHTML = 'Empty panel. There is no child view in this DropdownPanelView.';
+	ui.dropdown.add( dropdownView );
 
-	renderInto( '#dropdown', dropdownView );
+	dropdownView.panelView.element.innerHTML = 'Empty panel. There is no child view in this DropdownPanelView.';
 }
 
 function testList() {
@@ -56,7 +59,7 @@ function testList() {
 		console.log( 'List#execute:', evt.source.label );
 	} );
 
-	renderInto( '#list-dropdown', dropdownView );
+	ui.listDropdown.add( dropdownView );
 
 	window.listDropdownModel = model;
 	window.listDropdownCollection = collection;
@@ -74,8 +77,8 @@ function testSharedModel() {
 	const dropdownView1 = createDropdown( model );
 	const dropdownView2 = createDropdown( model );
 
-	renderInto( '#dropdown-shared', dropdownView1 );
-	renderInto( '#dropdown-shared', dropdownView2 );
+	ui.dropdownShared.add( dropdownView1 );
+	ui.dropdownShared.add( dropdownView2 );
 
 	dropdownView1.panelView.element.innerHTML = dropdownView2.panelView.element.innerHTML = 'Empty panel.';
 }
@@ -88,7 +91,7 @@ function testLongLabel() {
 		withText: true
 	} ) );
 
-	renderInto( '#dropdown-label', dropdownView );
+	ui.dropdownLabel.add( dropdownView );
 
 	dropdownView.panelView.element.innerHTML = 'Empty panel. There is no child view in this DropdownPanelView.';
 }

+ 8 - 5
packages/ckeditor5-ui/tests/editableui/editableuiview.js

@@ -19,7 +19,8 @@ describe( 'EditableUIView', () => {
 		locale = new Locale( 'en' );
 		editableElement = document.createElement( 'div' );
 
-		return ( view = new EditableUIView( locale ) ).init();
+		view = new EditableUIView( locale );
+		view.render();
 	} );
 
 	describe( 'constructor()', () => {
@@ -32,22 +33,24 @@ describe( 'EditableUIView', () => {
 		it( 'renders element from template when no editableElement', () => {
 			view = new EditableUIView( locale );
 
-			view.init();
+			view.render();
 			expect( view.element ).to.equal( view.editableElement );
 			expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
 			expect( view.externalElement ).to.be.undefined;
+			expect( view.isRendered ).to.be.true;
 		} );
 
 		it( 'accepts editableElement as an argument', () => {
 			view = new EditableUIView( locale, editableElement );
 
-			view.init();
+			view.render();
 			expect( view.element ).to.equal( editableElement );
 			expect( view.element ).to.equal( view.editableElement );
 			expect( view.element.classList.contains( 'ck-editor__editable' ) ).to.be.true;
 			expect( view.element.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
 			expect( view.externalElement ).to.equal( editableElement );
+			expect( view.isRendered ).to.be.true;
 		} );
 	} );
 
@@ -98,7 +101,7 @@ describe( 'EditableUIView', () => {
 
 				view = new EditableUIView( locale, editableElement );
 
-				view.init();
+				view.render();
 				expect( editableElement.contentEditable ).to.equal( 'true' );
 				view.destroy();
 				expect( editableElement.contentEditable ).to.equal( 'false' );
@@ -110,7 +113,7 @@ describe( 'EditableUIView', () => {
 
 				view = new EditableUIView( locale, editableElement );
 
-				view.init();
+				view.render();
 				expect( editableElement.contentEditable ).to.equal( 'true' );
 				view.destroy();
 				expect( editableElement.contentEditable ).to.equal( 'true' );

+ 2 - 1
packages/ckeditor5-ui/tests/editableui/inline/inlineeditableuiview.js

@@ -15,7 +15,8 @@ describe( 'InlineEditableUIView', () => {
 		locale = new Locale( 'en' );
 		editableElement = document.createElement( 'div' );
 
-		return ( view = new InlineEditableUIView( locale ) ).init();
+		view = new InlineEditableUIView( locale );
+		view.render();
 	} );
 
 	describe( 'constructor()', () => {

+ 4 - 1
packages/ckeditor5-ui/tests/editorui/boxed/boxededitoruiview.js

@@ -12,9 +12,12 @@ describe( 'BoxedEditorUIView', () => {
 
 	beforeEach( () => {
 		view = new BoxedEditorUIView( new Locale( 'en' ) );
+		view.render();
 		element = view.element;
+	} );
 
-		return view.init();
+	afterEach( () => {
+		view.destroy();
 	} );
 
 	describe( 'constructor()', () => {

+ 6 - 2
packages/ckeditor5-ui/tests/editorui/editoruiview.js

@@ -19,7 +19,11 @@ describe( 'EditorUIView', () => {
 		locale = new Locale( 'en' );
 		view = new EditorUIView( locale );
 
-		return view.init();
+		view.render();
+	} );
+
+	afterEach( () => {
+		view.destroy();
 	} );
 
 	describe( 'constructor()', () => {
@@ -32,7 +36,7 @@ describe( 'EditorUIView', () => {
 		} );
 	} );
 
-	describe( 'init()', () => {
+	describe( 'render()', () => {
 		it( 'sets the right class set to the body region', () => {
 			const el = view._bodyCollectionContainer;
 

+ 15 - 2
packages/ckeditor5-ui/tests/icon/iconview.js

@@ -9,10 +9,19 @@ describe( 'IconView', () => {
 	let view;
 
 	beforeEach( () => {
-		return ( view = new IconView() ).init();
+		view = new IconView();
+		view.render();
 	} );
 
 	describe( 'constructor()', () => {
+		it( 'sets #content', () => {
+			expect( view.content ).to.equal( '' );
+		} );
+
+		it( 'sets #viewBox', () => {
+			expect( view.viewBox ).to.equal( '0 0 20 20' );
+		} );
+
 		it( 'creates element from template', () => {
 			expect( view.element.tagName ).to.equal( 'svg' );
 			expect( view.element.getAttribute( 'class' ) ).to.equal( 'ck-icon' );
@@ -33,9 +42,13 @@ describe( 'IconView', () => {
 
 		describe( 'inline svg', () => {
 			it( 'should react to changes in view#content', () => {
-				view.content = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg"><g id="test"></g></svg>';
+				expect( view.element.innerHTML = '' );
 
+				view.content = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg"><g id="test"></g></svg>';
 				expect( view.element.innerHTML = '<g id="test"></g>' );
+
+				view.content = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg"></svg>';
+				expect( view.element.innerHTML = '' );
 			} );
 		} );
 	} );

+ 18 - 8
packages/ckeditor5-ui/tests/iframe/iframeview.js

@@ -13,26 +13,32 @@ describe( 'IframeView', () => {
 	describe( 'constructor()', () => {
 		it( 'creates view element from the template', () => {
 			view = new IframeView();
+			view.render();
 			document.body.appendChild( view.element );
 
 			expect( view.element.classList.contains( 'ck-reset_all' ) ).to.be.true;
 			expect( view.element.attributes.getNamedItem( 'sandbox' ).value ).to.equal( 'allow-same-origin allow-scripts' );
+
+			view.element.remove();
 		} );
 	} );
 
-	describe( 'init', () => {
-		it( 'returns promise', () => {
+	describe( 'render', () => {
+		it( 'returns a promise', () => {
 			view = new IframeView();
 
-			expect( view.init() ).to.be.an.instanceof( Promise );
+			expect( view.render() ).to.be.an.instanceof( Promise );
 		} );
 
 		it( 'returns promise which is resolved when iframe finished loading', () => {
 			view = new IframeView();
 
-			const promise = view.init().then( () => {
-				expect( view.element.contentDocument.readyState ).to.equal( 'complete' );
-			} );
+			const promise = view.render()
+				.then( () => {
+					expect( view.element.contentDocument.readyState ).to.equal( 'complete' );
+
+					view.element.remove();
+				} );
 
 			// Moving iframe into DOM trigger creation of a document inside iframe.
 			document.body.appendChild( view.element );
@@ -45,9 +51,13 @@ describe( 'IframeView', () => {
 		it( 'is fired when frame finished loading', done => {
 			view = new IframeView();
 
-			view.on( 'loaded', () => done() );
+			view.on( 'loaded', () => {
+				view.element.remove();
+
+				done();
+			} );
 
-			view.init();
+			view.render();
 
 			// Moving iframe into DOM trigger creation of a document inside iframe.
 			document.body.appendChild( view.element );

+ 1 - 1
packages/ckeditor5-ui/tests/inputtext/inputtextview.js

@@ -11,7 +11,7 @@ describe( 'InputTextView', () => {
 	beforeEach( () => {
 		view = new InputTextView();
 
-		view.init();
+		view.render();
 	} );
 
 	describe( 'constructor()', () => {

+ 1 - 1
packages/ckeditor5-ui/tests/label/labelview.js

@@ -11,7 +11,7 @@ describe( 'LabelView', () => {
 	beforeEach( () => {
 		view = new LabelView();
 
-		view.init();
+		view.render();
 	} );
 
 	describe( 'constructor()', () => {

+ 3 - 3
packages/ckeditor5-ui/tests/labeledinput/labeledinputview.js

@@ -15,7 +15,7 @@ describe( 'LabeledInputView', () => {
 	beforeEach( () => {
 		view = new LabeledInputView( locale, InputView );
 
-		view.init();
+		view.render();
 	} );
 
 	describe( 'constructor()', () => {
@@ -38,11 +38,11 @@ describe( 'LabeledInputView', () => {
 
 	describe( 'template', () => {
 		it( 'should have label view', () => {
-			expect( view.template.children.get( 0 ) ).to.equal( view.labelView );
+			expect( view.template.children[ 0 ] ).to.equal( view.labelView );
 		} );
 
 		it( 'should have input view', () => {
-			expect( view.template.children.get( 1 ) ).to.equal( view.inputView );
+			expect( view.template.children[ 1 ] ).to.equal( view.inputView );
 		} );
 
 		describe( 'DOM bindings', () => {

+ 5 - 3
packages/ckeditor5-ui/tests/list/listitemview.js

@@ -18,7 +18,7 @@ describe( 'ListItemView', () => {
 			label: 'bar'
 		} );
 
-		return view.init();
+		return view.render();
 	} );
 
 	describe( 'constructor()', () => {
@@ -31,11 +31,13 @@ describe( 'ListItemView', () => {
 		} );
 	} );
 
-	describe( 'init()', () => {
+	describe( 'render()', () => {
 		it( 'starts listening for #keystrokes coming from #element', () => {
+			view = new ListItemView();
+
 			const spy = sinon.spy( view.keystrokes, 'listenTo' );
 
-			view.init();
+			view.render();
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledWithExactly( spy, view.element );
 		} );

+ 19 - 8
packages/ckeditor5-ui/tests/list/listview.js

@@ -17,7 +17,12 @@ describe( 'ListView', () => {
 	let view;
 
 	beforeEach( () => {
-		return ( view = new ListView() ).init();
+		view = new ListView();
+		view.render();
+	} );
+
+	afterEach( () => {
+		view.destroy();
 	} );
 
 	describe( 'constructor()', () => {
@@ -26,10 +31,10 @@ describe( 'ListView', () => {
 			expect( view.element.classList.contains( 'ck-list' ) ).to.be.true;
 		} );
 
-		it( 'creates view#children collection', () => {
+		it( 'creates view#items collection', () => {
 			expect( view.items ).to.be.instanceOf( ViewCollection );
 			expect( view.template.children ).to.have.length( 1 );
-			expect( view.template.children.get( 0 ) ).to.equal( view.items );
+			expect( view.template.children[ 0 ] ).to.equal( view.items );
 		} );
 
 		it( 'creates #focusTracker instance', () => {
@@ -43,30 +48,36 @@ describe( 'ListView', () => {
 		it( 'creates #_focusCycler instance', () => {
 			expect( view._focusCycler ).to.be.instanceOf( FocusCycler );
 		} );
+	} );
 
+	describe( 'render()', () => {
 		it( 'registers #items in #focusTracker', () => {
+			const view = new ListView();
 			const spyAdd = sinon.spy( view.focusTracker, 'add' );
 			const spyRemove = sinon.spy( view.focusTracker, 'remove' );
 
+			sinon.assert.notCalled( spyAdd );
 			view.items.add( focusable() );
 			view.items.add( focusable() );
 
+			view.render();
 			sinon.assert.calledTwice( spyAdd );
 
 			view.items.remove( 1 );
 			sinon.assert.calledOnce( spyRemove );
+
+			view.destroy();
 		} );
-	} );
 
-	describe( 'init()', () => {
 		it( 'starts listening for #keystrokes coming from #element', () => {
-			view = new ListView();
-
+			const view = new ListView();
 			const spy = sinon.spy( view.keystrokes, 'listenTo' );
 
-			view.init();
+			view.render();
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledWithExactly( spy, view.element );
+
+			view.destroy();
 		} );
 
 		describe( 'activates keyboard navigation for the list', () => {

+ 1 - 1
packages/ckeditor5-ui/tests/manual/icon/icon.js

@@ -59,8 +59,8 @@ addCase( iconWrapper );
 function renderIcon( content, size, color ) {
 	const iconView = new IconView();
 
+	iconView.render();
 	iconView.content = content;
-	iconView.init();
 
 	if ( size ) {
 		iconView.element.style.width = `${ size }px`;

+ 0 - 33
packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.html

@@ -1,33 +0,0 @@
-<div id="editor">
-	<p><i>This</i> is a <strong>first line</strong> of text.</p>
-	<figure class="image">
-		<img src="sample.jpg" alt="CKEditor logo" />
-	</figure>
-	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <i>Sint ista Graecorum;</i> Istam voluptatem, inquit, Epicurus ignorat? <b>Idem iste, inquam, de voluptate quid sentit?</b> <mark>Peccata paria.</mark> Omnes enim iucundum motum, quo sensus hilaretur. Quae iam oratio non a philosopho aliquo, sed a censore opprimenda est. </p>
-	<p>Sed hoc sane concedamus. Quamvis enim depravatae non sint, pravae tamen esse possunt. Non dolere, inquam, istud quam vim habeat postea videro; Nam adhuc, meo fortasse vitio, quid ego quaeram non perspicis. Expectoque quid ad id, quod quaerebam, respondeas. Quod idem cum vestri faciant, non satis magnam tribuunt inventoribus gratiam. </p>
-	<p>Parvi enim primo ortu sic iacent, tamquam omnino sine animo sint. Vide, quantum, inquam, fallare, Torquate. Bonum integritas corporis: misera debilitas. Quae autem natura suae primae institutionis oblita est? </p>
-	<p>Nemo igitur esse beatus potest. Etsi ea quidem, quae adhuc dixisti, quamvis ad aetatem recte isto modo dicerentur. <i>Sed fortuna fortis;</i> <b>An nisi populari fama?</b> Paupertas si malum est, mendicus beatus esse nemo potest, quamvis sit sapiens. Ait enim se, si uratur, Quam hoc suave! dicturum. <i>Minime vero, inquit ille, consentit.</i> Esse enim, nisi eris, non potes. </p>
-	<p>Cum autem venissemus in Academiae non sine causa nobilitata spatia, solitudo erat ea, quam volueramus. <i>Duo Reges: constructio interrete.</i> Non enim solum Torquatus dixit quid sentiret, sed etiam cur. Obsecro, inquit, Torquate, haec dicit Epicurus? Primum divisit ineleganter; </p>
-	<p><em>This</em> is the <strong>end</strong> of text.</p>
-	<figure class="image">
-		<img src="sample.jpg" alt="CKEditor logo" />
-	</figure>
-</div>
-
-<style>
-	.ck-editor {
-		margin: 5em auto;
-		max-width: 70%;
-	}
-
-	.ck-editor .image {
-		font-size: 0;
-		line-height: 0;
-		max-width: 80%;
-		margin: 0 auto;
-	}
-
-	.ck-editor .image img {
-		max-width: 100%;
-	}
-</style>

+ 0 - 98
packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.js

@@ -1,98 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* globals window, document, console:false */
-
-import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
-import Enter from '@ckeditor/ckeditor5-enter/src/enter';
-import Typing from '@ckeditor/ckeditor5-typing/src/typing';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import Undo from '@ckeditor/ckeditor5-undo/src/undo';
-import Image from '@ckeditor/ckeditor5-image/src/image';
-import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
-import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
-
-import Template from '../../../src/template';
-import ToolbarView from '../../../src/toolbar/toolbarview';
-import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview';
-
-ClassicEditor
-	.create( document.querySelector( '#editor' ), {
-		plugins: [ Enter, Typing, Paragraph, Undo, Bold, Italic, Image ],
-		toolbar: [ 'bold', 'italic', 'undo', 'redo' ]
-	} )
-	.then( editor => {
-		createImageToolbar( editor );
-
-		window.editor = editor;
-	} )
-	.catch( err => {
-		console.error( err.stack );
-	} );
-
-function createImageToolbar( editor ) {
-	// Create a plain toolbar instance.
-	const toolbar = new ToolbarView();
-
-	// Create a BalloonPanelView instance.
-	const panel = new BalloonPanelView( editor.locale );
-
-	Template.extend( panel.template, {
-		attributes: {
-			class: [
-				'ck-toolbar-container',
-			]
-		}
-	} );
-
-	// Putting the toolbar inside of the balloon panel.
-	panel.content.add( toolbar );
-
-	editor.ui.view.body.add( panel ).then( () => {
-		const editingView = editor.editing.view;
-
-		// Fill the toolbar with some buttons. Simply copy default editor toolbar.
-		toolbar.fillFromConfig( editor.config.get( 'toolbar' ), editor.ui.componentFactory );
-
-		// Let the focusTracker know about new focusable UI element.
-		editor.ui.focusTracker.add( panel.element );
-
-		// Hide the panel when editor loses focus but no the other way around.
-		panel.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, is, was ) => {
-			if ( was && !is ) {
-				panel.hide();
-			}
-		} );
-
-		editingView.addObserver( ClickObserver );
-
-		// Check if the toolbar should be displayed each time the user clicked in editable.
-		editor.listenTo( editingView, 'click', () => {
-			if ( editingView.selection.isFake ) {
-				attachToolbar();
-
-				// TODO: These 2 need interval–based event debouncing for performance
-				// reasons. I guess even lodash offers such a helper.
-				editor.ui.view.listenTo( window, 'scroll', attachToolbar );
-				editor.ui.view.listenTo( window, 'resize', attachToolbar );
-			} else {
-				panel.hide();
-
-				editor.ui.view.stopListening( window, 'scroll', attachToolbar );
-				editor.ui.view.stopListening( window, 'resize', attachToolbar );
-			}
-		} );
-
-		function attachToolbar() {
-			const defaultPositions = BalloonPanelView.defaultPositions;
-
-			panel.attachTo( {
-				target: editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ),
-				positions: [ defaultPositions.northArrowSouth, defaultPositions.southArrowNorth ]
-			} );
-		}
-	} );
-}

+ 0 - 6
packages/ckeditor5-ui/tests/manual/imagetoolbar/imagetoolbar.md

@@ -1,6 +0,0 @@
-## Image toolbar demo
-
-1. Click on the image (select it).
-1. A toolbar should appear, pinned to the horizontal middle point on the top of the image.
-1. Scroll the window – the toolbar should change it's position to remain visible for as long as possible.
-1. Resize the window – the toolbar should remain pinned to the image.

BIN
packages/ckeditor5-ui/tests/manual/imagetoolbar/sample.jpg


+ 1 - 0
packages/ckeditor5-ui/tests/manual/panel/balloon/balloonpanelview.js

@@ -17,6 +17,7 @@ for ( const i in defaultPositions ) {
 	container.appendChild( target );
 
 	const balloon = new BalloonPanelView();
+	balloon.render();
 	balloon.element.textContent = i;
 	document.body.appendChild( balloon.element );
 

+ 2 - 2
packages/ckeditor5-ui/tests/manual/panel/sticky/stickypanelview.html

@@ -62,12 +62,12 @@
 		position: relative;
 	}
 
-	.ck-sticky-panel {
+	.ck-sticky-panel__content {
 		background: yellow !important;
 		padding: 1em;
 	}
 
-	.ck-sticky-panel:after {
+	.ck-sticky-panel__content:after {
 		content: "A sticky panel mock–up.";
 	}
 

+ 1 - 1
packages/ckeditor5-ui/tests/manual/tickets/126/1.js

@@ -12,8 +12,8 @@ import '@ckeditor/ckeditor5-theme-lark/theme/theme.scss';
 window.createPanel = selector => {
 	const view = new BalloonPanelView();
 
+	view.render();
 	view.element.innerHTML = `Parent of this panel has position:${ selector }.`;
-	view.init();
 
 	document.querySelector( `#${ selector }-container` ).appendChild( view.element );
 

+ 2 - 2
packages/ckeditor5-ui/tests/manual/tickets/170/1.js

@@ -21,8 +21,8 @@ ClassicEditor
 	.then( editor => {
 		const panel = new BalloonPanelView();
 
-		panel.element.innerHTML = 'Balloon content.';
 		editor.ui.view.body.add( panel );
+		panel.element.innerHTML = 'Balloon content.';
 
 		editor.ui.view.element.querySelector( '.ck-editor__editable' ).scrollTop = 360;
 
@@ -46,8 +46,8 @@ ClassicEditor
 	.then( editor => {
 		const panel = new BalloonPanelView();
 
-		panel.element.innerHTML = 'Balloon content.';
 		editor.ui.view.body.add( panel );
+		panel.element.innerHTML = 'Balloon content.';
 
 		editor.ui.view.element.querySelector( '.ck-editor__editable' ).scrollTop = 360;
 

+ 2 - 3
packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js

@@ -18,17 +18,16 @@ describe( 'BalloonPanelView', () => {
 
 	beforeEach( () => {
 		view = new BalloonPanelView();
+		view.render();
 
 		document.body.appendChild( view.element );
-
-		return view.init();
 	} );
 
 	afterEach( () => {
 		if ( view ) {
 			view.element.remove();
 
-			return view.destroy();
+			view.destroy();
 		}
 	} );
 

+ 2 - 1
packages/ckeditor5-ui/tests/panel/balloon/contextualballoon.js

@@ -48,12 +48,13 @@ describe( 'ContextualBalloon', () => {
 					}
 				} );
 
-				viewB.init();
+				viewB.render();
 			} );
 	} );
 
 	afterEach( () => {
 		editor.destroy();
+		editorElement.remove();
 	} );
 
 	it( 'should create a plugin instance', () => {

+ 90 - 96
packages/ckeditor5-ui/tests/panel/sticky/stickypanelview.js

@@ -16,14 +16,18 @@ import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
 testUtils.createSinonSandbox();
 
 describe( 'StickyPanelView', () => {
-	let view, element, limiterElement, locale, windowStub;
+	let view, element, contentElement, placeholderElement, limiterElement, locale, windowStub;
 
 	beforeEach( () => {
 		locale = {};
 		limiterElement = document.createElement( 'div' );
 
 		view = new StickyPanelView( locale );
+		view.render();
+
 		element = view.element;
+		contentElement = view.element.lastChild;
+		placeholderElement = view.element.firstChild;
 
 		// Dummy values just to let non–geometrical tests pass without reference errors.
 		view._toolbarRect = { top: 10, right: 20, bottom: 30, left: 40, width: 50, height: 60 };
@@ -41,14 +45,24 @@ describe( 'StickyPanelView', () => {
 		document.body.appendChild( element );
 	} );
 
+	afterEach( () => {
+		element.remove();
+	} );
+
 	describe( 'constructor()', () => {
 		it( 'inherits from View', () => {
 			expect( view ).to.be.instanceof( View );
 		} );
 
 		it( 'should create element from template', () => {
-			expect( view.element.tagName ).to.equal( 'DIV' );
-			expect( view.element.classList.contains( 'ck-sticky-panel' ) ).to.true;
+			expect( element.tagName ).to.equal( 'DIV' );
+			expect( element.classList.contains( 'ck-sticky-panel' ) ).to.true;
+
+			expect( placeholderElement.tagName ).to.equal( 'DIV' );
+			expect( placeholderElement.classList.contains( 'ck-sticky-panel__placeholder' ) ).to.true;
+
+			expect( contentElement.tagName ).to.equal( 'DIV' );
+			expect( contentElement.classList.contains( 'ck-sticky-panel__content' ) ).to.true;
 		} );
 
 		it( 'sets view attributes', () => {
@@ -69,109 +83,106 @@ describe( 'StickyPanelView', () => {
 		it( 'creates view#content collection', () => {
 			expect( view.content ).to.be.instanceOf( ViewCollection );
 		} );
-
-		it( 'creates the _elementPlaceholder', () => {
-			expect( view._elementPlaceholder.classList.contains( 'ck-sticky-panel__placeholder' ) ).to.be.true;
-		} );
 	} );
 
 	describe( 'element view bindings', () => {
 		beforeEach( () => {
 			view.limiterElement = limiterElement;
-			view.init();
 		} );
 
 		it( 'update the class on view#isSticky change', () => {
 			view.isSticky = false;
-			expect( element.classList.contains( 'ck-sticky-panel_sticky' ) ).to.be.false;
+			expect( contentElement.classList.contains( 'ck-sticky-panel__content_sticky' ) ).to.be.false;
 
 			view.isSticky = true;
-			expect( element.classList.contains( 'ck-sticky-panel_sticky' ) ).to.be.true;
+			expect( contentElement.classList.contains( 'ck-sticky-panel__content_sticky' ) ).to.be.true;
 		} );
 
 		it( 'update the class on view#_isStickyToTheLimiter change', () => {
 			view._isStickyToTheLimiter = false;
-			expect( element.classList.contains( 'ck-sticky-panel_sticky_bottom-limit' ) ).to.be.false;
+			expect( contentElement.classList.contains( 'ck-sticky-panel__content_sticky_bottom-limit' ) ).to.be.false;
 
 			view._isStickyToTheLimiter = true;
-			expect( element.classList.contains( 'ck-sticky-panel_sticky_bottom-limit' ) ).to.be.true;
+			expect( contentElement.classList.contains( 'ck-sticky-panel__content_sticky_bottom-limit' ) ).to.be.true;
 		} );
 
 		it( 'update the styles.top on view#_hasViewportTopOffset change', () => {
 			view.viewportTopOffset = 100;
 
 			view._hasViewportTopOffset = false;
-			expect( element.style.top ).to.equal( '' );
+			expect( contentElement.style.top ).to.equal( '' );
 
 			view._hasViewportTopOffset = true;
-			expect( element.style.top ).to.equal( '100px' );
+			expect( contentElement.style.top ).to.equal( '100px' );
 		} );
 
 		it( 'update the styles.width on view#isSticky change', () => {
-			testUtils.sinon.stub( view._elementPlaceholder, 'getBoundingClientRect' ).returns( { width: 100 } );
+			testUtils.sinon.stub( view._contentPanelPlaceholder, 'getBoundingClientRect' ).returns( { width: 100 } );
 
 			view.isSticky = false;
-			expect( element.style.width ).to.equal( '' );
+			expect( contentElement.style.width ).to.equal( '' );
 
 			view.isSticky = true;
-			expect( element.style.width ).to.equal( '100px' );
+			expect( contentElement.style.width ).to.equal( '100px' );
 		} );
 
 		it( 'update the styles.bottom on view#_isStickyToTheLimiter change', () => {
 			view._isStickyToTheLimiter = false;
-			expect( element.style.bottom ).to.equal( '' );
+			expect( contentElement.style.bottom ).to.equal( '' );
 
 			view._isStickyToTheLimiter = true;
-			expect( element.style.bottom ).to.equal( '50px' );
+			expect( contentElement.style.bottom ).to.equal( '50px' );
 		} );
 
 		it( 'update the styles.marginLeft on view#marginLeft change', () => {
 			view._marginLeft = '30px';
-			expect( element.style.marginLeft ).to.equal( '30px' );
+			expect( contentElement.style.marginLeft ).to.equal( '30px' );
 
 			view._marginLeft = '10px';
-			expect( element.style.marginLeft ).to.equal( '10px' );
+			expect( contentElement.style.marginLeft ).to.equal( '10px' );
 		} );
 	} );
 
-	describe( '_elementPlaceholder view bindings', () => {
+	describe( '_contentPanelPlaceholder view bindings', () => {
 		beforeEach( () => {
 			view.limiterElement = limiterElement;
-			view.init();
 		} );
 
 		it( 'update the styles.display on view#isSticky change', () => {
 			view.isSticky = false;
-			expect( view._elementPlaceholder.style.display ).to.equal( 'none' );
+			expect( placeholderElement.style.display ).to.equal( 'none' );
 
 			view.isSticky = true;
-			expect( view._elementPlaceholder.style.display ).to.equal( 'block' );
+			expect( placeholderElement.style.display ).to.equal( 'block' );
 		} );
 
 		it( 'update the styles.height on view#isSticky change', () => {
 			view._panelRect = { height: 50 };
 
 			view.isSticky = false;
-			expect( view._elementPlaceholder.style.height ).to.equal( '' );
+			expect( placeholderElement.style.height ).to.equal( '' );
 
 			view.isSticky = true;
-			expect( view._elementPlaceholder.style.height ).to.equal( '50px' );
+			expect( placeholderElement.style.height ).to.equal( '50px' );
 		} );
 	} );
 
 	describe( 'children', () => {
 		it( 'should react on view#content', () => {
-			expect( view.element.childNodes.length ).to.equal( 0 );
+			expect( contentElement.childNodes.length ).to.equal( 0 );
 
 			const label = new LabelView( { t() {} } );
 
 			view.content.add( label );
-			expect( view.element.childNodes.length ).to.equal( 1 );
+			expect( contentElement.childNodes.length ).to.equal( 1 );
 		} );
 	} );
 
-	describe( 'init()', () => {
+	describe( 'render()', () => {
+		let view;
+
 		beforeEach( () => {
+			view = new StickyPanelView();
 			view.limiterElement = limiterElement;
 		} );
 
@@ -179,23 +190,18 @@ describe( 'StickyPanelView', () => {
 			return view.destroy();
 		} );
 
-		it( 'calls init on parent class', () => {
-			const spy = testUtils.sinon.spy( View.prototype, 'init' );
+		it( 'calls render on parent class', () => {
+			const spy = testUtils.sinon.spy( View.prototype, 'render' );
 
-			view.init();
+			view.render();
 			expect( spy.calledOnce ).to.be.true;
 		} );
 
-		it( 'puts the view._elementPlaceholder before view.element', () => {
-			view.init();
-			expect( element.previousSibling ).to.equal( view._elementPlaceholder );
-		} );
-
 		it( 'checks if the panel should be sticky', () => {
 			const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' );
 			expect( spy.notCalled ).to.be.true;
 
-			view.init();
+			view.render();
 			expect( spy.calledOnce ).to.be.true;
 		} );
 
@@ -203,7 +209,8 @@ describe( 'StickyPanelView', () => {
 			const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' );
 			expect( spy.notCalled ).to.be.true;
 
-			view.init();
+			view.render();
+
 			global.window.fire( 'scroll' );
 			expect( spy.calledTwice ).to.be.true;
 		} );
@@ -212,7 +219,7 @@ describe( 'StickyPanelView', () => {
 			const spy = testUtils.sinon.spy( view, '_checkIfShouldBeSticky' );
 			expect( spy.notCalled ).to.be.true;
 
-			view.init();
+			view.render();
 			view.isActive = true;
 			expect( spy.calledTwice ).to.be.true;
 
@@ -235,11 +242,6 @@ describe( 'StickyPanelView', () => {
 			view.destroy();
 			expect( spy.calledOnce ).to.be.true;
 		} );
-
-		it( 'removes view._elementPlaceholder from DOM', () => {
-			view.destroy();
-			expect( view._elementPlaceholder.parentNode ).to.be.null;
-		} );
 	} );
 
 	describe( '_checkIfShouldBeSticky', () => {
@@ -249,107 +251,105 @@ describe( 'StickyPanelView', () => {
 
 		describe( 'view.isSticky', () => {
 			beforeEach( () => {
-				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
 					height: 20
 				} );
 			} );
 
 			it( 'is true if beyond the top of the viewport (panel is active)', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } );
-				view.isActive = true;
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } );
 
 				expect( view.isSticky ).to.be.false;
 
-				view._checkIfShouldBeSticky();
+				view.isActive = true;
+
 				expect( view.isSticky ).to.be.true;
 			} );
 
 			it( 'is false if beyond the top of the viewport (panel is inactive)', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } );
-				view.isActive = false;
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 100 } );
 
 				expect( view.isSticky ).to.be.false;
 
-				view._checkIfShouldBeSticky();
+				view.isActive = false;
+
 				expect( view.isSticky ).to.be.false;
 			} );
 
 			it( 'is false if in the viewport (panel is active)', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: 10, height: 100 } );
-				view.isActive = true;
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: 10, height: 100 } );
 
 				expect( view.isSticky ).to.be.false;
 
-				view._checkIfShouldBeSticky();
+				view.isActive = true;
+
 				expect( view.isSticky ).to.be.false;
 			} );
 
 			it( 'is false if view.limiterElement is smaller than the panel and view.limiterBottomOffset (panel is active)', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 60 } );
-				view.isActive = true;
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( { top: -10, height: 60 } );
+
 				view.limiterBottomOffset = 50;
 
 				expect( view.isSticky ).to.be.false;
 
-				view._checkIfShouldBeSticky();
+				view.isActive = true;
+
 				expect( view.isSticky ).to.be.false;
 			} );
 		} );
 
 		describe( 'view._isStickyToTheLimiter', () => {
 			it( 'is true if view.isSticky is true and reached the bottom edge of view.limiterElement', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
 					top: -10,
 					bottom: 10,
 					height: 100
 				} );
 
-				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
 					height: 20
 				} );
 
-				view.isActive = true;
-
 				expect( view.isSticky ).to.be.false;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 
-				view._checkIfShouldBeSticky();
+				view.isActive = true;
+
 				expect( view.isSticky ).to.be.true;
 				expect( view._isStickyToTheLimiter ).to.be.true;
 			} );
 
 			it( 'is false if view.isSticky is true and not reached the bottom edge of view.limiterElement', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
 					top: -10,
 					bottom: 90,
 					height: 100
 				} );
 
-				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
 					height: 20
 				} );
 
-				view.isActive = true;
-
 				expect( view.isSticky ).to.be.false;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 
-				view._checkIfShouldBeSticky();
+				view.isActive = true;
+
 				expect( view.isSticky ).to.be.true;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 			} );
 
 			it( 'is false if view.isSticky is false', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
 					top: 10,
 				} );
 
-				view.isActive = true;
-
 				expect( view.isSticky ).to.be.false;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 
-				view._checkIfShouldBeSticky();
+				view.isActive = true;
+
 				expect( view.isSticky ).to.be.false;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 			} );
@@ -359,19 +359,18 @@ describe( 'StickyPanelView', () => {
 			it( 'is true if view._isStickyToTheLimiter is false and view.viewportTopOffset has been specified', () => {
 				view.viewportTopOffset = 100;
 
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
 					top: 90,
 					bottom: 190,
 					height: 100
 				} );
 
-				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
 					height: 20
 				} );
 
 				view.isActive = true;
 
-				view._checkIfShouldBeSticky();
 				expect( view.isSticky ).to.be.true;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 				expect( view._hasViewportTopOffset ).to.be.true;
@@ -380,19 +379,18 @@ describe( 'StickyPanelView', () => {
 			it( 'is false if view._isStickyToTheLimiter is true and view.viewportTopOffset has been specified', () => {
 				view.viewportTopOffset = 100;
 
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
 					top: 10,
 					bottom: 110,
 					height: 100
 				} );
 
-				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
 					height: 20
 				} );
 
 				view.isActive = true;
 
-				view._checkIfShouldBeSticky();
 				expect( view.isSticky ).to.be.true;
 				expect( view._isStickyToTheLimiter ).to.be.true;
 				expect( view._hasViewportTopOffset ).to.be.false;
@@ -401,19 +399,18 @@ describe( 'StickyPanelView', () => {
 			it( 'is false if view._isStickyToTheLimiter is false and view.viewportTopOffset is 0', () => {
 				view.viewportTopOffset = 100;
 
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
 					top: 90,
 					bottom: 190,
 					height: 100
 				} );
 
-				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
 					height: 20
 				} );
 
 				view.isActive = true;
 
-				view._checkIfShouldBeSticky();
 				expect( view.isSticky ).to.be.true;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 				expect( view._hasViewportTopOffset ).to.be.true;
@@ -422,13 +419,13 @@ describe( 'StickyPanelView', () => {
 
 		describe( 'view._marginLeft', () => {
 			it( 'is set if view.isSticky is true view._isStickyToTheLimiter is false', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
 					top: -10,
 					bottom: 80,
 					height: 100
 				} );
 
-				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
 					height: 20
 				} );
 
@@ -437,27 +434,26 @@ describe( 'StickyPanelView', () => {
 					scrollY: 0
 				} );
 
-				view.isActive = true;
-
 				expect( view.isSticky ).to.be.false;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 				expect( view._marginLeft ).to.equal( null );
 
-				view._checkIfShouldBeSticky();
+				view.isActive = true;
+
 				expect( view.isSticky ).to.be.true;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 				expect( view._marginLeft ).to.equal( '-10px' );
 			} );
 
 			it( 'is not set if view._isStickyToTheLimiter is true', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
 					top: -10,
 					bottom: 10,
 					left: 60,
 					height: 100
 				} );
 
-				testUtils.sinon.stub( view.element, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( contentElement, 'getBoundingClientRect' ).returns( {
 					height: 20
 				} );
 
@@ -465,30 +461,28 @@ describe( 'StickyPanelView', () => {
 					left: 40
 				} );
 
-				view.isActive = true;
-
 				expect( view.isSticky ).to.be.false;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 				expect( view._marginLeft ).to.equal( null );
 
-				view._checkIfShouldBeSticky();
+				view.isActive = true;
+
 				expect( view.isSticky ).to.be.true;
 				expect( view._isStickyToTheLimiter ).to.be.true;
 				expect( view._marginLeft ).to.equal( null );
 			} );
 
 			it( 'is not set if view.isSticky is false', () => {
-				testUtils.sinon.stub( view.limiterElement, 'getBoundingClientRect' ).returns( {
+				testUtils.sinon.stub( limiterElement, 'getBoundingClientRect' ).returns( {
 					top: 10,
 				} );
 
-				view.isActive = true;
-
 				expect( view.isSticky ).to.be.false;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 				expect( view._marginLeft ).to.equal( null );
 
-				view._checkIfShouldBeSticky();
+				view.isActive = true;
+
 				expect( view.isSticky ).to.be.false;
 				expect( view._isStickyToTheLimiter ).to.be.false;
 				expect( view._marginLeft ).to.equal( null );

+ 125 - 26
packages/ckeditor5-ui/tests/template.js

@@ -13,15 +13,22 @@ import Model from '../src/model';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
-import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 
 testUtils.createSinonSandbox();
 
 let el, text;
+const injectedElements = [];
 
 describe( 'Template', () => {
+	// Clean-up document.body from the rendered elements.
+	afterEach( () => {
+		for ( const el of injectedElements ) {
+			el.remove();
+		}
+	} );
+
 	describe( 'constructor()', () => {
 		it( 'sets #_isRendered property', () => {
 			expect( new Template( { tag: 'p' } )._isRendered ).to.be.false;
@@ -29,6 +36,11 @@ describe( 'Template', () => {
 
 		it( 'accepts and normalizes the definition', () => {
 			const bind = Template.bind( new Model( {} ), Object.create( DomEmitterMixin ) );
+			const childNode = document.createElement( 'div' );
+			const childTemplate = new Template( {
+				tag: 'b'
+			} );
+
 			const tpl = new Template( {
 				tag: 'p',
 				attributes: {
@@ -49,7 +61,9 @@ describe( 'Template', () => {
 					'abc',
 					{
 						text: [ 'a', 'b' ]
-					}
+					},
+					childNode,
+					childTemplate
 				],
 				on: {
 					'a@span': bind.to( 'b' ),
@@ -66,12 +80,14 @@ describe( 'Template', () => {
 			expect( tpl.attributes.b[ 1 ] ).to.equal( 'baz' );
 			expect( tpl.attributes.c[ 0 ].value[ 0 ] ).to.be.instanceof( TemplateToBinding );
 
-			expect( tpl.children ).to.have.length( 4 );
-			expect( tpl.children.get( 0 ).text[ 0 ] ).to.equal( 'content' );
-			expect( tpl.children.get( 1 ).text[ 0 ] ).to.be.instanceof( TemplateToBinding );
-			expect( tpl.children.get( 2 ).text[ 0 ] ).to.equal( 'abc' );
-			expect( tpl.children.get( 3 ).text[ 0 ] ).to.equal( 'a' );
-			expect( tpl.children.get( 3 ).text[ 1 ] ).to.equal( 'b' );
+			expect( tpl.children ).to.have.length( 6 );
+			expect( tpl.children[ 0 ].text[ 0 ] ).to.equal( 'content' );
+			expect( tpl.children[ 1 ].text[ 0 ] ).to.be.instanceof( TemplateToBinding );
+			expect( tpl.children[ 2 ].text[ 0 ] ).to.equal( 'abc' );
+			expect( tpl.children[ 3 ].text[ 0 ] ).to.equal( 'a' );
+			expect( tpl.children[ 3 ].text[ 1 ] ).to.equal( 'b' );
+			expect( tpl.children[ 4 ] ).to.equal( childNode );
+			expect( tpl.children[ 5 ] ).to.equal( childTemplate );
 
 			expect( tpl.eventListeners[ 'a@span' ][ 0 ] ).to.be.instanceof( TemplateToBinding );
 			expect( tpl.eventListeners[ 'b@span' ][ 0 ] ).to.be.instanceof( TemplateToBinding );
@@ -92,8 +108,9 @@ describe( 'Template', () => {
 				text: 'foo'
 			} );
 
-			expect( elementTpl.children ).to.be.instanceof( Collection );
+			expect( elementTpl.children ).to.be.an( 'array' );
 			expect( elementTpl.children ).to.have.length( 0 );
+
 			// Text will never have children.
 			expect( textTpl.children ).to.be.undefined;
 		} );
@@ -114,12 +131,12 @@ describe( 'Template', () => {
 
 			expect( def.attributes ).to.not.equal( tpl.attributes );
 			expect( def.children ).to.not.equal( tpl.children );
-			expect( def.children[ 0 ] ).to.not.equal( tpl.children.get( 0 ) );
+			expect( def.children[ 0 ] ).to.not.equal( tpl.children[ 0 ] );
 			expect( def.attributes.a ).to.equal( 'foo' );
 			expect( def.children[ 0 ].tag ).to.equal( 'span' );
 
 			expect( tpl.attributes.a[ 0 ] ).to.equal( 'foo' );
-			expect( tpl.children.get( 0 ).tag ).to.equal( 'span' );
+			expect( tpl.children[ 0 ].tag ).to.equal( 'span' );
 		} );
 	} );
 
@@ -426,7 +443,7 @@ describe( 'Template', () => {
 
 			it( 'renders view children', () => {
 				const v1 = getView( {
-					tag: 'span',
+					tag: 'b',
 					attributes: {
 						class: [
 							'v1'
@@ -435,7 +452,7 @@ describe( 'Template', () => {
 				} );
 
 				const v2 = getView( {
-					tag: 'span',
+					tag: 'b',
 					attributes: {
 						class: [
 							'v2'
@@ -443,22 +460,36 @@ describe( 'Template', () => {
 					}
 				} );
 
+				const v3 = getView( {
+					tag: 'b',
+					attributes: {
+						class: [
+							'v3'
+						]
+					}
+				} );
+
+				v3.render();
+
 				const tpl = new Template( {
 					tag: 'p',
-					children: [ v1, v2 ]
+					children: [ v1, v2, v3 ]
 				} );
 
-				expect( tpl.children.get( 0 ) ).to.equal( v1 );
-				expect( tpl.children.get( 1 ) ).to.equal( v2 );
+				expect( tpl.children[ 0 ] ).to.equal( v1 );
+				expect( tpl.children[ 1 ] ).to.equal( v2 );
+				expect( tpl.children[ 2 ] ).to.equal( v3 );
 
 				const rendered = tpl.render();
 
-				expect( normalizeHtml( rendered.outerHTML ) ).to.equal( '<p><span class="v1"></span><span class="v2"></span></p>' );
+				expect( normalizeHtml( rendered.outerHTML ) )
+					.to.equal( '<p><b class="v1"></b><b class="v2"></b><b class="v3"></b></p>' );
 
 				// Make sure the child views will not re–render their elements but
 				// use ones rendered by the template instance above.
 				expect( v1.element ).to.equal( rendered.firstChild );
-				expect( v2.element ).to.equal( rendered.lastChild );
+				expect( v2.element ).to.equal( rendered.children[ 1 ] );
+				expect( v3.element ).to.equal( rendered.lastChild );
 			} );
 
 			it( 'renders view collection', () => {
@@ -500,6 +531,38 @@ describe( 'Template', () => {
 				expect( collection._parentElement ).to.equal( rendered );
 			} );
 
+			it( 'renders DOM nodes', () => {
+				const view = new View();
+
+				view.set( {
+					foo: 'bar',
+					bar: 'baz'
+				} );
+
+				const bind = Template.bind( view, view );
+
+				const childA = new Template( {
+					tag: 'b',
+					attributes: {
+						class: bind.to( 'foo' )
+					}
+				} ).render();
+
+				const childB = new Template( {
+					text: bind.to( 'bar' )
+				} ).render();
+
+				const rendered = new Template( {
+					tag: 'p',
+					children: [
+						childA,
+						childB
+					]
+				} ).render();
+
+				expect( normalizeHtml( rendered.outerHTML ) ).to.equal( '<p><b class="bar"></b>baz</p>' );
+			} );
+
 			// #117
 			it( 'renders template children', () => {
 				const childTplA = new Template( {
@@ -532,7 +595,7 @@ describe( 'Template', () => {
 				// words to explain it. But what actually matters is that it proves the Template
 				// class is free of "Maximum call stack size exceeded" error in certain
 				// situations.
-				view.template = new Template( {
+				view.setTemplate( {
 					tag: 'span',
 
 					children: [
@@ -562,9 +625,9 @@ describe( 'Template', () => {
 				} );
 
 				// Make sure child instances weren't cloned.
-				expect( tpl.children.get( 0 ) ).to.equal( childTplA );
-				expect( tpl.children.get( 1 ) ).to.equal( childTplB );
-				expect( tpl.children.get( 2 ) ).to.equal( childTplC );
+				expect( tpl.children[ 0 ] ).to.equal( childTplA );
+				expect( tpl.children[ 1 ] ).to.equal( childTplB );
+				expect( tpl.children[ 2 ] ).to.equal( childTplC );
 
 				expect( normalizeHtml( tpl.render().outerHTML ) ).to.equal(
 					'<p><a></a><b></b><i>foo</i></p>'
@@ -647,7 +710,8 @@ describe( 'Template', () => {
 		let observable, domEmitter, bind;
 
 		beforeEach( () => {
-			el = getElement( { tag: 'div' } );
+			setElement( { tag: 'div' } );
+
 			text = document.createTextNode( '' );
 
 			observable = new Model( {
@@ -1441,6 +1505,36 @@ describe( 'Template', () => {
 		} );
 	} );
 
+	describe( 'getViews()', () => {
+		it( 'returns iterator', () => {
+			const template = new Template( {} );
+
+			expect( template.getViews().next ).to.be.a( 'function' );
+			expect( Array.from( template.getViews() ) ).to.have.length( 0 );
+		} );
+
+		it( 'returns all child views', () => {
+			const viewA = new View();
+			const viewB = new View();
+			const viewC = new View();
+			const template = new Template( {
+				tag: 'div',
+				children: [
+					viewA,
+					{
+						tag: 'div',
+						children: [
+							viewB
+						]
+					},
+					viewC
+				]
+			} );
+
+			expect( Array.from( template.getViews() ) ).to.have.members( [ viewA, viewB, viewC ] );
+		} );
+	} );
+
 	describe( 'bind()', () => {
 		it( 'returns object', () => {
 			expect( Template.bind() ).to.be.an( 'object' );
@@ -2739,7 +2833,7 @@ describe( 'Template', () => {
 					]
 				} );
 
-				Template.extend( template.children.get( 0 ), {
+				Template.extend( template.children[ 0 ], {
 					attributes: {
 						class: 'bar'
 					}
@@ -2773,7 +2867,7 @@ describe( 'Template', () => {
 					]
 				} );
 
-				Template.extend( template.children.get( 0 ), {
+				Template.extend( template.children[ 0 ], {
 					attributes: {
 						class: 'B',
 					},
@@ -2898,7 +2992,10 @@ function getElement( template ) {
 
 function setElement( template ) {
 	el = new Template( template ).render();
+
 	document.body.appendChild( el );
+
+	injectedElements.push( el );
 }
 
 function extensionTest( baseDefinition, extendedDefinition, expectedHtml ) {
@@ -2912,6 +3009,8 @@ function extensionTest( baseDefinition, extendedDefinition, expectedHtml ) {
 
 	expect( normalizeHtml( el.outerHTML ) ).to.equal( expectedHtml );
 
+	injectedElements.push( el );
+
 	return el;
 }
 
@@ -2928,7 +3027,7 @@ function dispatchEvent( el, domEvtName ) {
 function getView( def ) {
 	const view = new View();
 
-	view.template = new Template( def );
+	view.setTemplate( def );
 
 	return view;
 }

+ 0 - 2
packages/ckeditor5-ui/tests/toolbar/contextual/contextualtoolbar.js

@@ -44,8 +44,6 @@ describe( 'ContextualToolbar', () => {
 
 				// Focus the engine.
 				editor.editing.view.isFocused = true;
-
-				contextualToolbar.toolbarView.init();
 			} );
 	} );
 

+ 4 - 4
packages/ckeditor5-ui/tests/toolbar/enabletoolbarkeyboardfocus.js

@@ -21,14 +21,14 @@ describe( 'enableToolbarKeyboardFocus()', () => {
 		originKeystrokeHandler = new KeystrokeHandler();
 		toolbar = new ToolbarView();
 
+		toolbar.render();
+
 		enableToolbarKeyboardFocus( {
 			origin,
 			originFocusTracker,
 			originKeystrokeHandler,
 			toolbar
 		} );
-
-		toolbar.init();
 	} );
 
 	it( 'focuses the toolbar on Alt+F10', () => {
@@ -99,6 +99,8 @@ describe( 'enableToolbarKeyboardFocus()', () => {
 		const originFocusSpy = origin.focus = sinon.spy();
 		const toolbarFocusTracker = toolbar.focusTracker;
 
+		toolbar.render();
+
 		enableToolbarKeyboardFocus( {
 			origin,
 			originFocusTracker,
@@ -108,8 +110,6 @@ describe( 'enableToolbarKeyboardFocus()', () => {
 			afterBlur
 		} );
 
-		toolbar.init();
-
 		let keyEvtData = {
 			keyCode: keyCodes.f10,
 			altKey: true,

+ 1 - 1
packages/ckeditor5-ui/tests/toolbar/toolbarseparatorview.js

@@ -11,7 +11,7 @@ describe( 'ToolbarSeparatorView', () => {
 	beforeEach( () => {
 		view = new ToolbarSeparatorView();
 
-		return view.init();
+		view.render();
 	} );
 
 	describe( 'template', () => {

+ 28 - 18
packages/ckeditor5-ui/tests/toolbar/toolbarview.js

@@ -25,7 +25,11 @@ describe( 'ToolbarView', () => {
 	beforeEach( () => {
 		locale = {};
 		view = new ToolbarView( locale );
-		view.init();
+		view.render();
+	} );
+
+	afterEach( () => {
+		view.destroy();
 	} );
 
 	describe( 'constructor()', () => {
@@ -48,19 +52,6 @@ describe( 'ToolbarView', () => {
 		it( 'creates #_focusCycler instance', () => {
 			expect( view._focusCycler ).to.be.instanceOf( FocusCycler );
 		} );
-
-		it( 'registers #items in #focusTracker', () => {
-			const spyAdd = sinon.spy( view.focusTracker, 'add' );
-			const spyRemove = sinon.spy( view.focusTracker, 'remove' );
-
-			view.items.add( focusable() );
-			view.items.add( focusable() );
-
-			sinon.assert.calledTwice( spyAdd );
-
-			view.items.remove( 1 );
-			sinon.assert.calledOnce( spyRemove );
-		} );
 	} );
 
 	describe( 'template', () => {
@@ -79,15 +70,34 @@ describe( 'ToolbarView', () => {
 		} );
 	} );
 
-	describe( 'init()', () => {
-		it( 'starts listening for #keystrokes coming from #element', () => {
-			view = new ToolbarView();
+	describe( 'render()', () => {
+		it( 'registers #items in #focusTracker', () => {
+			const view = new ToolbarView( locale );
+			const spyAdd = sinon.spy( view.focusTracker, 'add' );
+			const spyRemove = sinon.spy( view.focusTracker, 'remove' );
+
+			view.items.add( focusable() );
+			view.items.add( focusable() );
+			sinon.assert.notCalled( spyAdd );
+
+			view.render();
+			sinon.assert.calledTwice( spyAdd );
 
+			view.items.remove( 1 );
+			sinon.assert.calledOnce( spyRemove );
+
+			view.destroy();
+		} );
+
+		it( 'starts listening for #keystrokes coming from #element', () => {
+			const view = new ToolbarView();
 			const spy = sinon.spy( view.keystrokes, 'listenTo' );
 
-			view.init();
+			view.render();
 			sinon.assert.calledOnce( spy );
 			sinon.assert.calledWithExactly( spy, view.element );
+
+			view.destroy();
 		} );
 
 		describe( 'activates keyboard navigation for the toolbar', () => {

+ 1 - 0
packages/ckeditor5-ui/tests/tooltip/tooltipview.js

@@ -10,6 +10,7 @@ describe( 'TooltipView', () => {
 
 	beforeEach( () => {
 		view = new TooltipView();
+		view.render();
 		text = view.element.firstChild;
 	} );
 

+ 103 - 51
packages/ckeditor5-ui/tests/view.js

@@ -11,12 +11,21 @@ import Template from '../src/template';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import ViewCollection from '../src/viewcollection';
+import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 
 let TestView, view, childA, childB;
 
 testUtils.createSinonSandbox();
 
 describe( 'View', () => {
+	afterEach( () => {
+		if ( view.element ) {
+			view.element.remove();
+		}
+
+		view.destroy();
+	} );
+
 	describe( 'constructor()', () => {
 		beforeEach( () => {
 			setTestViewClass();
@@ -28,7 +37,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 +90,7 @@ describe( 'View', () => {
 		} );
 	} );
 
-	describe( 'addChildren()', () => {
+	describe( 'registerChildren()', () => {
 		beforeEach( () => {
 			setTestViewClass();
 			setTestViewInstance();
@@ -90,9 +99,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,77 +109,117 @@ 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( 'setTemplate()', () => {
+		it( 'sets the template', () => {
+			const view = new View();
+			const bind = view.bindTemplate;
+
+			view.set( 'foo', 'bar' );
+
+			view.setTemplate( {
+				tag: 'div',
+				attributes: {
+					class: [
+						bind.to( 'foo' )
+					]
+				}
+			} );
+
+			view.render();
+
+			expect( normalizeHtml( view.element.outerHTML ) ).to.equal( '<div class="bar"></div>' );
+		} );
+	} );
+
+	describe( 'extendTemplate()', () => {
+		it( 'extends the template', () => {
+			const view = new View();
+			const bind = view.bindTemplate;
+
+			view.set( 'foo', 'bar' );
+
+			view.setTemplate( {
+				tag: 'div'
+			} );
+
+			view.extendTemplate( {
+				attributes: {
+					class: [
+						bind.to( 'foo' )
+					]
+				}
+			} );
 
-		it( 'should throw if already initialized', () => {
-			view.init();
+			view.render();
+
+			expect( normalizeHtml( view.element.outerHTML ) ).to.equal( '<div class="bar"></div>' );
+		} );
+	} );
+
+	describe( 'render()', () => {
+		it( 'should throw if already rendered', () => {
+			const view = new View();
+
+			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;
-
-			view.init();
-			expect( view.ready ).to.be.true;
-		} );
+		it( 'should set view#isRendered', () => {
+			const view = new View();
 
-		it( 'calls init() on all view#_viewCollections', () => {
-			const collectionA = view.createCollection();
-			const collectionB = view.createCollection();
+			view.setTemplate( {
+				tag: 'div'
+			} );
 
-			const spyA = testUtils.sinon.spy( collectionA, 'init' );
-			const spyB = testUtils.sinon.spy( collectionB, 'init' );
+			expect( view.isRendered ).to.be.false;
 
-			view.init();
-			sinon.assert.calledOnce( spyA );
-			sinon.assert.calledOnce( spyB );
-			sinon.assert.callOrder( spyA, spyB );
+			view.render();
+			expect( view.isRendered ).to.be.true;
 		} );
 	} );
 
@@ -195,8 +244,6 @@ describe( 'View', () => {
 		beforeEach( createViewInstanceWithTemplate );
 
 		it( 'invokes out of #template', () => {
-			setTestViewInstance();
-
 			expect( view.element ).to.be.an.instanceof( HTMLElement );
 			expect( view.element.nodeName ).to.equal( 'A' );
 		} );
@@ -210,13 +257,17 @@ describe( 'View', () => {
 				}
 			}
 
-			view = new CustomView();
+			const view = new CustomView();
 
 			expect( view.element ).to.be.an.instanceof( HTMLElement );
 		} );
 
 		it( 'is null when there is no template', () => {
-			expect( new View().element ).to.be.null;
+			const view = new View();
+
+			view.render();
+
+			expect( view.element ).to.be.null;
 		} );
 
 		it( 'registers child views found in the template', () => {
@@ -225,11 +276,11 @@ describe( 'View', () => {
 			const viewB = new View();
 			const viewC = new View();
 
-			viewA.template = new Template( { tag: 'a' } );
-			viewB.template = new Template( { tag: 'b' } );
-			viewC.template = new Template( { tag: 'c' } );
+			viewA.setTemplate( { tag: 'a' } );
+			viewB.setTemplate( { tag: 'b' } );
+			viewC.setTemplate( { tag: 'c' } );
 
-			view.template = new Template( {
+			view.setTemplate( {
 				tag: 'div',
 				children: [
 					viewA,
@@ -248,8 +299,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 +332,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();
@@ -322,7 +372,7 @@ describe( 'View', () => {
 		} );
 
 		it( 'destroy a template–less view', () => {
-			view = new View();
+			const view = new View();
 
 			expect( () => {
 				view.destroy();
@@ -344,7 +394,7 @@ function setTestViewClass( templateDef ) {
 			this.locale = { t() {} };
 
 			if ( templateDef ) {
-				this.template = new Template( templateDef );
+				this.setTemplate( templateDef );
 			}
 		}
 	};
@@ -354,6 +404,8 @@ function setTestViewInstance() {
 	view = new TestView();
 
 	if ( view.template ) {
+		view.render();
+
 		document.body.appendChild( view.element );
 	}
 }
@@ -363,7 +415,7 @@ function createViewWithChildren() {
 		constructor() {
 			super();
 
-			this.template = new Template( {
+			this.setTemplate( {
 				tag: 'span'
 			} );
 		}

+ 8 - 63
packages/ckeditor5-ui/tests/viewcollection.js

@@ -9,7 +9,6 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import View from '../src/view';
 import ViewCollection from '../src/viewcollection';
-import Template from '../src/template';
 import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 
 let collection;
@@ -22,7 +21,6 @@ describe( 'ViewCollection', () => {
 	describe( 'constructor()', () => {
 		it( 'sets basic properties and attributes', () => {
 			expect( collection.locale ).to.be.undefined;
-			expect( collection.ready ).to.be.false;
 			expect( collection._parentElement ).to.be.null;
 			expect( collection._idProperty ).to.equal( 'viewUid' );
 		} );
@@ -46,7 +44,6 @@ describe( 'ViewCollection', () => {
 				} ).to.not.throw();
 
 				expect( () => {
-					collection.ready = true;
 					collection.add( viewA );
 					collection.remove( viewA );
 				} ).to.not.throw();
@@ -78,7 +75,7 @@ describe( 'ViewCollection', () => {
 				function getView( text ) {
 					const view = new View();
 
-					view.template = new Template( {
+					view.setTemplate( {
 						tag: 'li',
 						children: [
 							{
@@ -95,7 +92,7 @@ describe( 'ViewCollection', () => {
 				collection.add( viewB );
 
 				// Put the collection in the template.
-				view.template = new Template( {
+				view.setTemplate( {
 					tag: 'ul',
 					children: collection
 				} );
@@ -110,8 +107,7 @@ describe( 'ViewCollection', () => {
 				collection.add( viewC );
 				collection.remove( 1 );
 
-				// Finally init the view. Check what's in there.
-				view.init();
+				view.render();
 
 				expect( view.element.childNodes ).to.have.length( 2 );
 				expect( view.element.childNodes[ 0 ] ).to.equal( viewA.element );
@@ -120,45 +116,6 @@ describe( 'ViewCollection', () => {
 		} );
 	} );
 
-	describe( 'init()', () => {
-		it( 'should throw if already initialized', () => {
-			collection.init();
-
-			try {
-				collection.init();
-				throw new Error( 'This should not be executed.' );
-			} catch ( err ) {
-				expect( err ).to.be.instanceof( CKEditorError );
-				expect( err.message ).to.match( /ui-viewcollection-init-reinit/ );
-			}
-		} );
-
-		it( 'calls #init on all views in the collection', () => {
-			const viewA = new View();
-			const viewB = new View();
-
-			viewA.element = document.createElement( 'a' );
-			viewB.element = document.createElement( 'b' );
-
-			const spyA = testUtils.sinon.spy( viewA, 'init' );
-			const spyB = testUtils.sinon.spy( viewB, 'init' );
-
-			collection.setParent( document.body );
-
-			collection.add( viewA );
-			collection.add( viewB );
-
-			collection.init();
-			sinon.assert.calledOnce( spyA );
-			sinon.assert.calledOnce( spyB );
-			sinon.assert.callOrder( spyA, spyB );
-
-			expect( viewA.element.parentNode ).to.equal( collection._parentElement );
-			expect( viewA.element.nextSibling ).to.equal( viewB.element );
-			expect( collection.ready ).to.be.true;
-		} );
-	} );
-
 	describe( 'destroy()', () => {
 		it( 'calls #destroy on all views in the collection', () => {
 			const viewA = new View();
@@ -178,26 +135,14 @@ describe( 'ViewCollection', () => {
 	} );
 
 	describe( 'add()', () => {
-		it( 'initializes the new view in the collection', () => {
-			let view = new View();
-			let spy = testUtils.sinon.spy( view, 'init' );
-
-			expect( collection.ready ).to.be.false;
-			expect( view.ready ).to.be.false;
-
-			collection.add( view );
-			expect( collection.ready ).to.be.false;
-			expect( view.ready ).to.be.false;
-
-			sinon.assert.notCalled( spy );
-
-			view = new View();
-			spy = testUtils.sinon.spy( view, 'init' );
+		it( 'renders the new view in the collection', () => {
+			const view = new View();
+			const spy = testUtils.sinon.spy( view, 'render' );
 
-			collection.ready = true;
+			expect( view.isRendered ).to.be.false;
 
 			collection.add( view );
-			expect( view.ready ).to.be.true;
+			expect( view.isRendered ).to.be.true;
 			sinon.assert.calledOnce( spy );
 		} );
 

+ 2 - 2
packages/ckeditor5-ui/theme/components/panel/stickypanel.scss

@@ -3,12 +3,12 @@
 
 @include ck-editor {
 	.ck-sticky-panel {
-		&.ck-sticky-panel_sticky {
+		.ck-sticky-panel__content_sticky {
 			z-index: ck-z( 'modal' ); // #315
 			position: fixed;
 			top: 0;
 
-			&.ck-sticky-panel_sticky_bottom-limit {
+			&_bottom-limit {
 				top: auto;
 				position: absolute;
 			}