From 9680d5da87859132a16351d47367cadc14bebffd Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 2 Oct 2018 17:01:50 +0200 Subject: [PATCH 01/12] Docs: Improved documentation of the Observable#bind method. --- src/observablemixin.js | 89 +++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/src/observablemixin.js b/src/observablemixin.js index 41cf612..bb2fc26 100644 --- a/src/observablemixin.js +++ b/src/observablemixin.js @@ -730,26 +730,84 @@ function attachBindToListeners( observable, toBindings ) { */ /** - * Binds observable properties to another objects implementing {@link module:utils/observablemixin~Observable} - * interface (like {@link module:ui/model~Model}). - * - * Once bound, the observable will immediately share the current state of properties - * of the observable it is bound to and react to the changes to these properties - * in the future. + * Binds properties to other objects implementing {@link module:utils/observablemixin~Observable} + * interface (e.g. {@link module:ui/model~Model}). * * **Note**: To release the binding use {@link module:utils/observablemixin~Observable#unbind}. * - * Using `bind().to()` chain: + * # Simple bindings + * + * Let's consider two objects: a `button` and an associated `command` (both `Observable`). A simple property + * binding could look as follows: + * + * button.bind( 'isEnabled' ).to( command ); + * + * After that: + * + * * `button.isEnabled` equals `command.isEnabled`, + * * whenever `command.isEnabled` changes, `button.isEnabled` will immediately follow. + * + * Note that `command.isEnabled` **must** be defined using the {@link #set `set()`} method for the binding + * to be dynamic. `button.isEnabled` does not need to exist prior to the `bind()` call and in such case it + * will be created on demand. + * + * The last example corresponds to the following code: + * + * button.bind( 'isEnabled' ).to( command, 'isEnabled' ); + * + * You should notice the `to( ... )` interface which helps specify the name of the property ("rename" + * the property in the binding), for instance: + * + * button.bind( 'isEnabled' ).to( command, 'isCommandEnabled' ); + * + * In the above binding, whenever `command.isCommandEnabled` changes, the value of `button.isEnabled` + * will follow. + * + * # Binding multiple properties + * + * It is possible to bind more that one property at a time to simplify the code: + * + * button.bind( 'isEnabled', 'state' ).to( command ); * - * 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 ); + * which is the same as: * - * It is also possible to bind to the same property in a observables collection using `bind().toMany()` chain: + * button.bind( 'isEnabled' ).to( command ); + * button.bind( 'state' ).to( command ); * - * 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 ) ); + * In the above binding, the value of `button.isEnabled` will follow `command.isEnabled` and the value of + * `button.state` will follow `command.state`. + * + * Renaming is also possible when binding multiple properties. Consider the following example + * + * button.bind( 'isEnabled', 'state' ).to( command, 'isCommandEnabled', 'commandState' ); + * + * which binds `button.isEnabled` to `command.isCommandEnabled` and `button.state` to `command.commandState`. + * + * # Binding with multiple observables + * + * The binding can include more than one observable, combining multiple properties. Let's create a button + * that is enabled only when the `command` is enabled and the `editor` (also an `Observable`) is not read–only: + * + * button.bind( 'isEnabled' ).to( command, 'isEnabled', editor, 'isReadOnly', + * ( isCommandEnabled, isEditorReadOnly ) => isCommandEnabled && !isEditorReadOnly ); + * + * From now on the value of `button.isEnabled` depends both on `command.isEnabled` and `editor.isReadOnly` + * as specified by the function: the former must be `true` and the later must be `false` for the button + * to become enabled. + * + * # Binding with an array of observables + * + * It is possible to bind to the same property in an array of observables. Let's bind a `button` + * to multiple commands (also `Observables`) so that each one of them must be enabled for the button + * to become enabled: + * + * button.bind( 'isEnabled' ).toMany( [ commandA, commandB, commandC ], 'isEnabled', + * ( isAEnabled, isBEnabled, isCEnabled ) => isAEnabled && isBEnabled && isCEnabled ); + * + * The binding can be simplified using the spread operator (`...`) and the `Array.every()` method: + * + * button.bind( 'isEnabled' ).toMany( [ commandA, commandB, commandC ], 'isEnabled', + * ( ...areEnabled ) => areEnabled.every( isCommandEnabled => isCommandEnabled ) ); * * @method #bind * @param {...String} bindProperties Observable properties that will be bound to another observable(s). @@ -759,7 +817,10 @@ function attachBindToListeners( observable, toBindings ) { /** * Removes the binding created with {@link #bind}. * + * // Removes the binding for the 'a' property. * A.unbind( 'a' ); + * + * // Removes bindings for all properties. * A.unbind(); * * @method #unbind From fb45012f2e079bfa4b60a859c6765dffcc1cc88d Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 2 Oct 2018 17:05:46 +0200 Subject: [PATCH 02/12] Docs: Added missing comma in the Observable#bind docs. --- src/observablemixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observablemixin.js b/src/observablemixin.js index bb2fc26..52b7190 100644 --- a/src/observablemixin.js +++ b/src/observablemixin.js @@ -748,7 +748,7 @@ function attachBindToListeners( observable, toBindings ) { * * whenever `command.isEnabled` changes, `button.isEnabled` will immediately follow. * * Note that `command.isEnabled` **must** be defined using the {@link #set `set()`} method for the binding - * to be dynamic. `button.isEnabled` does not need to exist prior to the `bind()` call and in such case it + * to be dynamic. `button.isEnabled` does not need to exist prior to the `bind()` call and in such case, it * will be created on demand. * * The last example corresponds to the following code: From 5b868a7ededd4fdf1f71a5a08299f92f05d1ceae Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 5 Oct 2018 17:41:27 +0200 Subject: [PATCH 03/12] Docs: The first version of the Observables deep dive guide. --- .../framework/guides/deep-dive/observables.md | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 docs/framework/guides/deep-dive/observables.md diff --git a/docs/framework/guides/deep-dive/observables.md b/docs/framework/guides/deep-dive/observables.md new file mode 100644 index 0000000..acd3646 --- /dev/null +++ b/docs/framework/guides/deep-dive/observables.md @@ -0,0 +1,185 @@ +--- +category: framework-deep-dive +--- + +# Observables + +{@link module:utils/observablemixin~Observable Observables} are objects which have properties that can be observed. That means when the value of such property changes, an event is fired by the observable and the change can be reflected in other pieces of the code that listen to that event. + +Any class can become observable; all you need to do is mix the {@link module:utils/observablemixin~ObservableMixin} into it: + +```js +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; + +export default class AnyClass { + // ... +} + +mix( AnyClass, ObservableMixin ); +``` + +Observables are useful when it comes to managing the state of the application, which can be dynamic and, more often than not, centralized and shared between components of the application. One observable can also propagate its state (or its part) to another using [property bindings](#property-bindings). + + +## Making properties observable + +Having mixed the {@link module:utils/observablemixin~ObservableMixin} into your class, you can define observable properties. To do that, use the {@link module:utils/observablemixin~ObservableMixin#set `set()` method}. Let's set a couple of properties and see what they look like in a simple `Command` class: + +```js +export default class Command { + constructor( name ) { + // This property is not observable. + // Not all properties must be observable, it's up to you! + this.name = name; + + // this.value is observable but undefined. + this.set( 'value' ); + + // this.isEnabled is observable and false. + this.set( 'isEnabled', false ); + } +} + +mix( Command, ObservableMixin ); +``` + + + The `set()` method can accept an object of key/value pairs to shorten the code. Knowing that, making properties observable can be as simple as: + + ```js + this.set( { + value: undefined, + isEnabled: false + } ); + ``` + + +Finally, let's create a new command and see how it communicates with the world. + +Each time the `value` property changes, the command fires the `change:value` event containing information about its state in the past and the new value. The corresponding `change:isEnabled` will be fired when the `isEnabled` property changes too. + +```js +const command = new Command( 'bold' ); + +command.on( 'change:value', ( evt, propertyName, newValue, oldValue ) => { + console.log( + `${ propertyName } has changed from ${ oldValue } to ${ newValue }` + ); +} ) + +command.value = true; // -> 'value has changed from undefined to true' +command.value = false; // -> 'value has changed from true to false' + +command.name = 'italic'; // -> changing a regular property fires no event +``` + +During its life cycle, an instance of the `Command` can be enabled and disabled many times just as its `value` can change very often and different parts of the application will certainly be interested in that state. + +For instance, some commands can be represented by a button, which should be able to figure out its look ("pushed", disabled, etc.) as soon as possible. Using observable properties makes it a lot easier because all the button must know about its command is the names of properties to listen to apply changes instantly. + +Additionally, as the number of observable properties increases, you can save yourself the hassle of creating and maintaining multiple `command.on( 'change:property', () => { ... } )` listeners by sharing command's state with the button using [bound properties](#property-bindings), which are the key topic of the next chapter. + +## Property bindings + +One observable can also propagate its state (or part of it) to another observable to simplify the code and avoid numerous `change:property` event listeners. First, make sure both objects (classes) mix the {@link module:utils/observablemixin~ObservableMixin}. + +### Simple bindings + +Let's consider two objects: a `command` and a corresponding `button` (both {@link module:utils/observablemixin~Observable}). + +```js +const command = new Command( 'bold' ); +const command = new Button(); +``` + +Any decent button must update its look when the command becomes disabled. A simple property binding doing that could look as follows: + +```js +button.bind( 'isEnabled' ).to( command ); +``` + +After that: + +* `button.isEnabled` **instantly equals** `command.isEnabled`, +* whenever `command.isEnabled` changes, `button.isEnabled` will immediately reflect its value. + +Note that `command.isEnabled` **must** be defined using the `set()` method for the binding to be dynamic – we did that in the [previous chapter](#making-properties-observable). The `button.isEnabled` property does not need to exist prior to the `bind()` call and in such case, it will be created on demand. If the `button.isEnabled` property is already observable, don't worry: binding it to the command will do no harm. + +By creating the binding, we allowed the button to simply use its own `isEnabled` property, e.g. in the dynamic template (check out {@link framework/guides/architecture/ui-library#template this guide} to learn how). + +#### Renaming properties + +Now let's dive into the `bind( ... ).to( ... )` syntax for a minute. The last example corresponds to the following code: + +```js +button.bind( 'isEnabled' ).to( command, 'isEnabled' ); +``` + +You probably noticed the `to( ... )` interface which helps specify the name of the property ("rename" the property in the binding). + +What if instead of `isEnabled`, the `Command` class implemented the `isWorking` property, which does not quite fit into the button object? Let's bind two properties that have different names then: + +```js +button.bind( 'isEnabled' ).to( command, 'isWorking' ); +``` + +From now on, whenever `command.isWorking` changes, the value of `button.isEnabled` will reflect it. + +### Binding multiple properties + +It is also possible to bind more that one property at a time to simplify the code: + +```js +button.bind( 'isEnabled', 'value' ).to( command ); +``` + +which is the same as + +```js +button.bind( 'isEnabled' ).to( command ); +button.bind( 'value' ).to( command ); +``` + +In the above binding, the value of `button.isEnabled` will reflect `command.isEnabled` and the value of `button.value` will reflect `command.value`. + +Renaming is still possible when binding multiple properties. Consider the following example which binds `button.isEnabled` to `command.isWorking` and `button.currentState` to `command.value`: + +```js +button.bind( 'isEnabled', 'currentState' ).to( command, 'isWorking', 'value' ); +``` + +### Binding with multiple observables + +The binding can include more than one observable, combining multiple properties. Let's create a button that gets enabled only when the `command` is enabled and the `ui` (also an `Observable`) is visible: + +```js +button.bind( 'isEnabled' ).to( command, 'isEnabled', ui, 'isVisible', + ( isCommandEnabled, isUIVisible ) => isCommandEnabled && isUIVisible ); +``` + +From now on, the value of `button.isEnabled` depends both on `command.isEnabled` and `ui.isVisible` +as specified by the function: both must be `true` for the button to become enabled. + +### Binding with an array of observables + +It is possible to bind to the same property in an array of observables. Let's bind a `button` to multiple commands so that each and every one must be enabled for the button +to become enabled: + +```js +const commands = [ commandA, commandB, commandC ]; + +button.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( isAEnabled, isBEnabled, isCEnabled ) => { + return isAEnabled && isBEnabled && isCEnabled; +} ); +``` + +The binding can be simplified using the spread operator (`...`) and the `Array.every()` method: + +```js +const commands = [ commandA, commandB, commandC ]; + +button.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => { + return areEnabled.every( isCommandEnabled => isCommandEnabled ); +} ); +``` From cb7a552207ad69d10c23d12adf1918f76df7dea1 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 8 Oct 2018 11:55:11 +0200 Subject: [PATCH 04/12] Docs: Extended deep dive into observables guide. --- .../framework/guides/deep-dive/observables.md | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/framework/guides/deep-dive/observables.md b/docs/framework/guides/deep-dive/observables.md index acd3646..0dd1af9 100644 --- a/docs/framework/guides/deep-dive/observables.md +++ b/docs/framework/guides/deep-dive/observables.md @@ -82,7 +82,7 @@ Additionally, as the number of observable properties increases, you can save you ## Property bindings -One observable can also propagate its state (or part of it) to another observable to simplify the code and avoid numerous `change:property` event listeners. First, make sure both objects (classes) mix the {@link module:utils/observablemixin~ObservableMixin}. +One observable can also propagate its state (or part of it) to another observable to simplify the code and avoid numerous `change:property` event listeners. First, make sure both objects (classes) mix the {@link module:utils/observablemixin~ObservableMixin}, then use the {@link module:utils/observablemixin~ObservableMixin#bind `bind()`} method to create the binding. ### Simple bindings @@ -93,7 +93,7 @@ const command = new Command( 'bold' ); const command = new Button(); ``` -Any decent button must update its look when the command becomes disabled. A simple property binding doing that could look as follows: +Any "decent" button must update its look when the command becomes disabled. A simple property binding doing that could look as follows: ```js button.bind( 'isEnabled' ).to( command ); @@ -151,7 +151,7 @@ button.bind( 'isEnabled', 'currentState' ).to( command, 'isWorking', 'value' ); ### Binding with multiple observables -The binding can include more than one observable, combining multiple properties. Let's create a button that gets enabled only when the `command` is enabled and the `ui` (also an `Observable`) is visible: +The binding can include more than one observable, combining multiple properties in a custom callback function. Let's create a button that gets enabled only when the `command` is enabled and the `ui` (also an `Observable`) is visible: ```js button.bind( 'isEnabled' ).to( command, 'isEnabled', ui, 'isVisible', @@ -183,3 +183,34 @@ button.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => { return areEnabled.every( isCommandEnabled => isCommandEnabled ); } ); ``` + +### Releasing the bindings + +If you don't want your object's properties to be bound any longer, you can use the {@link module:utils/observablemixin~ObservableMixin#unbind `unbind()`} method. + +You can specify the names of the properties to selectively unbind them + +```js +button.bind( 'isEnabled', 'value' ).to( command ); + +// ... + +// From now on, button.isEnabled is no longer bound to the command. +button.unbind( 'isEnabled' ); +``` + +or you can dismiss all bindings by calling the method without arguments + +```js +button.bind( 'isEnabled', 'value' ).to( command ); + +// ... + +// Both "isEnabled" and "value" properties are independent back again. +// They will retain the values determined by the bindings, though. +button.unbind(); +``` + +## Decorating object methods + +TODO From 272bebe5f76ede8a8860005dd7671b46a141e877 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 8 Oct 2018 11:55:38 +0200 Subject: [PATCH 05/12] =?UTF-8?q?Docs:=20Simplified=20Observable#bind=20do?= =?UTF-8?q?cs=20and=20cross=E2=80=93linked=20to=20the=20deep=20dive=20into?= =?UTF-8?q?=20observables=20guide.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/observablemixin.js | 91 ++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 56 deletions(-) diff --git a/src/observablemixin.js b/src/observablemixin.js index 52b7190..fab998c 100644 --- a/src/observablemixin.js +++ b/src/observablemixin.js @@ -19,6 +19,11 @@ const boundPropertiesSymbol = Symbol( 'boundProperties' ); * Mixin that injects the "observable properties" and data binding functionality described in the * {@link ~Observable} interface. * + * Read more about the concept of observables in the: + * * {@glink framework/guides/architecture/core-editor-architecture#event-system-and-observables "Event system and observables"} + * section of the {@glink framework/guides/architecture/core-editor-architecture "Core editor architecture"} guide, + * * {@glink framework/guides/deep-dive/observables "Observables" deep dive} guide. + * * @mixin ObservableMixin * @mixes module:utils/emittermixin~EmitterMixin * @implements module:utils/observablemixin~Observable @@ -661,6 +666,11 @@ function attachBindToListeners( observable, toBindings ) { * * Can be easily implemented by a class by mixing the {@link module:utils/observablemixin~ObservableMixin} mixin. * + * Read more about the usage of this interface in the: + * * {@glink framework/guides/architecture/core-editor-architecture#event-system-and-observables "Event system and observables"} + * section of the {@glink framework/guides/architecture/core-editor-architecture "Core editor architecture"} guide, + * * {@glink framework/guides/deep-dive/observables "Observables" deep dive} guide. + * * @interface Observable * @extends module:utils/emittermixin~Emitter */ @@ -730,84 +740,53 @@ function attachBindToListeners( observable, toBindings ) { */ /** - * Binds properties to other objects implementing {@link module:utils/observablemixin~Observable} - * interface (e.g. {@link module:ui/model~Model}). - * - * **Note**: To release the binding use {@link module:utils/observablemixin~Observable#unbind}. - * - * # Simple bindings + * Binds {@link #set obvervable properties} to other objects implementing the + * {@link module:utils/observablemixin~Observable} interface. * - * Let's consider two objects: a `button` and an associated `command` (both `Observable`). A simple property - * binding could look as follows: + * Read more in the {@glink framework/guides/deep-dive/observables#property-bindings dedicated guide} + * covering the topic of property bindings with some additional examples. * - * button.bind( 'isEnabled' ).to( command ); - * - * After that: - * - * * `button.isEnabled` equals `command.isEnabled`, - * * whenever `command.isEnabled` changes, `button.isEnabled` will immediately follow. + * Let's consider two objects: a `button` and an associated `command` (both `Observable`). * - * Note that `command.isEnabled` **must** be defined using the {@link #set `set()`} method for the binding - * to be dynamic. `button.isEnabled` does not need to exist prior to the `bind()` call and in such case, it - * will be created on demand. - * - * The last example corresponds to the following code: + * A simple property binding could be as follows: * * button.bind( 'isEnabled' ).to( command, 'isEnabled' ); * - * You should notice the `to( ... )` interface which helps specify the name of the property ("rename" - * the property in the binding), for instance: - * - * button.bind( 'isEnabled' ).to( command, 'isCommandEnabled' ); - * - * In the above binding, whenever `command.isCommandEnabled` changes, the value of `button.isEnabled` - * will follow. - * - * # Binding multiple properties - * - * It is possible to bind more that one property at a time to simplify the code: - * - * button.bind( 'isEnabled', 'state' ).to( command ); - * - * which is the same as: + * or even shorter: * * button.bind( 'isEnabled' ).to( command ); - * button.bind( 'state' ).to( command ); * - * In the above binding, the value of `button.isEnabled` will follow `command.isEnabled` and the value of - * `button.state` will follow `command.state`. + * which works in the following way: * - * Renaming is also possible when binding multiple properties. Consider the following example + * * `button.isEnabled` **instantly equals** `command.isEnabled`, + * * whenever `command.isEnabled` changes, `button.isEnabled` will immediately reflect its value. * - * button.bind( 'isEnabled', 'state' ).to( command, 'isCommandEnabled', 'commandState' ); + * **Note**: To release the binding use {@link module:utils/observablemixin~Observable#unbind}. * - * which binds `button.isEnabled` to `command.isCommandEnabled` and `button.state` to `command.commandState`. + * You can also "rename" the property in the binding by specifying in in the `to()` chain: * - * # Binding with multiple observables + * button.bind( 'isEnabled' ).to( command, 'isWorking' ); * - * The binding can include more than one observable, combining multiple properties. Let's create a button - * that is enabled only when the `command` is enabled and the `editor` (also an `Observable`) is not read–only: + * It is possible to bind more that one property at a time to shorten the code: * - * button.bind( 'isEnabled' ).to( command, 'isEnabled', editor, 'isReadOnly', - * ( isCommandEnabled, isEditorReadOnly ) => isCommandEnabled && !isEditorReadOnly ); + * button.bind( 'isEnabled', 'value' ).to( command ); * - * From now on the value of `button.isEnabled` depends both on `command.isEnabled` and `editor.isReadOnly` - * as specified by the function: the former must be `true` and the later must be `false` for the button - * to become enabled. + * which corresponds to: * - * # Binding with an array of observables + * button.bind( 'isEnabled' ).to( command ); + * button.bind( 'value' ).to( command ); * - * It is possible to bind to the same property in an array of observables. Let's bind a `button` - * to multiple commands (also `Observables`) so that each one of them must be enabled for the button - * to become enabled: + * The binding can include more than one observable, combining multiple data sources in a custom callback: * - * button.bind( 'isEnabled' ).toMany( [ commandA, commandB, commandC ], 'isEnabled', - * ( isAEnabled, isBEnabled, isCEnabled ) => isAEnabled && isBEnabled && isCEnabled ); + * button.bind( 'isEnabled' ).to( command, 'isEnabled', ui, 'isVisible', + * ( isCommandEnabled, isUIVisible ) => isCommandEnabled && isUIVisible ); * - * The binding can be simplified using the spread operator (`...`) and the `Array.every()` method: + * It is also possible to bind to the same property in an array of observables. + * To bind a `button` to multiple commands (also `Observables`) so that each and every one of them + * must be enabled for the button to become enabled, use the following code: * * button.bind( 'isEnabled' ).toMany( [ commandA, commandB, commandC ], 'isEnabled', - * ( ...areEnabled ) => areEnabled.every( isCommandEnabled => isCommandEnabled ) ); + * ( isAEnabled, isBEnabled, isCEnabled ) => isAEnabled && isBEnabled && isCEnabled ); * * @method #bind * @param {...String} bindProperties Observable properties that will be bound to another observable(s). From dae50d975b6f4d0cc9a17cc9086855e16aad5bba Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Oct 2018 13:10:21 +0200 Subject: [PATCH 06/12] Docs: Added the section about decorating observable methods to the deep dive guide. Adjusted the API docs of the method. --- .../framework/guides/deep-dive/observables.md | 89 ++++++++++++++++++- src/observablemixin.js | 16 ++-- 2 files changed, 98 insertions(+), 7 deletions(-) diff --git a/docs/framework/guides/deep-dive/observables.md b/docs/framework/guides/deep-dive/observables.md index 0dd1af9..9660944 100644 --- a/docs/framework/guides/deep-dive/observables.md +++ b/docs/framework/guides/deep-dive/observables.md @@ -21,6 +21,7 @@ mix( AnyClass, ObservableMixin ); Observables are useful when it comes to managing the state of the application, which can be dynamic and, more often than not, centralized and shared between components of the application. One observable can also propagate its state (or its part) to another using [property bindings](#property-bindings). +Observables can also [decorate their methods](#decorating-object-methods) which makes it possible to control their execution using event listeners, giving external code some control over their behavior. ## Making properties observable @@ -213,4 +214,90 @@ button.unbind(); ## Decorating object methods -TODO +Decorating object methods transforms them into event–driven ones without changing their original behavior. + +When a method is decorated, an event of the same name is created and fired each time the method is executed. By listening to the event it is possible to cancel the execution, change the arguments or the value returned by the method. This offers an additional flexibility, e.g. giving a third–party code some way to interact with core classes that decorate their methods. + +Decorating is possible using the {@link module:utils/observablemixin~ObservableMixin#decorate `decorate()`} method. Having [mixed](#observables) the {@link module:utils/observablemixin~ObservableMixin}, we are going to use the `Command` class from previous sections of this guide to show the potential use–cases: + +```js +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; + +class Command { + constructor() { + this.decorate( 'execute' ); + } + + // Because the method is decorated, it always fires the #execute event. + execute( value ) { + console.log( `Executed the command with value="${ value }"` ); + } +} + +mix( Command, ObservableMixin ); +``` + +### Cancelling the execution + +Because the `execute()` method is event–driven, it can be controlled externally. E.g. the execution could be stopped for certain arguments. Note the `high` listener {@link module:utils/priorities~PriorityString priority} used to intercept the default action: + +```js +const command = new Command(); + +// ... + +// Some code interested in controlling this particular command. +command.on( 'execute', ( evt, args ) => { + if ( args[ 0 ] !== 'bold' ) { + evt.stop(); + } +}, { priority: 'high' } ); + +command.execute( 'bold' ); // -> 'Executed the command with value="bold"' +command.execute( 'italic' ); // Nothing is logged, the execution has been stopped. +``` + +### Changing the returned value + +It is possible to control the returned value of a decorated method using an event listener. The returned value is passed in the event data as a `return` property: + +```js +const command = new Command(); + +// ... + +// Some code interested in controlling this particular command. +command.on( 'execute', ( evt ) => { + if ( args[ 0 ] == 'bold' ) { + evt.return = true; + } else { + evt.return = false; + } +} ); + +console.log( command.execute( 'bold' ) ); // -> true +console.log( command.execute( 'italic' ) ); // -> false +console.log( command.execute() ); // -> false +``` + +### Changing arguments on the fly + +Just like the returned value, the arguments passed to the method can be changed in the event listener. Note the `high` listener {@link module:utils/priorities~PriorityString priority} of the used to intercept the default action: + + +```js +const command = new Command(); + +// ... + +// Some code interested in controlling this particular command. +command.on( 'execute', ( evt, args ) => { + if ( args[ 0 ] === 'bold' ) { + args[ 0 ] = 'underline'; + } +}, { priority: 'high' } ); + +command.execute( 'bold' ); // -> 'Executed the command with value="underline"' +command.execute( 'italic' ); // -> 'Executed the command with value="italic"' +``` diff --git a/src/observablemixin.js b/src/observablemixin.js index fab998c..a0f9f16 100644 --- a/src/observablemixin.js +++ b/src/observablemixin.js @@ -811,10 +811,13 @@ function attachBindToListeners( observable, toBindings ) { * 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), + * Read more in the {@glink framework/guides/deep-dive/observables#decorating-object-methods dedicated guide} + * covering the topic of decorating methods with some additional examples. + * + * Decorating the method does not change its behavior (it only adds an 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: + * For example, to cancel the method execution the event can be {@link module:utils/eventinfo~EventInfo#stop stopped}: * * class Foo { * constructor() { @@ -834,10 +837,11 @@ function attachBindToListeners( observable, toBindings ) { * foo.method(); // Nothing is logged. * * - * Note: we used a high priority listener here to execute this callback before the one which - * calls the original method (which used the default priority). + * **Note**: The high {@link module:utils/priorities~PriorityString priority} listener + * has been used to execute this particular callback before the one which calls the original method + * (which uses the "normal" priority). * - * It's also possible to change the return value: + * It is also possible to change the returned value: * * foo.on( 'method', ( evt ) => { * evt.return = 'Foo!'; @@ -845,7 +849,7 @@ function attachBindToListeners( observable, toBindings ) { * * foo.method(); // -> 'Foo' * - * Finally, it's possible to access and modify the parameters: + * Finally, it is possible to access and modify the arguments the method is called with: * * method( a, b ) { * console.log( `${ a }, ${ b }` ); From 2f754562fabf72abbf9203e3c676cdc2f712dc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 26 Oct 2018 14:53:07 +0200 Subject: [PATCH 07/12] Docs: Fixed broken documentation of the `Observable#bind` method. Co-Authored-By: oleq --- src/observablemixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observablemixin.js b/src/observablemixin.js index a0f9f16..6ab2687 100644 --- a/src/observablemixin.js +++ b/src/observablemixin.js @@ -763,7 +763,7 @@ function attachBindToListeners( observable, toBindings ) { * * **Note**: To release the binding use {@link module:utils/observablemixin~Observable#unbind}. * - * You can also "rename" the property in the binding by specifying in in the `to()` chain: + * You can also "rename" the property in the binding by specifying the new name in the `to()` chain: * * button.bind( 'isEnabled' ).to( command, 'isWorking' ); * From bb2608f3b91df4bef397215388fcaa5bb58290a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 26 Oct 2018 14:53:30 +0200 Subject: [PATCH 08/12] Docs: Fixed typo in the documentation of the `Observable#bind` method. Co-Authored-By: oleq --- src/observablemixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observablemixin.js b/src/observablemixin.js index 6ab2687..bf42c2d 100644 --- a/src/observablemixin.js +++ b/src/observablemixin.js @@ -767,7 +767,7 @@ function attachBindToListeners( observable, toBindings ) { * * button.bind( 'isEnabled' ).to( command, 'isWorking' ); * - * It is possible to bind more that one property at a time to shorten the code: + * It is possible to bind more than one property at a time to shorten the code: * * button.bind( 'isEnabled', 'value' ).to( command ); * From 8f8c7c307a3cc58b04ffca96cc7a793fee5edf86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 26 Oct 2018 14:55:16 +0200 Subject: [PATCH 09/12] Docs: Replaced a dash with an HTML entity in the observables deep dive guide. Co-Authored-By: oleq --- docs/framework/guides/deep-dive/observables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framework/guides/deep-dive/observables.md b/docs/framework/guides/deep-dive/observables.md index 9660944..3e7d350 100644 --- a/docs/framework/guides/deep-dive/observables.md +++ b/docs/framework/guides/deep-dive/observables.md @@ -105,7 +105,7 @@ After that: * `button.isEnabled` **instantly equals** `command.isEnabled`, * whenever `command.isEnabled` changes, `button.isEnabled` will immediately reflect its value. -Note that `command.isEnabled` **must** be defined using the `set()` method for the binding to be dynamic – we did that in the [previous chapter](#making-properties-observable). The `button.isEnabled` property does not need to exist prior to the `bind()` call and in such case, it will be created on demand. If the `button.isEnabled` property is already observable, don't worry: binding it to the command will do no harm. +Note that `command.isEnabled` **must** be defined using the `set()` method for the binding to be dynamic — we did that in the [previous chapter](#making-properties-observable). The `button.isEnabled` property does not need to exist prior to the `bind()` call and in such case, it will be created on demand. If the `button.isEnabled` property is already observable, don't worry: binding it to the command will do no harm. By creating the binding, we allowed the button to simply use its own `isEnabled` property, e.g. in the dynamic template (check out {@link framework/guides/architecture/ui-library#template this guide} to learn how). From db7f748cf4bcd2919246dcf80ff8630b9ccb3b94 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 26 Oct 2018 15:29:56 +0200 Subject: [PATCH 10/12] =?UTF-8?q?Docs:=20Explained=20the=20use=E2=80=93cas?= =?UTF-8?q?es=20and=20popularity=20of=20observables=20in=20the=20framework?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/framework/guides/deep-dive/observables.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/framework/guides/deep-dive/observables.md b/docs/framework/guides/deep-dive/observables.md index 3e7d350..8722f2d 100644 --- a/docs/framework/guides/deep-dive/observables.md +++ b/docs/framework/guides/deep-dive/observables.md @@ -6,6 +6,8 @@ category: framework-deep-dive {@link module:utils/observablemixin~Observable Observables} are objects which have properties that can be observed. That means when the value of such property changes, an event is fired by the observable and the change can be reflected in other pieces of the code that listen to that event. +Observables are common building blocks of the {@link framework/index CKEditor 5 Framework}. They are particularly popular in the UI, the {@link module:ui/view~View `View`} class and its subclasses benefiting from the observable interface the most: it is the {@link framework/guides/architecture/ui-library#interaction templates bound to the observables} what makes the user interface dynamic and interactive. Some of the basic classes like {@link module:core/editor/editor~Editor `Editor`} or {@link module:core/command~Command `Command`} are observables too. + Any class can become observable; all you need to do is mix the {@link module:utils/observablemixin~ObservableMixin} into it: ```js From 5757598fdf31846d072e843f108447b8c53ec952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Thu, 18 Oct 2018 12:34:30 +0200 Subject: [PATCH 11/12] Docs: Made contributing guide link to our docs. [skip ci] --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aefc066..95e8a02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ Contributing ======================================== -Information about contributing can be found at the following page: . +See the [official contributors' guide to CKEditor 5](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html) to learn more. From 10d4cac18476c4deac5e59f90ef22308f13ad69a Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 18 Oct 2018 16:57:39 +0200 Subject: [PATCH 12/12] Feature: Implmented env#isGecko. --- src/env.js | 20 +++++++++++++++++++- tests/env.js | 30 +++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/env.js b/src/env.js index 39154db..81cb6c6 100644 --- a/src/env.js +++ b/src/env.js @@ -31,7 +31,15 @@ const env = { * @static * @member {Boolean} module:utils/env~env#isEdge */ - isEdge: isEdge( userAgent ) + isEdge: isEdge( userAgent ), + + /** + * Indicates that the application is running in Firefox (Gecko). + * + * @static + * @member {Boolean} module:utils/env~env#isEdge + */ + isGecko: isGecko( userAgent ) }; export default env; @@ -55,3 +63,13 @@ export function isMac( userAgent ) { export function isEdge( userAgent ) { return !!userAgent.match( /edge\/(\d+.?\d*)/ ); } + +/** + * Checks if User Agent represented by the string is Firefox (Gecko). + * + * @param {String} userAgent **Lowercase** `navigator.userAgent` string. + * @returns {Boolean} Whether User Agent is Firefox or not. + */ +export function isGecko( userAgent ) { + return !!userAgent.match( /gecko\/\d+/ ); +} diff --git a/tests/env.js b/tests/env.js index 91fb373..2cae15a 100644 --- a/tests/env.js +++ b/tests/env.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import env, { isEdge, isMac } from '../src/env'; +import env, { isEdge, isMac, isGecko } from '../src/env'; function toLowerCase( str ) { return str.toLowerCase(); @@ -29,6 +29,12 @@ describe( 'Env', () => { } ); } ); + describe( 'isGecko', () => { + it( 'is a boolean', () => { + expect( env.isGecko ).to.be.a( 'boolean' ); + } ); + } ); + describe( 'isMac()', () => { it( 'returns true for macintosh UA strings', () => { expect( isMac( 'macintosh' ) ).to.be.true; @@ -75,4 +81,26 @@ describe( 'Env', () => { ) ) ).to.be.false; } ); } ); + + describe( 'isGecko()', () => { + it( 'returns true for Firefox UA strings', () => { + expect( isGecko( 'gecko/42' ) ).to.be.true; + expect( isGecko( 'foo gecko/42 bar' ) ).to.be.true; + + expect( isGecko( toLowerCase( + 'mozilla/5.0 (macintosh; intel mac os x 10.13; rv:62.0) gecko/20100101 firefox/62.0' + ) ) ).to.be.true; + } ); + + it( 'returns false for non–Edge UA strings', () => { + expect( isGecko( '' ) ).to.be.false; + expect( isGecko( 'foo' ) ).to.be.false; + expect( isGecko( 'Mozilla' ) ).to.be.false; + + // Chrome + expect( isGecko( toLowerCase( + 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' + ) ) ).to.be.false; + } ); + } ); } );