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

Commit

Permalink
Feature: Added beforeChange event to ObservableMixin.
Browse files Browse the repository at this point in the history
  • Loading branch information
oskarwrobel committed May 25, 2018
1 parent 81fefc9 commit 8d1feb7
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 3 deletions.
46 changes: 43 additions & 3 deletions src/observablemixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,17 @@ const ObservableMixin = {
// Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
// Note: When properties map has no such own property, then its value is undefined.
if ( oldValue !== value || !properties.has( name ) ) {
properties.set( name, value );
this.fire( 'change:' + name, name, value, oldValue );
// Fire `beforeChange` event before the new value will be changed to make it possible
// to override observable property without affecting `change` event.
// See https://github.com/ckeditor/ckeditor5-utils/issues/171.
let newValue = this.fire( 'beforeChange:' + name, name, value, oldValue );

if ( newValue === undefined ) {
newValue = value;
}

properties.set( name, newValue );
this.fire( 'change:' + name, name, newValue, oldValue );
}
}
} );
Expand Down Expand Up @@ -664,7 +673,7 @@ function attachBindToListeners( observable, toBindings ) {
*
* observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
* console.log( `${ propertyName } has changed from ${ oldValue } to ${ newValue }` );
* } )
* } );
*
* observable.prop = 2; // -> 'prop has changed from 1 to 2'
*
Expand All @@ -674,6 +683,37 @@ function attachBindToListeners( observable, toBindings ) {
* @param {*} oldValue The previous property value.
*/

/**
* Fired when a property value is going to be changed but is not changed yet
* (before the {@link module:utils/observablemixin~ObservableMixin#change} event is fired).
*
* When some value will be set as {@link module:utils/eventinfo~EventInfo#return event return value} then
* this value become an observalbe new value.
*
* observable.set( 'prop', 1 );
*
* observable.on( 'beforeChange:prop', ( evt, propertyName, newValue, oldValue ) => {
* console.log( `Value is going to be changed from ${ oldValue } to ${ newValue }` );
* console.log( `Current property value is ${ observable[ propertyName ] }` );
*
* // Let's override the value.
* evt.return = 3;
* } );
*
* observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
* console.log( `Value has changed from ${ oldValue } to ${ newValue }` );
* } );
*
* observable.prop = 2; // -> 'Value is going to be changed from 1 to 2'
* // -> 'Current property value is 1'
* // -> 'Value has changed from 1 to 3'
*
* @event beforeChange:{property}
* @param {String} name The property name.
* @param {*} value The new property value.
* @param {*} oldValue The previous property value.
*/

/**
* Creates and sets the value of an observable property of this object. Such an property becomes a part
* of the state and is be observable.
Expand Down
73 changes: 73 additions & 0 deletions tests/observablemixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,79 @@ describe( 'Observable', () => {
sinon.assert.notCalled( spyColor );
} );

it( 'should fire the "beforeChange" event', () => {
const spy = sinon.spy();
const spyColor = sinon.spy();
const spyYear = sinon.spy();
const spyWheels = sinon.spy();

car.on( 'beforeChange', spy );
car.on( 'beforeChange:color', spyColor );
car.on( 'beforeChange:year', spyYear );
car.on( 'beforeChange:wheels', spyWheels );

// Set property in all possible ways.
car.color = 'blue';
car.set( { year: 2003 } );
car.set( 'wheels', 4 );

// Check number of calls.
sinon.assert.calledThrice( spy );
sinon.assert.calledOnce( spyColor );
sinon.assert.calledOnce( spyYear );
sinon.assert.calledOnce( spyWheels );

// Check context.
sinon.assert.alwaysCalledOn( spy, car );
sinon.assert.calledOn( spyColor, car );
sinon.assert.calledOn( spyYear, car );
sinon.assert.calledOn( spyWheels, car );

// Check params.
sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'color', 'blue', 'red' );
sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'year', 2003, 2015 );
sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'wheels', 4, sinon.match.typeOf( 'undefined' ) );
sinon.assert.calledWithExactly( spyColor, sinon.match.instanceOf( EventInfo ), 'color', 'blue', 'red' );
sinon.assert.calledWithExactly( spyYear, sinon.match.instanceOf( EventInfo ), 'year', 2003, 2015 );
sinon.assert.calledWithExactly(
spyWheels, sinon.match.instanceOf( EventInfo ),
'wheels', 4, sinon.match.typeOf( 'undefined' )
);
} );

it( 'should use "beforeChange" return value as an observable new value', () => {
car.color = 'blue';

const spy = sinon.spy();

car.on( 'beforeChange:color', evt => {
evt.stop();
evt.return = 'red';
}, { priority: 'high' } );

car.on( 'change:color', spy );

car.color = 'pink';

sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'color', 'red', 'blue' );
} );

it( 'should not fire the "beforeChange" event for the same property value', () => {
const spy = sinon.spy();
const spyColor = sinon.spy();

car.on( 'beforeChange', spy );
car.on( 'beforeChange:color', spyColor );

// Set the "color" property in all possible ways.
car.color = 'red';
car.set( 'color', 'red' );
car.set( { color: 'red' } );

sinon.assert.notCalled( spy );
sinon.assert.notCalled( spyColor );
} );

it( 'should throw when overriding already existing property', () => {
car.normalProperty = 1;

Expand Down

0 comments on commit 8d1feb7

Please sign in to comment.