Selaa lähdekoodia

Split ObservableMixin from Model and moved model to ui/. Plus accompanying changes.

Piotrek Koszuliński 10 vuotta sitten
vanhempi
commit
04fee29ba9

+ 9 - 8
packages/ckeditor5-ui/src/config.js

@@ -5,17 +5,18 @@
 
 'use strict';
 
-import Model from './model.js';
+import ObservableMixin from './observablemixin.js';
 import utilsLang from './lib/lodash/lang.js';
+import utils from './utils.js';
 
 /**
  * Handles a configuration dictionary.
  *
- * @class Config
- * @extends Model
+ * @class core.Config
+ * @mixins core.ObservableMixin
  */
 
-export default class Config extends Model {
+export default class Config {
 	/**
 	 * Creates an instance of the {@link Config} class.
 	 *
@@ -23,8 +24,6 @@ export default class Config extends Model {
 	 * @constructor
 	 */
 	constructor( configurations ) {
-		super();
-
 		if ( configurations ) {
 			this.set( configurations );
 		}
@@ -71,7 +70,7 @@ export default class Config extends Model {
 		// Just pass the call to the original set() in case of an object. It'll deal with recursing through the
 		// object and calling set( name, value ) again for each property.
 		if ( utilsLang.isObject( name ) ) {
-			super.set.apply( this, arguments );
+			ObservableMixin.set.apply( this, arguments );
 
 			return;
 		}
@@ -114,7 +113,7 @@ export default class Config extends Model {
 		}
 
 		// Call the original set() on the target.
-		super.set.call( target, name, value );
+		ObservableMixin.set.call( target, name, value );
 	}
 
 	/**
@@ -190,3 +189,5 @@ export default class Config extends Model {
 		this.definition.set( name, value );
 	}
 }
+
+utils.mix( Config, ObservableMixin );

+ 10 - 3
packages/ckeditor5-ui/src/emittermixin.js

@@ -15,8 +15,9 @@ let eventsCounter = 0;
 /**
  * Mixin that injects the events API into its host.
  *
- * @class EmitterMixin
  * @singleton
+ * @class core.EmitterMixin
+ * @implementes core.Emitter
  */
 
 const EmitterMixin = {
@@ -111,7 +112,7 @@ const EmitterMixin = {
 	/**
 	 * Registers a callback function to be executed when an event is fired in a specific (emitter) object.
 	 *
-	 * @param {Emitter} emitter The object that fires the event.
+	 * @param {core.Emitter} emitter The object that fires the event.
 	 * @param {String} event The name of the event.
 	 * @param {Function} callback The function to be called on event.
 	 * @param {Object} [ctx] The object that represents `this` in the callback. Defaults to `emitter`.
@@ -168,7 +169,7 @@ const EmitterMixin = {
 	 * * To stop listening to all events fired by a specific object.
 	 * * To stop listening to all events fired by all object.
 	 *
-	 * @param {Emitter} [emitter] The object to stop listening to. If omitted, stops it for all objects.
+	 * @param {core.Emitter} [emitter] The object to stop listening to. If omitted, stops it for all objects.
 	 * @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
 	 * for all events from `emitter`.
 	 * @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
@@ -297,3 +298,9 @@ function getCallbacksIfAny( source, event ) {
 
 	return callbacks;
 }
+
+/**
+ * Interface representing classes which mix in {@link core.EmitterMixin}.
+ *
+ * @interface core.Emitter
+ */

+ 0 - 646
packages/ckeditor5-ui/src/model.js

@@ -1,646 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-import EmitterMixin from './emittermixin.js';
-import CKEditorError from './ckeditorerror.js';
-import utilsObject from './lib/lodash/object.js';
-import utilsLang from './lib/lodash/lang.js';
-import utils from './utils.js';
-
-/**
- * The base MVC model class.
- *
- * @class Model
- * @mixins EventEmitter
- */
-
-export default class Model {
-	/**
-	 * Creates a new Model instance.
-	 *
-	 * @param {Object} [attributes] The model state attributes to be set during the instance creation.
-	 * @param {Object} [properties] The properties to be appended to the instance during creation.
-	 * @method constructor
-	 */
-	constructor( attributes, properties ) {
-		/**
-		 * The internal hash containing the model's state.
-		 *
-		 * @property _attributes
-		 * @private
-		 */
-		this._attributes = {};
-
-		/**
-		 * Map containing bindings to external models. It shares the binding objects
-		 * (`{ model: A, attr: 'a', to: ... }`) with {@link #_boundAttributes} and
-		 * it is used to observe external models to update own attributes accordingly.
-		 * See {@link #bind}.
-		 *
-		 *		A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
-		 *		console.log( A._boundModels );
-		 *
-		 *			Map( {
-		 *				B: {
-		 *					x: Set( [
-		 *						{ model: A, attr: 'a', to: [ [ B, 'x' ] ] },
-		 *						{ model: A, attr: 'c', to: [ [ B, 'x' ] ] }
-		 *					] ),
-		 *					y: Set( [
-		 *						{ model: A, attr: 'b', to: [ [ B, 'y' ] ] },
-		 *					] )
-		 *				}
-		 *			} )
-		 *
-		 *		A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
-		 *		console.log( A._boundModels );
-		 *
-		 *			Map( {
-		 *				B: {
-		 *					x: Set( [
-		 *						{ model: A, attr: 'a', to: [ [ B, 'x' ] ] },
-		 *						{ model: A, attr: 'c', to: [ [ B, 'x' ] ] }
-		 *					] ),
-		 *					y: Set( [
-		 *						{ model: A, attr: 'b', to: [ [ B, 'y' ] ] },
-		 *					] ),
-		 *					z: Set( [
-		 *						{ model: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
-		 *					] )
-		 *				},
-		 *				C: {
-		 *					w: Set( [
-		 *						{ model: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
-		 *					] )
-		 *				}
-		 *			} )
-		 *
-		 * @private
-		 * @property {Map}
-		 */
-		this._boundModels = new Map();
-
-		/**
-		 * Object that stores which attributes of this model are bound and how. It shares
-		 * the binding objects (`{ model: A, attr: 'a', to: ... }`) with {@link #_boundModels}.
-		 * This data structure is a reverse of {@link #_boundModels} and it is helpful for {@link #unbind}.
-		 * See {@link #bind}.
-		 *
-		 *		A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
-		 *		console.log( A._boundAttributes );
-		 *
-		 *			{
-		 *				a: { model: A, attr: 'a', to: [ [ B, 'x' ] ] },
-		 *				b: { model: A, attr: 'b', to: [ [ B, 'y' ] ] },
-		 *				c: { model: A, attr: 'c', to: [ [ B, 'x' ] ] }
-		 *			}
-		 *
-		 *		A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
-		 *		console.log( A._boundAttributes );
-		 *
-		 *			{
-		 *				a: { model: A, attr: 'a', to: [ [ B, 'x' ] ] },
-		 *				b: { model: A, attr: 'b', to: [ [ B, 'y' ] ] },
-		 *				c: { model: A, attr: 'c', to: [ [ B, 'x' ] ] },
-		 *				d: { model: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
-		 *			}
-		 *
-		 * @private
-		 * @property {Object}
-		 */
-		this._boundAttributes = {};
-
-		// Extend this instance with the additional (out of state) properties.
-		if ( properties ) {
-			utilsObject.extend( this, properties );
-		}
-
-		// Initialize the attributes.
-		if ( attributes ) {
-			this.set( attributes );
-		}
-	}
-
-	/**
-	 * Creates and sets the value of a model attribute of this object. This attribute will be part of the model
-	 * state and will be observable.
-	 *
-	 * It accepts also a single object literal containing key/value pairs with attributes to be set.
-	 *
-	 * This method throws the {@link model-set-cannot-override} error if the model instance already
-	 * have a property with a given attribute name. This prevents from mistakenly overriding existing
-	 * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
-	 *
-	 * @param {String} name The attributes name.
-	 * @param {*} value The attributes value.
-	 */
-	set( name, value ) {
-		// If the first parameter is an Object, we gonna interact through its properties.
-		if ( utilsLang.isObject( name ) ) {
-			Object.keys( name ).forEach( ( attr ) => {
-				this.set( attr, name[ attr ] );
-			}, this );
-
-			return;
-		}
-
-		if ( ( name in this ) && !( name in this._attributes ) ) {
-			/**
-			 * Cannot override an existing property.
-			 *
-			 * This error is thrown when trying to {@link Model#set set} an attribute with
-			 * a name of an already existing property. For example:
-			 *
-			 *		let model = new Model();
-			 *		model.property = 1;
-			 *		model.set( 'property', 2 );		// throws
-			 *
-			 *		model.set( 'attr', 1 );
-			 *		model.set( 'attr', 2 );			// ok, because this is an existing attribute.
-			 *
-			 * @error model-set-cannot-override
-			 */
-			throw new CKEditorError( 'model-set-cannot-override: Cannot override an existing property.' );
-		}
-
-		Object.defineProperty( this, name, {
-			enumerable: true,
-			configurable: true,
-
-			get: () => {
-				return this._attributes[ name ];
-			},
-
-			set: ( value ) => {
-				const oldValue = this._attributes[ name ];
-
-				// Allow undefined as an initial value like A.set( 'x', undefined ) (#132).
-				// Note: When _attributes has no such own property, then its value is undefined.
-				if ( oldValue !== value || !this._attributes.hasOwnProperty( name ) ) {
-					this._attributes[ name ] = value;
-					this.fire( 'change', name, value, oldValue );
-					this.fire( 'change:' + name, value, oldValue );
-				}
-			}
-		} );
-
-		this[ name ] = value;
-	}
-
-	/**
-	 * Binds model attributes to another {@link Model} instance.
-	 *
-	 * Once bound, the model will immediately share the current state of attributes
-	 * of the model it is bound to and react to the changes to these attributes
-	 * in the future.
-	 *
-	 * **Note**: To release the binding use {@link #unbind}.
-	 *
-	 *		A.bind( 'a' ).to( B );
-	 *		A.bind( 'a' ).to( B, 'b' );
-	 *		A.bind( 'a', 'b' ).to( B, 'c', 'd' );
-	 *		A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
-	 *
-	 * @param {String...} bindAttrs Model attributes use that will be bound to another model(s).
-	 * @returns {BindChain}
-	 */
-	bind( ...bindAttrs ) {
-		if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) {
-			/**
-			 * All attributes must be strings.
-			 *
-			 * @error model-bind-wrong-attrs
-			 */
-			throw new CKEditorError( 'model-bind-wrong-attrs: All attributes must be strings.' );
-		}
-
-		if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) {
-			/**
-			 * Attributes must be unique.
-			 *
-			 * @error model-bind-duplicate-attrs
-			 */
-			throw new CKEditorError( 'model-bind-duplicate-attrs: Attributes must be unique.' );
-		}
-
-		bindAttrs.forEach( attrName => {
-			if ( attrName in this._boundAttributes ) {
-				/**
-				 * Cannot bind the same attribute more that once.
-				 *
-				 * @error model-bind-rebind
-				 */
-				throw new CKEditorError( 'model-bind-rebind: Cannot bind the same attribute more that once.' );
-			}
-		} );
-
-		const bindings = {};
-
-		/**
-		 * @typedef Binding
-		 * @type Object
-		 * @property {Array} attr Attribute which is bound.
-		 * @property {Array} to Array of model–attribute components of the binding (`{ model: ..., attr: .. }`).
-		 * @property {Array} callback A function which processes `to` components.
-		 */
-		bindAttrs.forEach( a => {
-			this._boundAttributes[ a ] = bindings[ a ] = { attr: a, to: [] };
-		} );
-
-		/**
-		 * @typedef BindChain
-		 * @type Object
-		 * @property {Function} to See {@link #_bindTo}.
-		 * @property {Model} _model The model which initializes the binding.
-		 * @property {Array} _bindAttrs Array of `_model` attributes to be bound.
-		 * @property {Array} _to Array of `to()` model–attributes (`{ model: toModel, attrs: ...toAttrs }`).
-		 * @property {Object} _bindings Stores bindings to be kept in {@link #_boundAttributes}/{@link #_boundModels}
-		 * initiated in this binding chain.
-		 */
-		return {
-			to: this._bindTo,
-
-			_model: this,
-			_bindAttrs: bindAttrs,
-			_to: [],
-			_bindings: bindings
-		};
-	}
-
-	/**
-	 * Removes the binding created with {@link #bind}.
-	 *
-	 *		A.unbind( 'a' );
-	 *		A.unbind();
-	 *
-	 * @param {String...} [bindAttrs] Model attributes to unbound. All the bindings will
-	 * be released if not attributes provided.
-	 */
-	unbind( ...unbindAttrs ) {
-		if ( unbindAttrs.length ) {
-			if ( !isStringArray( unbindAttrs ) ) {
-				/**
-				 * Attributes must be strings.
-				 *
-				 * @error model-unbind-wrong-attrs
-				 */
-				throw new CKEditorError( 'model-unbind-wrong-attrs: Attributes must be strings.' );
-			}
-
-			unbindAttrs.forEach( attrName => {
-				const binding = this._boundAttributes[ attrName ];
-				let toModel, toAttr, toAttrs, toAttrBindings;
-
-				binding.to.forEach( to => {
-					// TODO: ES6 destructuring.
-					toModel = to[ 0 ];
-					toAttr = to[ 1 ];
-					toAttrs = this._boundModels.get( toModel );
-					toAttrBindings = toAttrs[ toAttr ];
-
-					toAttrBindings.delete( binding );
-
-					if ( !toAttrBindings.size ) {
-						delete toAttrs[ toAttr ];
-					}
-
-					if ( !Object.keys( toAttrs ).length ) {
-						this._boundModels.delete( toModel );
-						this.stopListening( toModel, 'change' );
-					}
-				} );
-
-				delete this._boundAttributes[ attrName ];
-			} );
-		} else {
-			this._boundModels.forEach( ( bindings, boundModel ) => {
-				this.stopListening( boundModel, 'change' );
-			} );
-
-			this._boundModels.clear();
-			this._boundAttributes = {};
-		}
-	}
-
-	/**
-	 * A chaining for {@link #bind} providing `.to()` interface.
-	 *
-	 * @protected
-	 * @param {...[Model|String|Function]} args Arguments of the `.to( args )` binding.
-	 */
-	_bindTo( ...args ) {
-		const parsedArgs = parseBindToArgs( ...args );
-		const bindingsKeys = Object.keys( this._bindings );
-		const numberOfBindings = bindingsKeys.length;
-
-		// Eliminate A.bind( 'x' ).to( B, C )
-		if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
-			/**
-			 * Binding multiple models only possible with callback.
-			 *
-			 * @error model-bind-no-callback
-			 */
-			throw new CKEditorError( 'model-bind-to-no-callback: Binding multiple models only possible with callback.' ) ;
-		}
-
-		// Eliminate A.bind( 'x', 'y' ).to( B, callback )
-		if ( numberOfBindings > 1 && parsedArgs.callback ) {
-			/**
-			 * Cannot bind multiple attributes and use a callback in one binding.
-			 *
-			 * @error model-bind-to-extra-callback
-			 */
-			throw new CKEditorError( 'model-bind-to-extra-callback: Cannot bind multiple attributes and use a callback in one binding.' ) ;
-		}
-
-		parsedArgs.to.forEach( to => {
-			// Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
-			if ( to.attrs.length && to.attrs.length !== numberOfBindings ) {
-				/**
-				 * The number of attributes must match.
-				 *
-				 * @error model-bind-to-attrs-length
-				 */
-				throw new CKEditorError( 'model-bind-to-attrs-length: The number of attributes must match.' );
-			}
-
-			// When no to.attrs specified, observing MODEL attributes instead.
-			if ( !to.attrs.length ) {
-				to.attrs = this._bindAttrs;
-			}
-
-			// Eliminate A.bind( 'x', 'y' ).to( B, 'a', 'b' ) when B has no 'a'.
-			if ( !hasAttributes( to.model, to.attrs ) ) {
-				/*
-				 * Model has no such attribute(s).
-				 *
-				 * @error model-bind-to-missing-attr
-				 */
-				throw new CKEditorError( 'model-bind-to-missing-attr: Model has no such attribute(s).' );
-			}
-		} );
-
-		this._to = parsedArgs.to;
-
-		// Fill {@link BindChain#_bindings} with callback.
-		if ( parsedArgs.callback ) {
-			this._bindings[ bindingsKeys[ 0 ] ].callback = parsedArgs.callback;
-		}
-
-		attachBindToListeners( this._model, this._to );
-
-		// Update model._boundAttributes and model._boundModels.
-		updateBindToBound( this );
-
-		// Set initial values of bound attributes.
-		this._bindAttrs.forEach( attrName => {
-			updateBoundModelAttr( this._model, attrName );
-		} );
-	}
-}
-
-/**
- * Check if the {@link Model} has given `attrs`.
- *
- * @private
- * @param {Model} model Model to be checked.
- * @param {Array} arr An array of `String`.
- * @returns {Boolean}
- */
-function hasAttributes( model, attrs ) {
-	return attrs.every( a => a in model._attributes );
-}
-
-/**
- * Check if all entries of the array are of `String` type.
- *
- * @private
- * @param {Array} arr An array to be checked.
- * @returns {Boolean}
- */
-function isStringArray( arr ) {
-	return arr.every( a => typeof a == 'string' );
-}
-
-/**
- * Parses and validates {@link Model#bind}`.to( args )` arguments and returns
- * an object with a parsed structure. For example
- *
- *		A.bind( 'x' ).to( B, 'a', C, 'b', call );
- *
- * becomes
- *
- *		{
- *			to: [
- *				{ model: B, attrs: [ 'a' ] },
- *				{ model: C, attrs: [ 'b' ] },
- *			],
- *			callback: call
- * 		}
- *
- * @private
- * @param {...*} args Arguments of {@link Model#bind}`.to( args )`.
- * @returns {Object}
- */
-function parseBindToArgs( ...args ) {
-	// Eliminate A.bind( 'x' ).to()
-	if ( !args.length ) {
-		/**
-		 * Invalid argument syntax in `to()`.
-		 *
-		 * @error model-bind-to-parse-error
-		 */
-		throw new CKEditorError( 'model-bind-to-parse-error: Invalid argument syntax in `to()`.' );
-	}
-
-	const parsed = { to: [] };
-	let lastModel;
-
-	args.forEach( a => {
-		// Callback has already been defined.
-		// Eliminate A.bind( 'x' ).to( B, 'a', callback, C )
-		if ( parsed.callback ) {
-			throw new CKEditorError( 'model-bind-to-parse-error: Invalid argument syntax in `to()`.' );
-		} else if ( a instanceof Model ) {
-			parsed.to.push( ( lastModel = { model: a, attrs: [] } ) );
-		} else if ( typeof a == 'string' ) {
-			lastModel.attrs.push( a );
-		} else if ( typeof a == 'function' ) {
-			parsed.callback = a;
-		}
-		// Eliminate A.bind( 'x' ).to( null, new Date(), etc. )
-		else {
-			throw new CKEditorError( 'model-bind-to-parse-error: Invalid argument syntax in `to()`.' );
-		}
-	} );
-
-	return parsed;
-}
-
-/**
- * Synchronizes {@link Model#_boundModels} with {@link Binding}.
- *
- * @private
- * @param {Binding} binding A binding to store in {@link Model#_boundModels}.
- * @param {Model} toModel A model, which is a new component of `binding`.
- * @param {String} toAttrName A name of `toModel`'s attribute, a new component of the `binding`.
- */
-function updateBoundModels( model, binding, toModel, toAttrName ) {
-	const bindingsToModel = model._boundModels.get( toModel );
-	const bindings = bindingsToModel || {};
-
-	if ( !bindings[ toAttrName ] ) {
-		bindings[ toAttrName ] = new Set();
-	}
-
-	// Pass the binding to a corresponding Set in `model._boundModels`.
-	bindings[ toAttrName ].add( binding );
-
-	if ( !bindingsToModel ) {
-		model._boundModels.set( toModel, bindings );
-	}
-}
-
-/**
- * Synchronizes {@link Model#_boundAttributes} and {@link Model#_boundModels}
- * with {@link BindChain}.
- *
- * Assuming the following binding being created
- *
- * 		A.bind( 'a', 'b' ).to( B, 'x', 'y' );
- *
- * the following bindings were initialized by {@link Model#bind} in {@link BindChain#_bindings}:
- *
- * 		{
- * 			a: { model: A, attr: 'a', to: [] },
- * 			b: { model: A, attr: 'b', to: [] },
- * 		}
- *
- * Iterate over all bindings in this chain and fill their `to` properties with
- * corresponding to( ... ) arguments (components of the binding), so
- *
- * 		{
- * 			a: { model: A, attr: 'a', to: [ B, 'x' ] },
- * 			b: { model: A, attr: 'b', to: [ B, 'y' ] },
- * 		}
- *
- * Then update the structure of {@link Model#_boundModels} with updated
- * binding, so it becomes:
- *
- * 		Map( {
- * 			B: {
- * 				x: Set( [
- * 					{ model: A, attr: 'a', to: [ [ B, 'x' ] ] }
- * 				] ),
- * 				y: Set( [
- * 					{ model: A, attr: 'b', to: [ [ B, 'y' ] ] },
- * 				] )
- *			}
- * 		} )
- *
- * @private
- * @param {BindChain} chain The binding initialized by {@link Model#bind}.
- */
-function updateBindToBound( chain ) {
-	let binding, toAttr;
-
-	for ( let attrName in chain._bindings ) {
-		binding = chain._bindings[ attrName ];
-
-		// Note: For a binding without a callback, this will run only once
-		// like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
-		// TODO: ES6 destructuring.
-		chain._to.forEach( to => {
-			toAttr = to.attrs[ binding.callback ? 0 : chain._bindAttrs.indexOf( attrName ) ];
-
-			binding.to.push( [ to.model, toAttr ] );
-			updateBoundModels( chain._model, binding, to.model, toAttr );
-		} );
-	}
-}
-
-/**
- * Updates an attribute of a {@link Model} with a value
- * determined by an entry in {@link Model#_boundAttributes}.
- *
- * @private
- * @param {Model} model A model which attribute is to be updated.
- * @param {String} attrName An attribute to be updated.
- */
-function updateBoundModelAttr( model, attrName ) {
-	const binding = model._boundAttributes[ attrName ];
-	let attrValue;
-
-	// When a binding with callback is created like
-	//
-	// 		A.bind( 'a' ).to( B, 'b', C, 'c', callback );
-	//
-	// collect B.b and C.c, then pass them to callback to set A.a.
-	if ( binding.callback ) {
-		attrValue = binding.callback.apply( model, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
-	} else {
-		attrValue = binding.to[ 0 ];
-		attrValue = attrValue[ 0 ][ attrValue[ 1 ] ];
-	}
-
-	// TODO: Needs update after https://github.com/ckeditor/ckeditor5-core/issues/132.
-	if ( model.hasOwnProperty( attrName ) ) {
-		model[ attrName ] = attrValue;
-	} else {
-		model.set( attrName, attrValue );
-	}
-}
-
-/**
- * Starts listening to changes in {@link BindChain._to} models to update
- * {@link BindChain._model} {@link BindChain._bindAttrs}. Also sets the
- * initial state of {@link BindChain._model}.
- *
- * @private
- * @param {BindChain} chain The chain initialized by {@link Model#bind}.
- */
-function attachBindToListeners( model, toBindings ) {
-	toBindings.forEach( to => {
-		const boundModels = model._boundModels;
-		let bindings;
-
-		// If there's already a chain between the models (`model` listens to
-		// `to.model`), there's no need to create another `change` event listener.
-		if ( !boundModels.get( to.model ) ) {
-			model.listenTo( to.model, 'change', ( evt, attrName ) => {
-				bindings = boundModels.get( to.model )[ attrName ];
-
-				// Note: to.model will fire for any attribute change, react
-				// to changes of attributes which are bound only.
-				if ( bindings ) {
-					bindings.forEach( binding => {
-						updateBoundModelAttr( model, binding.attr );
-					} );
-				}
-			} );
-		}
-	} );
-}
-
-utils.mix( Model, EmitterMixin );
-
-/**
- * Fired when an attribute changed value.
- *
- * @event change
- * @param {String} name The attribute name.
- * @param {*} value The new attribute value.
- * @param {*} oldValue The previous attribute value.
- */
-
-/**
- * Fired when an specific attribute changed value.
- *
- * @event change:{attribute}
- * @param {*} value The new attribute value.
- * @param {*} oldValue The previous attribute value.
- */

+ 653 - 0
packages/ckeditor5-ui/src/observablemixin.js

@@ -0,0 +1,653 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import EmitterMixin from './emittermixin.js';
+import CKEditorError from './ckeditorerror.js';
+import utilsObject from './lib/lodash/object.js';
+import utilsLang from './lib/lodash/lang.js';
+
+const attributesSymbol = Symbol( 'attributes' );
+const boundObservablesSymbol = Symbol( 'boundObservables' );
+const boundAttributesSymbol = Symbol( 'boundAttributes' );
+
+/**
+ * Mixin that injects the "observable attributes" and data binding functionality.
+ * Used mainly in the {@link core.ui.Model} class.
+ *
+ * @singleton
+ * @class core.ObservableMixin
+ * @mixins EmitterMixin
+ * @implements core.Observable
+ */
+
+const ObservableMixin = {
+	/**
+	 * Creates and sets the value of a observable attribute of this object. This attribute will be part of the observable
+	 * state and will be observable.
+	 *
+	 * It accepts also a single object literal containing key/value pairs with attributes to be set.
+	 *
+	 * This method throws the {@link observable-set-cannot-override} error if the observable instance already
+	 * have a property with a given attribute name. This prevents from mistakenly overriding existing
+	 * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
+	 *
+	 * @param {String} name The attributes name.
+	 * @param {*} value The attributes value.
+	 */
+	set( name, value ) {
+		// If the first parameter is an Object, we gonna interact through its properties.
+		if ( utilsLang.isObject( name ) ) {
+			Object.keys( name ).forEach( ( attr ) => {
+				this.set( attr, name[ attr ] );
+			}, this );
+
+			return;
+		}
+
+		initObservable( this );
+
+		const attributes = this[ attributesSymbol ];
+
+		if ( ( name in this ) && !attributes.has( name ) ) {
+			/**
+			 * Cannot override an existing property.
+			 *
+			 * This error is thrown when trying to {@link core.ui.Observable#set set} an attribute with
+			 * a name of an already existing property. For example:
+			 *
+			 *		let observable = new Model();
+			 *		observable.property = 1;
+			 *		observable.set( 'property', 2 );		// throws
+			 *
+			 *		observable.set( 'attr', 1 );
+			 *		observable.set( 'attr', 2 );			// ok, because this is an existing attribute.
+			 *
+			 * @error observable-set-cannot-override
+			 */
+			throw new CKEditorError( 'observable-set-cannot-override: Cannot override an existing property.' );
+		}
+
+		Object.defineProperty( this, name, {
+			enumerable: true,
+			configurable: true,
+
+			get() {
+				return attributes.get( name );
+			},
+
+			set( value ) {
+				const oldValue = attributes.get( name );
+
+				// Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
+				// Note: When attributes map has no such own property, then its value is undefined.
+				if ( oldValue !== value || !attributes.has( name ) ) {
+					attributes.set( name, value );
+					this.fire( 'change', name, value, oldValue );
+					this.fire( 'change:' + name, value, oldValue );
+				}
+			}
+		} );
+
+		this[ name ] = value;
+	},
+
+	/**
+	 * Binds observable attributes to another objects implementing {@link ObservableMixin} interface (like {@link core.ui.Model}).
+	 *
+	 * Once bound, the observable will immediately share the current state of attributes
+	 * of the observable it is bound to and react to the changes to these attributes
+	 * in the future.
+	 *
+	 * **Note**: To release the binding use {@link #unbind}.
+	 *
+	 *		A.bind( 'a' ).to( B );
+	 *		A.bind( 'a' ).to( B, 'b' );
+	 *		A.bind( 'a', 'b' ).to( B, 'c', 'd' );
+	 *		A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
+	 *
+	 * @param {String...} bindAttrs Observable attributes that will be bound to another observable(s).
+	 * @returns {BindChain}
+	 */
+	bind( ...bindAttrs ) {
+		if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) {
+			/**
+			 * All attributes must be strings.
+			 *
+			 * @error observable-bind-wrong-attrs
+			 */
+			throw new CKEditorError( 'observable-bind-wrong-attrs: All attributes must be strings.' );
+		}
+
+		if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) {
+			/**
+			 * Attributes must be unique.
+			 *
+			 * @error observable-bind-duplicate-attrs
+			 */
+			throw new CKEditorError( 'observable-bind-duplicate-attrs: Attributes must be unique.' );
+		}
+
+		initObservable( this );
+
+		const boundAttributes = this[ boundAttributesSymbol ];
+
+		bindAttrs.forEach( attrName => {
+			if ( boundAttributes.has( attrName ) ) {
+				/**
+				 * Cannot bind the same attribute more that once.
+				 *
+				 * @error observable-bind-rebind
+				 */
+				throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same attribute more that once.' );
+			}
+		} );
+
+		const bindings = new Map();
+
+		/**
+		 * @typedef Binding
+		 * @type Object
+		 * @property {Array} attr Attribute which is bound.
+		 * @property {Array} to Array of observable–attribute components of the binding (`{ observable: ..., attr: .. }`).
+		 * @property {Array} callback A function which processes `to` components.
+		 */
+		bindAttrs.forEach( a => {
+			const binding = { attr: a, to: [] };
+
+			boundAttributes.set( a, binding );
+			bindings.set( a, binding );
+		} );
+
+		/**
+		 * @typedef BindChain
+		 * @type Object
+		 * @property {Function} to See {@link #_bindTo}.
+		 * @property {Observable} _observable The observable which initializes the binding.
+		 * @property {Array} _bindAttrs Array of `_observable` attributes to be bound.
+		 * @property {Array} _to Array of `to()` observable–attributes (`{ observable: toObservable, attrs: ...toAttrs }`).
+		 * @property {Map} _bindings Stores bindings to be kept in {@link #_boundAttributes}/{@link #_boundObservables}
+		 * initiated in this binding chain.
+		 */
+		return {
+			to: bindTo,
+
+			_observable: this,
+			_bindAttrs: bindAttrs,
+			_to: [],
+			_bindings: bindings
+		};
+	},
+
+	/**
+	 * Removes the binding created with {@link #bind}.
+	 *
+	 *		A.unbind( 'a' );
+	 *		A.unbind();
+	 *
+	 * @param {String...} [bindAttrs] Observable attributes to unbound. All the bindings will
+	 * be released if not attributes provided.
+	 */
+	unbind( ...unbindAttrs ) {
+		// Nothing to do here if not inited yet.
+		if ( !( attributesSymbol in this ) ) {
+			return;
+		}
+
+		const boundAttributes = this[ boundAttributesSymbol ];
+		const boundObservables = this[ boundObservablesSymbol ];
+
+		if ( unbindAttrs.length ) {
+			if ( !isStringArray( unbindAttrs ) ) {
+				/**
+				 * Attributes must be strings.
+				 *
+				 * @error observable-unbind-wrong-attrs
+				 */
+				throw new CKEditorError( 'observable-unbind-wrong-attrs: Attributes must be strings.' );
+			}
+
+			unbindAttrs.forEach( attrName => {
+				const binding = boundAttributes.get( attrName );
+				let toObservable, toAttr, toAttrs, toAttrBindings;
+
+				binding.to.forEach( to => {
+					// TODO: ES6 destructuring.
+					toObservable = to[ 0 ];
+					toAttr = to[ 1 ];
+					toAttrs = boundObservables.get( toObservable );
+					toAttrBindings = toAttrs[ toAttr ];
+
+					toAttrBindings.delete( binding );
+
+					if ( !toAttrBindings.size ) {
+						delete toAttrs[ toAttr ];
+					}
+
+					if ( !Object.keys( toAttrs ).length ) {
+						boundObservables.delete( toObservable );
+						this.stopListening( toObservable, 'change' );
+					}
+				} );
+
+				boundAttributes.delete( attrName );
+			} );
+		} else {
+			boundObservables.forEach( ( bindings, boundObservable ) => {
+				this.stopListening( boundObservable, 'change' );
+			} );
+
+			boundObservables.clear();
+			boundAttributes.clear();
+		}
+	}
+};
+
+export default ObservableMixin;
+
+// Init symbol properties needed to for the observable mechanism to work.
+//
+// @private
+// @param {ObservableMixin} observable
+function initObservable( observable ) {
+	// Do nothing if already inited.
+	if ( attributesSymbol in observable ) {
+		return;
+	}
+
+	// The internal hash containing the observable's state.
+	//
+	// @private
+	// @property {Map} _attributes
+	Object.defineProperty( observable, attributesSymbol, {
+		value: new Map()
+	} );
+
+	// Map containing bindings to external observables. It shares the binding objects
+	// (`{ observable: A, attr: 'a', to: ... }`) with {@link #_boundAttributes} and
+	// it is used to observe external observables to update own attributes accordingly.
+	// See {@link #bind}.
+	//
+	//		A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
+	//		console.log( A._boundObservables );
+	//
+	//			Map( {
+	//				B: {
+	//					x: Set( [
+	//						{ observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
+	//						{ observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
+	//					] ),
+	//					y: Set( [
+	//						{ observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
+	//					] )
+	//				}
+	//			} )
+	//
+	//		A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
+	//		console.log( A._boundObservables );
+	//
+	//			Map( {
+	//				B: {
+	//					x: Set( [
+	//						{ observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
+	//						{ observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
+	//					] ),
+	//					y: Set( [
+	//						{ observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
+	//					] ),
+	//					z: Set( [
+	//						{ observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
+	//					] )
+	//				},
+	//				C: {
+	//					w: Set( [
+	//						{ observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
+	//					] )
+	//				}
+	//			} )
+	//
+	// @private
+	// @property {Map} _boundObservables
+	Object.defineProperty( observable, boundObservablesSymbol, {
+		value: new Map()
+	} );
+
+	// Object that stores which attributes of this observable are bound and how. It shares
+	// the binding objects (`{ observable: A, attr: 'a', to: ... }`) with {@link #_boundObservables}.
+	// This data structure is a reverse of {@link #_boundObservables} and it is helpful for {@link #unbind}.
+	// See {@link #bind}.
+	//
+	//		A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
+	//		console.log( A._boundAttributes );
+	//
+	//			Map( {
+	//				a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
+	//				b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
+	//				c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
+	//			} )
+	//
+	//		A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
+	//		console.log( A._boundAttributes );
+	//
+	//			Map( {
+	//				a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
+	//				b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
+	//				c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] },
+	//				d: { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
+	//			} )
+	//
+	// @private
+	// @property {Map} _boundAttributes
+	Object.defineProperty( observable, boundAttributesSymbol, {
+		value: new Map()
+	} );
+}
+
+// A chaining for {@link #bind} providing `.to()` interface.
+//
+// @private
+// @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding.
+function bindTo( ...args ) {
+	/* jshint validthis: true */
+	const parsedArgs = parseBindToArgs( ...args );
+	const bindingsKeys = Array.from( this._bindings.keys() );
+	const numberOfBindings = bindingsKeys.length;
+
+	// Eliminate A.bind( 'x' ).to( B, C )
+	if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
+		/**
+		 * Binding multiple observables only possible with callback.
+		 *
+		 * @error observable-bind-no-callback
+		 */
+		throw new CKEditorError( 'observable-bind-to-no-callback: Binding multiple observables only possible with callback.' ) ;
+	}
+
+	// Eliminate A.bind( 'x', 'y' ).to( B, callback )
+	if ( numberOfBindings > 1 && parsedArgs.callback ) {
+		/**
+		 * Cannot bind multiple attributes and use a callback in one binding.
+		 *
+		 * @error observable-bind-to-extra-callback
+		 */
+		throw new CKEditorError( 'observable-bind-to-extra-callback: Cannot bind multiple attributes and use a callback in one binding.' ) ;
+	}
+
+	parsedArgs.to.forEach( to => {
+		// Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
+		if ( to.attrs.length && to.attrs.length !== numberOfBindings ) {
+			/**
+			 * The number of attributes must match.
+			 *
+			 * @error observable-bind-to-attrs-length
+			 */
+			throw new CKEditorError( 'observable-bind-to-attrs-length: The number of attributes must match.' );
+		}
+
+		// When no to.attrs specified, observing model attributes instead.
+		if ( !to.attrs.length ) {
+			to.attrs = this._bindAttrs;
+		}
+
+		// Eliminate A.bind( 'x', 'y' ).to( B, 'a', 'b' ) when B has no 'a'.
+		if ( !hasAttributes( to.observable, to.attrs ) ) {
+			/*
+			 * Observable has no such attribute(s).
+			 *
+			 * @error observable-bind-to-missing-attr
+			 */
+			throw new CKEditorError( 'observable-bind-to-missing-attr: Observable has no such attribute(s).' );
+		}
+	} );
+
+	this._to = parsedArgs.to;
+
+	// Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding.
+	if ( parsedArgs.callback ) {
+		this._bindings.get( bindingsKeys[ 0 ] ).callback = parsedArgs.callback;
+	}
+
+	attachBindToListeners( this._observable, this._to );
+
+	// Update observable._boundAttributes and observable._boundObservables.
+	updateBindToBound( this );
+
+	// Set initial values of bound attributes.
+	this._bindAttrs.forEach( attrName => {
+		updateBoundObservableAttr( this._observable, attrName );
+	} );
+}
+
+// Check if the {@link core.Observable} has given `attrs`.
+//
+// @private
+// @param {Observable} observable Observable to be checked.
+// @param {Array} arr An array of `String`.
+// @returns {Boolean}
+function hasAttributes( observable, attrs ) {
+	return attrs.every( a => observable[ attributesSymbol ].has( a ) );
+}
+
+// Check if all entries of the array are of `String` type.
+//
+// @private
+// @param {Array} arr An array to be checked.
+// @returns {Boolean}
+function isStringArray( arr ) {
+	return arr.every( a => typeof a == 'string' );
+}
+
+// Parses and validates {@link Observable#bind}`.to( args )` arguments and returns
+// an object with a parsed structure. For example
+//
+//		A.bind( 'x' ).to( B, 'a', C, 'b', call );
+//
+// becomes
+//
+//		{
+//			to: [
+//				{ observable: B, attrs: [ 'a' ] },
+//				{ observable: C, attrs: [ 'b' ] },
+//			],
+//			callback: call
+// 		}
+//
+// @private
+// @param {...*} args Arguments of {@link Observable#bind}`.to( args )`.
+// @returns {Object}
+function parseBindToArgs( ...args ) {
+	// Eliminate A.bind( 'x' ).to()
+	if ( !args.length ) {
+		/**
+		 * Invalid argument syntax in `to()`.
+		 *
+		 * @error observable-bind-to-parse-error
+		 */
+		throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
+	}
+
+	const parsed = { to: [] };
+	let lastObservable;
+
+	if ( typeof args[ args.length - 1 ] == 'function' ) {
+		parsed.callback = args.pop();
+	}
+
+	args.forEach( a => {
+		if ( typeof a == 'string' ) {
+			lastObservable.attrs.push( a );
+		} else if ( typeof a == 'object' ) {
+			lastObservable = { observable: a, attrs: [] };
+			parsed.to.push( lastObservable );
+		} else {
+			throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
+		}
+	} );
+
+	return parsed;
+}
+
+// Synchronizes {@link Observable#_boundObservables} with {@link Binding}.
+//
+// @private
+// @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
+// @param {Observable} toObservable A observable, which is a new component of `binding`.
+// @param {String} toAttrName A name of `toObservable`'s attribute, a new component of the `binding`.
+function updateBoundObservables( observable, binding, toObservable, toAttrName ) {
+	const boundObservables = observable[ boundObservablesSymbol ];
+	const bindingsToObservable = boundObservables.get( toObservable );
+	const bindings = bindingsToObservable || {};
+
+	if ( !bindings[ toAttrName ] ) {
+		bindings[ toAttrName ] = new Set();
+	}
+
+	// Pass the binding to a corresponding Set in `observable._boundObservables`.
+	bindings[ toAttrName ].add( binding );
+
+	if ( !bindingsToObservable ) {
+		boundObservables.set( toObservable, bindings );
+	}
+}
+
+// Synchronizes {@link Observable#_boundAttributes} and {@link Observable#_boundObservables}
+// with {@link BindChain}.
+//
+// Assuming the following binding being created
+//
+// 		A.bind( 'a', 'b' ).to( B, 'x', 'y' );
+//
+// the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}:
+//
+// 		{
+// 			a: { observable: A, attr: 'a', to: [] },
+// 			b: { observable: A, attr: 'b', to: [] },
+// 		}
+//
+// Iterate over all bindings in this chain and fill their `to` properties with
+// corresponding to( ... ) arguments (components of the binding), so
+//
+// 		{
+// 			a: { observable: A, attr: 'a', to: [ B, 'x' ] },
+// 			b: { observable: A, attr: 'b', to: [ B, 'y' ] },
+// 		}
+//
+// Then update the structure of {@link Observable#_boundObservables} with updated
+// binding, so it becomes:
+//
+// 		Map( {
+// 			B: {
+// 				x: Set( [
+// 					{ observable: A, attr: 'a', to: [ [ B, 'x' ] ] }
+// 				] ),
+// 				y: Set( [
+// 					{ observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
+// 				] )
+//			}
+// 		} )
+//
+// @private
+// @param {BindChain} chain The binding initialized by {@link Observable#bind}.
+function updateBindToBound( chain ) {
+	let toAttr;
+
+	chain._bindings.forEach( ( binding, attrName ) => {
+		// Note: For a binding without a callback, this will run only once
+		// like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
+		// TODO: ES6 destructuring.
+		chain._to.forEach( to => {
+			toAttr = to.attrs[ binding.callback ? 0 : chain._bindAttrs.indexOf( attrName ) ];
+
+			binding.to.push( [ to.observable, toAttr ] );
+			updateBoundObservables( chain._observable, binding, to.observable, toAttr );
+		} );
+	} );
+}
+
+// Updates an attribute of a {@link Observable} with a value
+// determined by an entry in {@link Observable#_boundAttributes}.
+//
+// @private
+// @param {Observable} observable A observable which attribute is to be updated.
+// @param {String} attrName An attribute to be updated.
+function updateBoundObservableAttr( observable, attrName ) {
+	const boundAttributes = observable[ boundAttributesSymbol ];
+	const binding = boundAttributes.get( attrName );
+	let attrValue;
+
+	// When a binding with callback is created like
+	//
+	// 		A.bind( 'a' ).to( B, 'b', C, 'c', callback );
+	//
+	// collect B.b and C.c, then pass them to callback to set A.a.
+	if ( binding.callback ) {
+		attrValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
+	} else {
+		attrValue = binding.to[ 0 ];
+		attrValue = attrValue[ 0 ][ attrValue[ 1 ] ];
+	}
+
+	// TODO: Needs update after https://github.com/ckeditor/ckeditor5-core.issues/132.
+	if ( observable.hasOwnProperty( attrName ) ) {
+		observable[ attrName ] = attrValue;
+	} else {
+		observable.set( attrName, attrValue );
+	}
+}
+
+// Starts listening to changes in {@link BindChain._to} observables to update
+// {@link BindChain._observable} {@link BindChain._bindAttrs}. Also sets the
+// initial state of {@link BindChain._observable}.
+//
+// @private
+// @param {BindChain} chain The chain initialized by {@link Observable#bind}.
+function attachBindToListeners( observable, toBindings ) {
+	toBindings.forEach( to => {
+		const boundObservables = observable[ boundObservablesSymbol ];
+		let bindings;
+
+		// If there's already a chain between the observables (`observable` listens to
+		// `to.observable`), there's no need to create another `change` event listener.
+		if ( !boundObservables.get( to.observable ) ) {
+			observable.listenTo( to.observable, 'change', ( evt, attrName ) => {
+				bindings = boundObservables.get( to.observable )[ attrName ];
+
+				// Note: to.observable will fire for any attribute change, react
+				// to changes of attributes which are bound only.
+				if ( bindings ) {
+					bindings.forEach( binding => {
+						updateBoundObservableAttr( observable, binding.attr );
+					} );
+				}
+			} );
+		}
+	} );
+}
+
+utilsObject.extend( ObservableMixin, EmitterMixin );
+
+/**
+ * Fired when an attribute changed value.
+ *
+ * @event change
+ * @param {String} name The attribute name.
+ * @param {*} value The new attribute value.
+ * @param {*} oldValue The previous attribute value.
+ */
+
+/**
+ * Fired when an specific attribute changed value.
+ *
+ * @event change:{attribute}
+ * @param {*} value The new attribute value.
+ * @param {*} oldValue The previous attribute value.
+ */
+
+/**
+ * Interface representing classes which mix in {@link core.ObservableMixin}.
+ *
+ * @interface core.Observable
+ */

