入门

监控属性

使用内置绑定

控制文本和外观

绑定逻辑控制

处理表单属性

解析模板

高级应用

插件

更多信息

Using extenders to augment observables

Knockout observables provide the basic features necessary to support reading/writing values and notifying subscribers when that value changes. In some cases, though, you may wish to add additional functionality to an observable. This might include adding additional properties to the observable or intercepting writes by placing a writeable computed observable in front of the observable. Knockout extenders provide an easy and flexible way to do this type of augmentation to an observable.

How to create an extender

Creating an extender involves adding a function to the ko.extenders object. The function takes in the observable itself as the first argument and any options in the second argument. It can then either return the observable or return something new like a computed observable that uses the original observable in some way.

This simple logChange extender subscribes to the observable and uses the console to write any changes along with a configurable message.

  1. ko.extenders.logChange = function(target, option) {
  2. target.subscribe(function(newValue) {
  3. console.log(option + ": " + newValue);
  4. });
  5. return target;
  6. };

You would use this extender by calling the extend function of an observable and passing an object that contains a logChange property.

  1. this.firstName = ko.observable("Bob").extend({logChange: "first name"});

If the firstName observable’s value was changed to Ted, then the console would show first name: Ted.

Live Example 1: Forcing input to be numeric

This example creates an extender that forces writes to an observable to be numeric rounded to a configurable level of precision. In this case, the extender will return a new writeable computed observable that will sit in front of the real observable intercepting writes.

(round to whole number)

(round to two decimals)

Source code: View

  1. <p><input data-bind="value: myNumberOne" /> (round to whole number)</p>
  2. <p><input data-bind="value: myNumberTwo" /> (round to two decimals)</p>

Source code: View model

  1. ko.extenders.numeric = function(target, precision) {
  2. //create a writeable computed observable to intercept writes to our observable
  3. var result = ko.computed({
  4. read: target, //always return the original observables value
  5. write: function(newValue) {
  6. var current = target(),
  7. roundingMultiplier = Math.pow(10, precision),
  8. newValueAsNum = isNaN(newValue) ? 0 : parseFloat(+newValue),
  9. valueToWrite = Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;
  10.  
  11. //only write if it changed
  12. if (valueToWrite !== current) {
  13. target(valueToWrite);
  14. } else {
  15. //if the rounded value is the same, but a different value was written, force a notification for the current field
  16. if (newValue !== current) {
  17. target.notifySubscribers(valueToWrite);
  18. }
  19. }
  20. }
  21. });
  22.  
  23. //initialize with current value to make sure it is rounded appropriately
  24. result(target());
  25.  
  26. //return the new computed observable
  27. return result;
  28. };
  29.  
  30. function AppViewModel(one, two) {
  31. this.myNumberOne = ko.observable(one).extend({ numeric: 0 });
  32. this.myNumberTwo = ko.observable(two).extend({ numeric: 2 });
  33. }
  34.  
  35. ko.applyBindings(new AppViewModel(221.2234, 123.4525));

Live Example 2: Adding validation to an observable

This example creates an extender that allows an observable to be marked as required. Instead of returning a new object, this extender simply adds additional sub-observables to the existing observable. Since observables are functions, they can actually have their own properties. However, when the view model is converted to JSON, the sub-observables will be dropped and we will simply be left with the value of our actual observable. This is a nice way to add additional functionality that is only relevant for the UI and does not need to be sent back to the server.


Source code: View

  1. <p data-bind="css: { error: firstName.hasError }">
  2. <input data-bind='value: firstName, valueUpdate: "afterkeydown"' />
  3. <span data-bind='visible: firstName.hasError, text: firstName.validationMessage'> </span>
  4. </p>
  5. <p data-bind="css: { error: lastName.hasError }">
  6. <input data-bind='value: lastName, valueUpdate: "afterkeydown"' />
  7. <span data-bind='visible: lastName.hasError, text: lastName.validationMessage'> </span>
  8. </p>

Source code: View model

  1. ko.extenders.required = function(target, overrideMessage) {
  2. //add some sub-observables to our observable
  3. target.hasError = ko.observable();
  4. target.validationMessage = ko.observable();
  5.  
  6. //define a function to do validation
  7. function validate(newValue) {
  8. target.hasError(newValue ? false : true);
  9. target.validationMessage(newValue ? "" : overrideMessage || "This field is required");
  10. }
  11.  
  12. //initial validation
  13. validate(target());
  14.  
  15. //validate whenever the value changes
  16. target.subscribe(validate);
  17.  
  18. //return the original observable
  19. return target;
  20. };
  21.  
  22. function AppViewModel(first, last) {
  23. this.firstName = ko.observable(first).extend({ required: "Please enter a first name" });
  24. this.lastName = ko.observable(last).extend({ required: "" });
  25. }
  26.  
  27. ko.applyBindings(new AppViewModel("Bob","Smith"));

Applying multiple extenders

More than one extender can be applied in a single call to the .extend method of an observable.

  1. this.firstName = ko.observable(first).extend({ required: "Please enter a first name", logChange: "first name" });

In this case, both the required and logChange extenders would be executed against our observable.

(c) knockoutjs.com