Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #225 from ckeditor/t/ckeditor5-ui/344
Browse files Browse the repository at this point in the history
Feature: Introduce `bind().toMany()` binding chain in `ObservableMixin`. Closes #224.
  • Loading branch information
Reinmar authored Feb 8, 2018
2 parents dc6b226 + b755087 commit cfa7d0e
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 2 deletions.
47 changes: 45 additions & 2 deletions src/observablemixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ const ObservableMixin = {

// @typedef {Object} BindChain
// @property {Function} to See {@link ~ObservableMixin#_bindTo}.
// @property {Function} toMany See {@link ~ObservableMixin#_bindToMany}.
// @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
// @property {Array} _bindProperties Array of `_observable` properties to be bound.
// @property {Array} _to Array of `to()` observable–properties (`{ observable: toObservable, properties: ...toProperties }`).
Expand All @@ -144,6 +145,7 @@ const ObservableMixin = {
// initiated in this binding chain.
return {
to: bindTo,
toMany: bindToMany,

_observable: this,
_bindProperties: bindProperties,
Expand Down Expand Up @@ -414,6 +416,40 @@ function bindTo( ...args ) {
} );
}

// Binds to an attribute in a set of iterable observables.
//
// @private
// @param {Iterable.<Observable>} observables
// @param {String} attribute
// @param {Function} callback
function bindToMany( observables, attribute, callback ) {
if ( this._bindings.size > 1 ) {
/**
* Binding one attribute to many observables only possible with one attribute.
*
* @error observable-bind-to-many-not-one-binding
*/
throw new CKEditorError( 'observable-bind-to-many-not-one-binding: Cannot bind multiple properties with toMany().' );
}

this.to(
// Bind to #attribute of each observable...
...getBindingTargets( observables, attribute ),
// ...using given callback to parse attribute values.
callback
);
}

// Returns an array of binding components for
// {@link Observable#bind} from a set of iterable observables.
//
// @param {Iterable.<Observable>} observables
// @param {String} attribute
// @returns {Array.<String>}
function getBindingTargets( observables, attribute ) {
return Array.prototype.concat( ...observables.map( observable => [ observable, attribute ] ) );
}

// Check if all entries of the array are of `String` type.
//
// @private
Expand Down Expand Up @@ -660,14 +696,21 @@ function attachBindToListeners( observable, toBindings ) {
*
* **Note**: To release the binding use {@link module:utils/observablemixin~Observable#unbind}.
*
* Using `bind().to()` chain:
*
* 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 );
*
* It is also possible to bind to the same property in a observables collection using `bind().toMany()` chain:
*
* A.bind( 'a' ).toMany( [ B, C, D ], 'x', ( a, b, c ) => a + b + c );
* A.bind( 'a' ).toMany( [ B, C, D ], 'x', ( ...x ) => x.every( x => x ) );
*
* @method #bind
* @param {...String} bindProperties Observable properties that will be bound to another observable(s).
* @returns {Object} The bind chain with the `to()` method.
* @returns {Object} The bind chain with the `to()` and `toMany()` methods.
*/

/**
Expand Down Expand Up @@ -709,7 +752,7 @@ function attachBindToListeners( observable, toBindings ) {
*
*
* Note: we used a high priority listener here to execute this callback before the one which
* calls the orignal method (which used the default priority).
* calls the original method (which used the default priority).
*
* It's also possible to change the return value:
*
Expand Down
57 changes: 57 additions & 0 deletions tests/observablemixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,63 @@ describe( 'Observable', () => {
.to.have.members( [ 'color', 'year', 'color', 'year' ] );
} );
} );

describe( 'toMany()', () => {
let Wheel;

beforeEach( () => {
Wheel = class extends Observable {
};
} );

it( 'should not chain', () => {
expect(
car.bind( 'color' ).toMany( [ new Observable( { color: 'red' } ) ], 'color', () => {} )
).to.be.undefined;
} );

it( 'should throw when binding multiple properties', () => {
let vehicle = new Car();

expect( () => {
vehicle.bind( 'color', 'year' ).toMany( [ car ], 'foo', () => {} );
} ).to.throw( CKEditorError, /observable-bind-to-many-not-one-binding/ );

expect( () => {
vehicle = new Car();

vehicle.bind( 'color', 'year' ).to( car, car, () => {} );
} ).to.throw( CKEditorError, /observable-bind-to-extra-callback/ );
} );

it( 'binds observable property to collection property using callback', () => {
const wheels = [
new Wheel( { isTyrePressureOK: true } ),
new Wheel( { isTyrePressureOK: true } ),
new Wheel( { isTyrePressureOK: true } ),
new Wheel( { isTyrePressureOK: true } )
];

car.bind( 'showTyrePressureWarning' ).toMany( wheels, 'isTyrePressureOK', ( ...areEnabled ) => {
// Every tyre must have OK pressure.
return !areEnabled.every( isTyrePressureOK => isTyrePressureOK );
} );

expect( car.showTyrePressureWarning ).to.be.false;

wheels[ 0 ].isTyrePressureOK = false;

expect( car.showTyrePressureWarning ).to.be.true;

wheels[ 0 ].isTyrePressureOK = true;

expect( car.showTyrePressureWarning ).to.be.false;

wheels[ 1 ].isTyrePressureOK = false;

expect( car.showTyrePressureWarning ).to.be.true;
} );
} );
} );

describe( 'unbind()', () => {
Expand Down

0 comments on commit cfa7d0e

Please sign in to comment.