+ 12 - 4
packages/ckeditor5-ui/src/ui/controller.js

@@ -6,10 +6,18 @@
 'use strict';
 
 import Collection from '../collection.js';
-import Model from '../model.js';
 import CKEditorError from '../ckeditorerror.js';
+import EmitterMixin from '../emittermixin.js';
+import utils from '../utils.js';
 
-export default class Controller extends Model {
+/**
+ * Basic Controller class.
+ *
+ * @class Controller
+ * @mixins EmitterMixin
+ */
+
+export default class Controller {
 	/**
 	 * Creates an instance of the {@link Controller} class.
 	 *
@@ -18,8 +26,6 @@ export default class Controller extends Model {
 	 * @constructor
 	 */
 	constructor( model, view ) {
-		super();
-
 		/**
 		 * Model of this controller.
 		 *
@@ -186,3 +192,5 @@ export default class Controller extends Model {
 		return Promise.all( promises );
 	}
 }
+
+utils.mix( Controller, EmitterMixin );

+ 23 - 12
packages/ckeditor5-ui/src/ui/domemittermixin.js

@@ -16,10 +16,14 @@ import log from '../log.js';
  *
  * @class DOMEmitterMixin
  * @mixins EmitterMixin
- * @param {Node} node DOM Node that fires events.
- * @returns {Object} ProxyEmitter instance bound to the DOM Node.
+ * @implements DOMEmitter
  */
+
 class ProxyEmitter {
+	/**
+	 * @param {Node} node DOM Node that fires events.
+	 * @returns {Object} ProxyEmitter instance bound to the DOM Node.
+	 */
 	constructor( node ) {
 		// Set emitter ID to match DOM Node "expando" property.
 		this._emitterId = getNodeUID( node );
@@ -33,6 +37,7 @@ objectUtils.extend( ProxyEmitter.prototype, EmitterMixin, {
 	/**
 	 * Collection of native DOM listeners.
 	 *
+	 * @private
 	 * @property {Object} _domListeners
 	 */
 
@@ -146,12 +151,13 @@ objectUtils.extend( ProxyEmitter.prototype, EmitterMixin, {
  *                 +-----------------------------------------+
  *                             fire( click, DOM Event )
  *
- * @class DOMEmitterMixin
- * @extends EmitterMixin
  * @singleton
+ * @class DOMEmitterMixin
+ * @mixins EmitterMixin
+ * @implements DOMEmitter
  */
 
-const DOMEmitterMixin = {
+const DOMEmitterMixin = objectUtils.extend( {}, EmitterMixin, {
 	/**
 	 * Registers a callback function to be executed when an event is fired in a specific Emitter or DOM Node.
 	 * It is backwards compatible with {@link EmitterMixin#listenTo}.
@@ -235,16 +241,21 @@ const DOMEmitterMixin = {
 
 		return proxy || null;
 	}
-};
+} );
 
 export default DOMEmitterMixin;
 
-/**
- * Gets an unique DOM Node identifier. The identifier will be set if not defined.
- *
- * @param {Node} node
- * @return {Number} UID for given DOM Node.
- */
+// Gets an unique DOM Node identifier. The identifier will be set if not defined.
+//
+// @private
+// @param {Node} node
+// @return {Number} UID for given DOM Node.
 function getNodeUID( node ) {
 	return node[ 'data-ck-expando' ] || ( node[ 'data-ck-expando' ] = utils.uid() );
 }
+
+/**
+ * Interface representing classes which mix in {@link core.ui.DOMEmitter}.
+ *
+ * @interface core.ui.DOMEmitter
+ */

+ 40 - 0
packages/ckeditor5-ui/src/ui/model.js

@@ -0,0 +1,40 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import utilsObject from '../lib/lodash/object.js';
+import utils from '../utils.js';
+import ObservableMixin from '../observablemixin.js';
+
+/**
+ * The base MVC model class.
+ *
+ * @class core.ui.Model
+ * @mixins core.ObservableMixin
+ */
+
+export default class Model {
+	/**
+	 * Creates a new Model instance.
+	 *
+	 * @param {Object} [attributes] The model state attributes to be defined during the instance creation.
+	 * @param {Object} [properties] The (out of state) properties to be appended to the instance during creation.
+	 * @method constructor
+	 */
+	constructor( attributes, properties ) {
+		// Extend this instance with the additional (out of state) properties.
+		if ( properties ) {
+			utilsObject.extend( this, properties );
+		}
+
+		// Initialize the attributes.
+		if ( attributes ) {
+			this.set( attributes );
+		}
+	}
+}
+
+utils.mix( Model, ObservableMixin );

+ 1 - 4
packages/ckeditor5-ui/src/ui/region.js

@@ -6,7 +6,6 @@
 'use strict';
 
 import Collection from '../collection.js';
-import Model from '../model.js';
 
 /**
  * Basic Region class.
@@ -15,7 +14,7 @@ import Model from '../model.js';
  * @extends Model
  */
 
-export default class Region extends Model {
+export default class Region {
 	/**
 	 * Creates an instance of the {@link Region} class.
 	 *
@@ -24,8 +23,6 @@ export default class Region extends Model {
 	 * @constructor
 	 */
 	constructor( name ) {
-		super();
-
 		/**
 		 * The name of the region.
 		 *

+ 1 - 5
packages/ckeditor5-ui/src/ui/view.js

@@ -6,7 +6,6 @@
 'use strict';
 
 import Collection from '../collection.js';
-import Model from '../model.js';
 import Region from './region.js';
 import Template from './template.js';
 import CKEditorError from '../ckeditorerror.js';
@@ -17,11 +16,10 @@ import utils from '../utils.js';
  * Basic View class.
  *
  * @class View
- * @extends Model
  * @mixins DOMEmitterMixin
  */
 
-export default class View extends Model {
+export default class View {
 	/**
 	 * Creates an instance of the {@link View} class.
 	 *
@@ -29,8 +27,6 @@ export default class View extends Model {
 	 * @constructor
 	 */
 	constructor( model ) {
-		super();
-
 		/**
 		 * Model of this view.
 		 *

+ 96 - 170
packages/ckeditor5-ui/tests/model.js

@@ -6,56 +6,60 @@
 'use strict';
 
 import testUtils from '/tests/_utils/utils.js';
-import Model from '/ckeditor5/core/model.js';
+import ObservableMixin from '/ckeditor5/core/observablemixin.js';
+import EmitterMixin from '/ckeditor5/core/emittermixin.js';
 import EventInfo from '/ckeditor5/core/eventinfo.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
-
-let Car, car;
+import utils from '/ckeditor5/core/utils.js';
 
 testUtils.createSinonSandbox();
 
-describe( 'Model', () => {
-	beforeEach( 'Create a test model instance', () => {
-		Car = class extends Model {};
+describe( 'ObservableMixin', () => {
+	it( 'exists', () => {
+		expect( ObservableMixin ).to.be.an( 'object' );
+	} );
 
-		car = new Car( {
-			color: 'red',
-			year: 2015
-		} );
+	it( 'mixes in EmitterMixin', () => {
+		expect( ObservableMixin ).to.have.property( 'on', EmitterMixin.on );
+	} );
+
+	it( 'implements set, bind, and unbind methods', () => {
+		expect( ObservableMixin ).to.contain.keys( 'set', 'bind', 'unbind' );
 	} );
+} );
 
-	//////////
+describe( 'Observable', () => {
+	class Observable {
+		constructor( attrs ) {
+			if ( attrs ) {
+				this.set( attrs );
+			}
+		}
+	}
+	utils.mix( Observable, ObservableMixin );
+
+	let Car, car;
+
+	beforeEach( () => {
+		Car = class extends Observable {};
 
-	it( 'should set _attributes on creation', () => {
-		expect( car._attributes ).to.deep.equal( {
+		car = new Car( {
 			color: 'red',
 			year: 2015
 		} );
 	} );
 
-	it( 'should get correctly after set', () => {
-		car.color = 'blue';
-
-		expect( car.color ).to.equal( 'blue' );
-		expect( car._attributes.color ).to.equal( 'blue' );
+	it( 'should set attributes on creation', () => {
+		expect( car ).to.have.property( 'color', 'red' );
+		expect( car ).to.have.property( 'year', 2015 );
 	} );
 
-	it( 'should get correctly after setting _attributes', () => {
-		car._attributes.color = 'blue';
+	it( 'should get correctly after set', () => {
+		car.color = 'blue';
 
 		expect( car.color ).to.equal( 'blue' );
 	} );
 
-	it( 'should add properties on creation', () => {
-		let car = new Car( null, {
-			prop: 1
-		} );
-
-		expect( car ).to.have.property( 'prop' ).to.equal( 1 );
-	} );
-
-	//////////
-
 	describe( 'set', () => {
 		it( 'should work when passing an object', () => {
 			car.set( {
@@ -64,7 +68,7 @@ describe( 'Model', () => {
 				seats: 5
 			} );
 
-			expect( car._attributes ).to.deep.equal( {
+			expect( car ).to.deep.equal( {
 				color: 'blue',
 				year: 2015,
 				wheels: 4,
@@ -76,7 +80,7 @@ describe( 'Model', () => {
 			car.set( 'color', 'blue' );
 			car.set( 'wheels', 4 );
 
-			expect( car._attributes ).to.deep.equal( {
+			expect( car ).to.deep.equal( {
 				color: 'blue',
 				year: 2015,
 				wheels: 4
@@ -141,13 +145,13 @@ describe( 'Model', () => {
 
 			expect( () => {
 				car.set( 'normalProperty', 2 );
-			} ).to.throw( CKEditorError, /^model-set-cannot-override/ );
+			} ).to.throw( CKEditorError, /^observable-set-cannot-override/ );
 
 			expect( car ).to.have.property( 'normalProperty', 1 );
 		} );
 
 		it( 'should throw when overriding already existing property (in the prototype)', () => {
-			class Car extends Model {
+			class Car extends Observable {
 				method() {}
 			}
 
@@ -155,7 +159,7 @@ describe( 'Model', () => {
 
 			expect( () => {
 				car.set( 'method', 2 );
-			} ).to.throw( CKEditorError, /^model-set-cannot-override/ );
+			} ).to.throw( CKEditorError, /^observable-set-cannot-override/ );
 
 			expect( car.method ).to.be.a( 'function' );
 		} );
@@ -177,17 +181,6 @@ describe( 'Model', () => {
 		} );
 	} );
 
-	describe( 'extend', () => {
-		it( 'should create new Model based classes', () => {
-			class Truck extends Car {}
-
-			let truck = new Truck();
-
-			expect( truck ).to.be.an.instanceof( Car );
-			expect( truck ).to.be.an.instanceof( Model );
-		} );
-	} );
-
 	describe( 'bind', () => {
 		it( 'should chain for a single attribute', () => {
 			expect( car.bind( 'color' ) ).to.contain.keys( 'to' );
@@ -204,89 +197,77 @@ describe( 'Model', () => {
 		it( 'should throw when attributes are not strings', () => {
 			expect( () => {
 				car.bind();
-			} ).to.throw( CKEditorError, /model-bind-wrong-attrs/ );
+			} ).to.throw( CKEditorError, /observable-bind-wrong-attrs/ );
 
 			expect( () => {
 				car.bind( new Date() );
-			} ).to.throw( CKEditorError, /model-bind-wrong-attrs/ );
+			} ).to.throw( CKEditorError, /observable-bind-wrong-attrs/ );
 
 			expect( () => {
 				car.bind( 'color', new Date() );
-			} ).to.throw( CKEditorError, /model-bind-wrong-attrs/ );
+			} ).to.throw( CKEditorError, /observable-bind-wrong-attrs/ );
 		} );
 
 		it( 'should throw when the same attribute is used than once', () => {
 			expect( () => {
 				car.bind( 'color', 'color' );
-			} ).to.throw( CKEditorError, /model-bind-duplicate-attrs/ );
+			} ).to.throw( CKEditorError, /observable-bind-duplicate-attrs/ );
 		} );
 
 		it( 'should throw when binding the same attribute more than once', () => {
 			expect( () => {
 				car.bind( 'color' );
 				car.bind( 'color' );
-			} ).to.throw( CKEditorError, /model-bind-rebind/ );
+			} ).to.throw( CKEditorError, /observable-bind-rebind/ );
 		} );
 
 		describe( 'to', () => {
 			it( 'should not chain', () => {
 				expect(
-					car.bind( 'color' ).to( new Model( { color: 'red' } ) )
+					car.bind( 'color' ).to( new Observable( { color: 'red' } ) )
 				).to.be.undefined;
 			} );
 
-			it( 'should throw when arguments are of invalid type', () => {
+			it( 'should throw when arguments are of invalid type - empty', () => {
 				expect( () => {
 					car = new Car();
 
 					car.bind( 'color' ).to();
-				} ).to.throw( CKEditorError, /model-bind-to-parse-error/ );
-
-				expect( () => {
-					car = new Car();
-
-					car.bind( 'color' ).to( new Model(), new Date() );
-				} ).to.throw( CKEditorError, /model-bind-to-parse-error/ );
-
-				expect( () => {
-					car = new Car( { color: 'red' } );
-
-					car.bind( 'color' ).to( new Model(), 'color', new Date() );
-				} ).to.throw( CKEditorError, /model-bind-to-parse-error/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-parse-error/ );
 			} );
 
-			it( 'should throw when binding multiple attributes to multiple models', () => {
+			it( 'should throw when binding multiple attributes to multiple observables', () => {
 				let vehicle = new Car();
 				const car1 = new Car( { color: 'red', year: 1943 } );
 				const car2 = new Car( { color: 'yellow', year: 1932 } );
 
 				expect( () => {
 					vehicle.bind( 'color', 'year' ).to( car1, 'color', car2, 'year' );
-				} ).to.throw( CKEditorError, /model-bind-to-no-callback/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
 
 				expect( () => {
 					vehicle = new Car();
 
 					vehicle.bind( 'color', 'year' ).to( car1, car2 );
-				} ).to.throw( CKEditorError, /model-bind-to-no-callback/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
 
 				expect( () => {
 					vehicle = new Car();
 
 					vehicle.bind( 'color', 'year' ).to( car1, car2, 'year' );
-				} ).to.throw( CKEditorError, /model-bind-to-no-callback/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
 
 				expect( () => {
 					vehicle = new Car();
 
 					vehicle.bind( 'color', 'year' ).to( car1, 'color', car2 );
-				} ).to.throw( CKEditorError, /model-bind-to-no-callback/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
 
 				expect( () => {
 					vehicle = new Car();
 
 					vehicle.bind( 'color', 'year', 'custom' ).to( car, car );
-				} ).to.throw( CKEditorError, /model-bind-to-no-callback/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-no-callback/ );
 			} );
 
 			it( 'should throw when binding multiple attributes but passed a callback', () => {
@@ -294,13 +275,13 @@ describe( 'Model', () => {
 
 				expect( () => {
 					vehicle.bind( 'color', 'year' ).to( car, () => {} );
-				} ).to.throw( CKEditorError, /model-bind-to-extra-callback/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-extra-callback/ );
 
 				expect( () => {
 					vehicle = new Car();
 
 					vehicle.bind( 'color', 'year' ).to( car, car, () => {} );
-				} ).to.throw( CKEditorError, /model-bind-to-extra-callback/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-extra-callback/ );
 			} );
 
 			it( 'should throw when binding a single attribute but multiple callbacks', () => {
@@ -308,13 +289,13 @@ describe( 'Model', () => {
 
 				expect( () => {
 					vehicle.bind( 'color' ).to( car, () => {}, () => {} );
-				} ).to.throw( CKEditorError, /model-bind-to-parse-error/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-parse-error/ );
 
 				expect( () => {
 					vehicle = new Car();
 
 					vehicle.bind( 'color' ).to( car, car, () => {}, () => {} );
-				} ).to.throw( CKEditorError, /model-bind-to-parse-error/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-parse-error/ );
 			} );
 
 			it( 'should throw when a number of attributes does not match', () => {
@@ -322,50 +303,53 @@ describe( 'Model', () => {
 
 				expect( () => {
 					vehicle.bind( 'color' ).to( car, 'color', 'year' );
-				} ).to.throw( CKEditorError, /model-bind-to-attrs-length/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-attrs-length/ );
 
 				expect( () => {
 					vehicle = new Car();
 
 					vehicle.bind( 'color', 'year' ).to( car, 'color' );
-				} ).to.throw( CKEditorError, /model-bind-to-attrs-length/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-attrs-length/ );
 
 				expect( () => {
 					vehicle = new Car();
 
 					vehicle.bind( 'color' ).to( car, 'color', 'year', () => {} );
-				} ).to.throw( CKEditorError, /model-bind-to-attrs-length/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-attrs-length/ );
 
 				expect( () => {
 					vehicle = new Car();
 
 					vehicle.bind( 'color' ).to( car, 'color', car, 'color', 'year', () => {} );
-				} ).to.throw( CKEditorError, /model-bind-to-attrs-length/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-attrs-length/ );
 			} );
 
-			it( 'should throw when attributes don\'t exist in to() model', () => {
+			it( 'should throw when attributes don\'t exist in to() observable', () => {
 				const vehicle = new Car();
 
 				expect( () => {
 					vehicle.bind( 'color' ).to( car, 'nonexistent in car' );
-				} ).to.throw( CKEditorError, /model-bind-to-missing-attr/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-missing-attr/ );
 
 				expect( () => {
 					vehicle.bind( 'nonexistent in car' ).to( car );
-				} ).to.throw( CKEditorError, /model-bind-to-missing-attr/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-missing-attr/ );
 
 				expect( () => {
 					vehicle.bind( 'year' ).to( car, 'color', car, 'nonexistent in car', () => {} );
-				} ).to.throw( CKEditorError, /model-bind-to-missing-attr/ );
+				} ).to.throw( CKEditorError, /observable-bind-to-missing-attr/ );
 			} );
 
-			it( 'should set new model attributes', () => {
+			it( 'should set new observable attributes', () => {
 				const car = new Car( { color: 'green', year: 2001, type: 'pickup' } );
 				const vehicle = new Car( { 'not involved': true } );
 
 				vehicle.bind( 'color', 'year', 'type' ).to( car );
 
-				expect( vehicle._attributes ).to.have.keys( 'color', 'year', 'type', 'not involved' );
+				expect( vehicle ).to.have.property( 'color' );
+				expect( vehicle ).to.have.property( 'year' );
+				expect( vehicle ).to.have.property( 'type' );
+				expect( vehicle ).to.have.property( 'not involved' );
 			} );
 
 			it( 'should work when no attribute specified #1', () => {
@@ -410,7 +394,7 @@ describe( 'Model', () => {
 				);
 			} );
 
-			it( 'should work for attributes that don\'t exist in the model', () => {
+			it( 'should work for attributes that don\'t exist in the observable', () => {
 				const vehicle = new Car();
 
 				vehicle.bind( 'nonexistent in vehicle' ).to( car, 'color' );
@@ -453,7 +437,7 @@ describe( 'Model', () => {
 				);
 			} );
 
-			it( 'should work with callback – set a new model attribute', () => {
+			it( 'should work with callback – set a new observable attribute', () => {
 				const vehicle = new Car();
 				const car1 = new Car( { type: 'pickup' } );
 				const car2 = new Car( { type: 'truck' } );
@@ -461,7 +445,7 @@ describe( 'Model', () => {
 				vehicle.bind( 'type' )
 					.to( car1, car2, ( ...args ) => args.join( '' ) );
 
-				expect( vehicle._attributes ).to.have.keys( [ 'type' ] );
+				expect( vehicle ).to.have.property( 'type' );
 			} );
 
 			it( 'should work with callback #1', () => {
@@ -719,10 +703,16 @@ describe( 'Model', () => {
 	} );
 
 	describe( 'unbind', () => {
+		it( 'should not fail when unbinding a fresh observable', () => {
+			const observable = new Observable();
+
+			observable.unbind();
+		} );
+
 		it( 'should throw when non-string attribute is passed', () => {
 			expect( () => {
 				car.unbind( new Date() );
-			} ).to.throw( CKEditorError, /model-unbind-wrong-attrs/ );
+			} ).to.throw( CKEditorError, /observable-unbind-wrong-attrs/ );
 		} );
 
 		it( 'should remove all bindings', () => {
@@ -774,57 +764,25 @@ describe( 'Model', () => {
 			);
 		} );
 
-		it( 'should process the internal structure and listeners correctly', () => {
-			const model = new Model();
-
-			const bound1 = new Model( { b1a: 'foo' } );
-			const bound2 = new Model( { b2b: 42, 'b2c': 'bar' } );
-			const bound3 = new Model( { b3d: 'baz' } );
-
-			model.bind( 'a' ).to( bound1, 'b1a' );
-			model.bind( 'b', 'c' ).to( bound2, 'b2b', 'b2c' );
-			model.bind( 'd', 'e' ).to( bound3, 'b3d', 'b3d' );
-
-			assertStructure( model,
-				[ 'a', 'b', 'c', 'd', 'e' ],
-				[ bound1, bound2, bound3 ],
-				[
-					{ b1a: [ 'a' ] },
-					{ b2b: [ 'b' ], b2c: [ 'c' ] },
-					{ b3d: [ 'd', 'e' ] }
-				]
-			);
-
-			model.unbind( 'c', 'd' );
-
-			assertStructure( model,
-				[ 'a', 'b', 'e' ],
-				[ bound1, bound2, bound3 ],
-				[
-					{ b1a: [ 'a' ] },
-					{ b2b: [ 'b' ] },
-					{ b3d: [ 'e' ] }
-				]
-			);
+		it( 'should be able to unbind two attributes from a single source observable attribute', () => {
+			const vehicle = new Car();
 
-			model.unbind( 'b' );
+			vehicle.bind( 'color' ).to( car, 'color' );
+			vehicle.bind( 'interiorColor' ).to( car, 'color' );
+			vehicle.unbind( 'color' );
+			vehicle.unbind( 'interiorColor' );
 
-			assertStructure( model,
-				[ 'a', 'e' ],
-				[ bound1, bound3 ],
+			assertBinding( vehicle,
+				{ color: 'red', interiorColor: 'red' },
 				[
-					{ b1a: [ 'a' ] },
-					{ b3d: [ 'e' ] }
-				]
+					[ car, { color: 'blue' } ]
+				],
+				{ color: 'red', interiorColor: 'red' }
 			);
-
-			model.unbind();
-
-			assertStructure( model, [], [], [] );
 		} );
 	} );
 
-	// Syntax given that model `A` is bound to models [`B`, `C`, ...]:
+	// Syntax given that observable `A` is bound to observables [`B`, `C`, ...]:
 	//
 	//		assertBinding( A,
 	//			{ initial `A` attributes },
@@ -836,14 +794,14 @@ describe( 'Model', () => {
 	//			{ `A` attributes after [`B`, 'C', ...] changed }
 	//		);
 	//
-	function assertBinding( model, stateBefore, data, stateAfter ) {
+	function assertBinding( observable, stateBefore, data, stateAfter ) {
 		let key, pair;
 
 		for ( key in stateBefore ) {
-			expect( model[ key ] ).to.be.equal( stateBefore[ key ] );
+			expect( observable[ key ] ).to.be.equal( stateBefore[ key ] );
 		}
 
-		// Change attributes of bound models.
+		// Change attributes of bound observables.
 		for ( pair of data ) {
 			for ( key in pair[ 1 ] ) {
 				pair[ 0 ][ key ] = pair[ 1 ][ key ];
@@ -851,39 +809,7 @@ describe( 'Model', () => {
 		}
 
 		for ( key in stateAfter ) {
-			expect( model[ key ] ).to.be.equal( stateAfter[ key ] );
+			expect( observable[ key ] ).to.be.equal( stateAfter[ key ] );
 		}
 	}
-
-	function assertStructure( model, expectedBoundAttributes, expectedBoundModels, expectedBindings ) {
-		const boundModels = [ ...model._boundModels.keys() ];
-
-		// Check model._boundAttributes object.
-		if ( expectedBoundAttributes.length ) {
-			expect( model._boundAttributes ).to.have.keys( expectedBoundAttributes );
-		} else {
-			expect( model._boundAttributes ).to.be.empty;
-		}
-
-		// Check model._boundModels models.
-		expect( boundModels ).to.have.members( expectedBoundModels );
-
-		// Check model._listeningTo models.
-		boundModels.map( boundModel => {
-			expect( model._listeningTo ).to.have.ownProperty( boundModel._emitterId );
-		} );
-
-		// Check model._boundModels bindings.
-		expectedBindings.forEach( ( binding, index ) => {
-			const bindingKeys = Object.keys( binding );
-
-			expect( model._boundModels.get( expectedBoundModels[ index ] ) ).to.have.keys( bindingKeys );
-
-			bindingKeys.forEach( key => {
-				const entries = [ ...model._boundModels.get( expectedBoundModels[ index ] )[ key ] ];
-
-				expect( entries.map( e => e.attr ) ).to.have.members( binding[ key ] );
-			} );
-		} );
-	}
 } );

+ 1 - 1
packages/ckeditor5-ui/tests/ui/controller.js

@@ -12,7 +12,7 @@ import View from '/ckeditor5/core/ui/view.js';
 import Controller from '/ckeditor5/core/ui/controller.js';
 import ControllerCollection from '/ckeditor5/core/ui/controllercollection.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
-import Model from '/ckeditor5/core/model.js';
+import Model from '/ckeditor5/core/ui/model.js';
 import EventInfo from '/ckeditor5/core/eventinfo.js';
 
 let ParentController, ParentView;

+ 41 - 0
packages/ckeditor5-ui/tests/ui/model.js

@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Model from '/ckeditor5/core/ui/model.js';
+
+let Car, car;
+
+describe( 'Model', () => {
+	beforeEach( () => {
+		Car = class extends Model {};
+
+		car = new Car( {
+			color: 'red',
+			year: 2015
+		} );
+	} );
+
+	it( 'should set attributes on creation', () => {
+		expect( car ).to.have.property( 'color', 'red' );
+		expect( car ).to.have.property( 'year', 2015 );
+
+		const spy = sinon.spy();
+
+		car.on( 'change:color', spy );
+		car.color = 'blue';
+
+		expect( spy.called ).to.be.true;
+	} );
+
+	it( 'should add properties on creation', () => {
+		let car = new Car( null, {
+			prop: 1
+		} );
+
+		expect( car ).to.have.property( 'prop', 1 );
+	} );
+} );

+ 1 - 1
packages/ckeditor5-ui/tests/ui/view.js

@@ -12,7 +12,7 @@ import testUtils from '/tests/_utils/utils.js';
 import View from '/ckeditor5/core/ui/view.js';
 import Region from '/ckeditor5/core/ui/region.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
-import Model from '/ckeditor5/core/model.js';
+import Model from '/ckeditor5/core/ui/model.js';
 
 let TestView, view;