Kaynağa Gözat

Implemented ObservableMixin#decorate().

Piotrek Koszuliński 8 yıl önce
ebeveyn
işleme
208a53d6e2

+ 70 - 0
packages/ckeditor5-utils/src/observablemixin.js

@@ -247,6 +247,76 @@ const ObservableMixin = {
 			boundObservables.clear();
 			boundAttributes.clear();
 		}
+	},
+
+	/**
+	 * Turns the given methods of this object into event-based ones. This means that the new method will fire an event
+	 * (named after the method) and the original action will be plugged as a listener to that event.
+	 *
+	 * This is a very simplified method decoration. Itself it doesn't change the behavior of a method (expect adding the event),
+	 * but it allows to modify it later on by listening to the method's event.
+	 *
+	 * For example, in order to cancel the method execution one can stop the event:
+	 *
+	 *		class Foo {
+	 *			constructor() {
+	 *				this.decorate( 'method' );
+	 *			}
+	 *
+	 *			method() {
+	 *				console.log( 'called!' );
+	 *			}
+	 *		}
+	 *
+	 *		const foo = new Foo();
+	 *		foo.on( 'method', ( evt ) => {
+	 *			evt.stop();
+	 *		}, { priority: 'high' } );
+	 *
+	 *		foo.method(); // Nothing is logged.
+	 *
+	 *
+	 * 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).
+	 *
+	 * It's also possible to change the return value:
+	 *
+	 *		foo.on( 'method', ( evt ) => {
+	 *			evt.return = 'Foo!';
+	 *		} );
+	 *
+	 *		foo.method(); // -> 'Foo'
+	 *
+	 * Finally, it's possible to access and modify the parameters:
+	 *
+	 *		method( a, b ) {
+	 *			console.log( `${ a }, ${ b }`  );
+	 *		}
+	 *
+	 *		// ...
+	 *
+	 *		foo.on( 'method', ( evt, args ) => {
+	 *			args[ 0 ] = 3;
+	 *
+	 *			console.log( args[ 1 ] ); // -> 2
+	 *		}, { priority: 'high' } );
+	 *
+	 *		foo.method( 1, 2 ); // -> '3, 2'
+	 *
+	 * @param {String} ...methodNames Names of the methods to decorate.
+	 */
+	decorate( ...methodNames ) {
+		methodNames.forEach( methodName => {
+			const originalMethod = this[ methodName ];
+
+			this.on( methodName, ( evt, args ) => {
+				evt.return = originalMethod.apply( this, args );
+			} );
+
+			this[ methodName ] = function( ...args ) {
+				return this.fire( methodName, args );
+			};
+		} );
 	}
 
 	/**

+ 102 - 4
packages/ckeditor5-utils/tests/observablemixin.js

@@ -61,7 +61,7 @@ describe( 'Observable', () => {
 		expect( car.color ).to.equal( 'blue' );
 	} );
 
-	describe( 'set', () => {
+	describe( 'set()', () => {
 		it( 'should work when passing an object', () => {
 			car.set( {
 				color: 'blue',	// Override
@@ -185,7 +185,7 @@ describe( 'Observable', () => {
 		} );
 	} );
 
-	describe( 'bind', () => {
+	describe( 'bind()', () => {
 		it( 'should chain for a single attribute', () => {
 			expect( car.bind( 'color' ) ).to.contain.keys( 'to' );
 		} );
@@ -225,7 +225,7 @@ describe( 'Observable', () => {
 			} ).to.throw( CKEditorError, /observable-bind-rebind/ );
 		} );
 
-		describe( 'to', () => {
+		describe( 'to()', () => {
 			it( 'should not chain', () => {
 				expect(
 					car.bind( 'color' ).to( new Observable( { color: 'red' } ) )
@@ -732,7 +732,7 @@ describe( 'Observable', () => {
 		} );
 	} );
 
-	describe( 'unbind', () => {
+	describe( 'unbind()', () => {
 		it( 'should not fail when unbinding a fresh observable', () => {
 			const observable = new Observable();
 
@@ -811,4 +811,102 @@ describe( 'Observable', () => {
 			);
 		} );
 	} );
+
+	describe( 'decorate()', () => {
+		it( 'makes the method fire an event', () => {
+			const spy = sinon.spy();
+
+			class Foo extends Observable {
+				method() {}
+			}
+
+			const foo = new Foo();
+
+			foo.decorate( 'method' );
+
+			foo.on( 'method', spy );
+
+			foo.method( 1, 2 );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 1 ] ).to.deep.equal( [ 1, 2 ] );
+		} );
+
+		it( 'executes the original method in a listener with the default priority', () => {
+			const calls = [];
+
+			class Foo extends Observable {
+				method() {
+					calls.push( 'original' );
+				}
+			}
+
+			const foo = new Foo();
+
+			foo.decorate( 'method' );
+
+			foo.on( 'method', () => calls.push( 'high' ), { priority: 'high' } );
+			foo.on( 'method', () => calls.push( 'low' ), { priority: 'low' } );
+
+			foo.method();
+
+			expect( calls ).to.deep.equal( [ 'high', 'original', 'low' ] );
+		} );
+
+		it( 'supports overriding return values', () => {
+			class Foo extends Observable {
+				method() {
+					return 1;
+				}
+			}
+
+			const foo = new Foo();
+
+			foo.decorate( 'method' );
+
+			foo.on( 'method', evt => {
+				expect( evt.return ).to.equal( 1 );
+
+				evt.return = 2;
+			} );
+
+			expect( foo.method() ).to.equal( 2 );
+		} );
+
+		it( 'supports overriding arguments', () => {
+			class Foo extends Observable {
+				method( a ) {
+					expect( a ).to.equal( 2 );
+				}
+			}
+
+			const foo = new Foo();
+
+			foo.decorate( 'method' );
+
+			foo.on( 'method', ( evt, args ) => {
+				args[ 0 ] = 2;
+			}, { priority: 'high' } );
+
+			foo.method( 1 );
+		} );
+
+		it( 'supports stopping the event (which prevents execution of the orignal method', () => {
+			class Foo extends Observable {
+				method() {
+					throw new Error( 'this should not be executed' );
+				}
+			}
+
+			const foo = new Foo();
+
+			foo.decorate( 'method' );
+
+			foo.on( 'method', evt => {
+				evt.stop();
+			}, { priority: 'high' } );
+
+			foo.method();
+		} );
+	} );
 } );