Kaynağa Gözat

Merge pull request #225 from ckeditor/t/ckeditor5-ui/344

Feature: Introduce `bind().toMany()` binding chain in `ObservableMixin`. Closes #224.
Piotrek Koszuliński 8 yıl önce
ebeveyn
işleme
b356de2be5

+ 45 - 2
packages/ckeditor5-utils/src/observablemixin.js

@@ -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 }`).
@@ -144,6 +145,7 @@ const ObservableMixin = {
 		// initiated in this binding chain.
 		return {
 			to: bindTo,
+			toMany: bindToMany,
 
 			_observable: this,
 			_bindProperties: bindProperties,
@@ -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
@@ -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.
  */
 
 /**
@@ -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:
  *

+ 57 - 0
packages/ckeditor5-utils/tests/observablemixin.js

@@ -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()', () => {