- Mastering KnockoutJS
- Timothy Moran
- 946字
- 2021-08-05 17:13:12
Extenders
The last "basic" feature to cover is extenders (don't worry, there is still plenty of advanced stuff to cover). Extenders offer a way to modify individual observables. Two common uses of extenders are as follows:
- Adding properties or functions to the observable
- Adding a wrapper around the observable to modify writes or reads
Simple extenders
Adding an extender is as simple as adding a new function to the ko.extenders
object with the name you want to use. This function receives the observable being extended (called the target) as the first argument, and any configuration passed to the extender is received as the second argument, as shown in the following code:
ko.extenders.recordChanges = function(target, options) { target.previousValues = ko.observableArray(); target.subscribe(function(oldValue) { target.previousValues.push(oldValue); }, null, 'beforeChange'); return target; };
This extender will create a new previousValues
property on the observable. This new property is as an observable array and old values are pushed to it as the original observable is changed (the current value is already in the observable of course).
The reason the extender has to return the target is because the result of the extender is the new observable. The need for this is apparent when looking at how the extender is called:
var amount = ko.observable(0).extend({ recordChanges: true});
The true
value sent to recordChanges
is received by the extender as the options
parameter. This value can be any JavaScript value, including objects and functions.
You can also add multiple extenders to an observable in the same call. The object sent to the extend
method will call an observable for every property it contains:
var amount = ko.observable(0).extend({ recordChanges: true,anotherExtender: { intOption: 1});
As the extend
method is called on the observable, usually during its initial creation, the result of the extend
call is what is actually stored. If the target is not returned, the amount
variable would not be the intended observable.
To access the extended value, you would use amount.previousValues()
from JavaScript, or amount.previousValues
if accessing it from a binding. Note the lack of parentheses after amount; because previousValues
is a property of the observable, not a property of the observable's value, it is accessed directly. This might not be immediately obvious, but it should make sense as long as you remember that the observable and the value the observable contains are two different JavaScript objects.
An example of this extender is in the cp1-extend
branch.
Extenders with options
The previous example does not pass any options to the recordChanges
extender, it just uses true
because the property requires a value to be a valid JavaScript. If you want a configuration for your extender, you can pass it as this value, and a complex configuration can be achieved by using another object as the value.
If we wanted to supply a list of values that are not to be recorded, we could modify the extender to use the options as an array:
ko.extenders.recordChanges = function(target, options) { target.previousValues = ko.observableArray(); target.subscribe(function(oldValue) { if (!(options.ignore && options.ignore.indexOf(oldValue) !== -1)) target.previousValues.push(oldValue) }, null, 'beforeChange'); return target; };
Then we could call the extender with an array:
var history = ko.observable(0).extend({ recordChanges: { ignore: [0, null] } });
Now our history
observable won't record values for 0
or null
.
Extenders that replace the target
Another common use for extenders is to wrap the observable with a computed observable that modifies reads or writes, in which case, it would return the new observable instead of the original target.
Let's take our recordChanges
extender a step further and actually block writes that are in our ignore
array (never mind that an extender named recordChanges
should never do something like this in the real world!):
ko.extenders.recordChanges = function(target, options) { var ignore = options.ignore instanceof Array ? options.ignore : []; //Make sure this value is available var result = ko.computed({ read: target, write: function(newValue) { if (ignore.indexOf(newValue) === -1) { result.previousValues.push(target()); target(newValue); } else { target.notifySubscribers(target()); } } }).extend({ notify: 'always'}); result.previousValues = ko.observableArray(); //Return the computed observable return result; };
That's a lot of changes, so let's unpack them.
First, to make ignore
easier to reference, I've set a new variable that will either be the options.ignore
property or an empty array. Defaulting to an empty array lets us skip the null check later, which makes the code a little easier to read. Second, I created a writable computed observable. The read
function just routes to the target observable, but the write
function will only write to the target if the ignore
option doesn't contain the new value. Otherwise, it will notify the target subscribers of the old value. This is necessary because if a UI binding on the observable initiated the change, it needs the illegal change to be reverted. The UI element would already have updated and the easiest way to change it back is through the standard binding notification mechanism that is already listening for changes.
The last change is the notify: always
extender that's on the result
. This is one of Knockout's default extenders. Normally, an observable will only report changes to subscribers when the value has been modified. To get the observable to reject changes, it needs to be able to notify subscribers of its current unchanged value. The notify extender forces the observable to always report changes, even when they are the same.
Finally, the extender returns the new computed observable instead of the target, so that anyone trying to write a value does so against the computed.
The cp1-extendreplace
branch has an example of this binding. Notice that trying to enter values into the input box that are included in the ignored options (0
or an empty string) are immediately reverted.
- 極簡算法史:從數學到機器的故事
- 深度實踐OpenStack:基于Python的OpenStack組件開發
- Hands-On Machine Learning with scikit:learn and Scientific Python Toolkits
- 大學計算機基礎實驗教程
- Spring技術內幕:深入解析Spring架構與設計
- oreilly精品圖書:軟件開發者路線圖叢書(共8冊)
- SQL語言從入門到精通
- Mastering macOS Programming
- 深度學習:算法入門與Keras編程實踐
- PLC編程與調試技術(松下系列)
- Learning Hadoop 2
- 深入理解C指針
- Creating Data Stories with Tableau Public
- Hands-On Neural Network Programming with C#
- Raspberry Pi Robotic Projects(Third Edition)