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

Commit 6ef1246

Browse files
authored
Merge pull request #241 from ckeditor/t/171
Feature: Introduced `beforeChange:{property}` event in `ObservableMixin`. Closes #171.
2 parents 3b158fa + 296a57b commit 6ef1246

File tree

2 files changed

+115
-3
lines changed

2 files changed

+115
-3
lines changed

src/observablemixin.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,17 @@ const ObservableMixin = {
7575
// Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
7676
// Note: When properties map has no such own property, then its value is undefined.
7777
if ( oldValue !== value || !properties.has( name ) ) {
78-
properties.set( name, value );
79-
this.fire( 'change:' + name, name, value, oldValue );
78+
// Fire `beforeChange` event before the new value will be changed to make it possible
79+
// to override observable property without affecting `change` event.
80+
// See https://github.com/ckeditor/ckeditor5-utils/issues/171.
81+
let newValue = this.fire( 'beforeChange:' + name, name, value, oldValue );
82+
83+
if ( newValue === undefined ) {
84+
newValue = value;
85+
}
86+
87+
properties.set( name, newValue );
88+
this.fire( 'change:' + name, name, newValue, oldValue );
8089
}
8190
}
8291
} );
@@ -664,7 +673,7 @@ function attachBindToListeners( observable, toBindings ) {
664673
*
665674
* observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
666675
* console.log( `${ propertyName } has changed from ${ oldValue } to ${ newValue }` );
667-
* } )
676+
* } );
668677
*
669678
* observable.prop = 2; // -> 'prop has changed from 1 to 2'
670679
*
@@ -674,6 +683,36 @@ function attachBindToListeners( observable, toBindings ) {
674683
* @param {*} oldValue The previous property value.
675684
*/
676685

686+
/**
687+
* Fired when a property value is going to be changed but is not changed yet (before the `change` event is fired).
688+
*
689+
* You can control the final value of the property by using
690+
* the {@link module:utils/eventinfo~EventInfo#return event's `return` property}.
691+
*
692+
* observable.set( 'prop', 1 );
693+
*
694+
* observable.on( 'beforeChange:prop', ( evt, propertyName, newValue, oldValue ) => {
695+
* console.log( `Value is going to be changed from ${ oldValue } to ${ newValue }` );
696+
* console.log( `Current property value is ${ observable[ propertyName ] }` );
697+
*
698+
* // Let's override the value.
699+
* evt.return = 3;
700+
* } );
701+
*
702+
* observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
703+
* console.log( `Value has changed from ${ oldValue } to ${ newValue }` );
704+
* } );
705+
*
706+
* observable.prop = 2; // -> 'Value is going to be changed from 1 to 2'
707+
* // -> 'Current property value is 1'
708+
* // -> 'Value has changed from 1 to 3'
709+
*
710+
* @event beforeChange:{property}
711+
* @param {String} name The property name.
712+
* @param {*} value The new property value.
713+
* @param {*} oldValue The previous property value.
714+
*/
715+
677716
/**
678717
* Creates and sets the value of an observable property of this object. Such an property becomes a part
679718
* of the state and is be observable.

tests/observablemixin.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,79 @@ describe( 'Observable', () => {
144144
sinon.assert.notCalled( spyColor );
145145
} );
146146

147+
it( 'should fire the "beforeChange" event', () => {
148+
const spy = sinon.spy();
149+
const spyColor = sinon.spy();
150+
const spyYear = sinon.spy();
151+
const spyWheels = sinon.spy();
152+
153+
car.on( 'beforeChange', spy );
154+
car.on( 'beforeChange:color', spyColor );
155+
car.on( 'beforeChange:year', spyYear );
156+
car.on( 'beforeChange:wheels', spyWheels );
157+
158+
// Set property in all possible ways.
159+
car.color = 'blue';
160+
car.set( { year: 2003 } );
161+
car.set( 'wheels', 4 );
162+
163+
// Check number of calls.
164+
sinon.assert.calledThrice( spy );
165+
sinon.assert.calledOnce( spyColor );
166+
sinon.assert.calledOnce( spyYear );
167+
sinon.assert.calledOnce( spyWheels );
168+
169+
// Check context.
170+
sinon.assert.alwaysCalledOn( spy, car );
171+
sinon.assert.calledOn( spyColor, car );
172+
sinon.assert.calledOn( spyYear, car );
173+
sinon.assert.calledOn( spyWheels, car );
174+
175+
// Check params.
176+
sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'color', 'blue', 'red' );
177+
sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'year', 2003, 2015 );
178+
sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'wheels', 4, sinon.match.typeOf( 'undefined' ) );
179+
sinon.assert.calledWithExactly( spyColor, sinon.match.instanceOf( EventInfo ), 'color', 'blue', 'red' );
180+
sinon.assert.calledWithExactly( spyYear, sinon.match.instanceOf( EventInfo ), 'year', 2003, 2015 );
181+
sinon.assert.calledWithExactly(
182+
spyWheels, sinon.match.instanceOf( EventInfo ),
183+
'wheels', 4, sinon.match.typeOf( 'undefined' )
184+
);
185+
} );
186+
187+
it( 'should use "beforeChange" return value as an observable new value', () => {
188+
car.color = 'blue';
189+
190+
const spy = sinon.spy();
191+
192+
car.on( 'beforeChange:color', evt => {
193+
evt.stop();
194+
evt.return = 'red';
195+
}, { priority: 'high' } );
196+
197+
car.on( 'change:color', spy );
198+
199+
car.color = 'pink';
200+
201+
sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 'color', 'red', 'blue' );
202+
} );
203+
204+
it( 'should not fire the "beforeChange" event for the same property value', () => {
205+
const spy = sinon.spy();
206+
const spyColor = sinon.spy();
207+
208+
car.on( 'beforeChange', spy );
209+
car.on( 'beforeChange:color', spyColor );
210+
211+
// Set the "color" property in all possible ways.
212+
car.color = 'red';
213+
car.set( 'color', 'red' );
214+
car.set( { color: 'red' } );
215+
216+
sinon.assert.notCalled( spy );
217+
sinon.assert.notCalled( spyColor );
218+
} );
219+
147220
it( 'should throw when overriding already existing property', () => {
148221
car.normalProperty = 1;
149222

0 commit comments

Comments
 (0)