javascriptknockout.jsko-custom-binding

Extending an observable in a custom binding


I have a custom binding handler that I am binding to a complex object in my view model.

The binding handler works correctly and the update function is called when any of the observable's properties update. However, the update function is called for every updated property, leading to odd behaviour since I am relying on the entire object to be available and up to date.

I understand why this is happening, as each property is causing an update to be called, and I think I know how to prevent this - by using the deferred updates functionality of Knockout.

However, I am unable to find how to enable deferred updates just for the observable in my custom binding. I do not want to enable it application wide as I am writing the binding as a library function.

I have tried many different methods including:

All of which have not worked.

I have not found any other custom binding handler that comes remotely close to this sort of function and have been trying to piece it together from other functions.

My binding code itself is relatively simple, I am taking the bound object and simply splitting out the parameters and passing them to a Code Mirror instance.

ko.bindingHandlers.editor = {
    init: function(element, valueAccessor, allBindingsAccessor) {
        var observableValue = ko.utils.unwrap(valueAccessor());
        initEditor(element, observableValue, allBindingsAccessor);
    },
    update: function(element, valueAccessor, allBindingsAccessor) {
        var observableValue = ko.unwrap(valueAccessor());

        createEditor(codeEditorDiv, observableValue);
        resize();
        updateEditor(element, observableValue, allBindingsAccessor);
    }
};

And my HTML code is:

 <div id="editor" data-bind="editor: EditorVM"></div>

I am using Dotnetify for the ViewModel so it is a reasonable complex C# class, but suffice it to say that the binding is working and updating, but I need it to only call 'update' once all properties have been updated.


Solution

  • It's unfortunate you haven't shown what initEditor, createEditor and updateEditor do with the observableValue, because that's probably where you should be extending your observables.

    The init and update methods of a binding create computed dependencies, meaning that any observable that is unwrapped in the call stack starting from init will cause the update method to be called.

    In an abstract example:

    const someVM = ko.observable({
      a: ko.observable(1),
      b: ko.observable(2),
      c: ko.observable(3)
    });
    
    // Some function that unwraps properties
    const logABC = function(vm) {
      console.log(
        vm.a(),
        vm.b(),
        vm.c()
      );
    }
    
    // Regular binding update:
    ko.computed(function update() {
      console.log(
        "Regular binding update:",
      )
      logABC(someVM())
    });
    
    // Change VM
    someVM(someVM());
    
    // Change a, b, and c
    someVM().a("A");
    someVM().b("B");
    someVM().c("C");
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

    Note that update is called:

    1. When initializing the computed
    2. When changing the observable that contains the viewmodel
    3. When changing any of the observable properties of the viewmodel

    There are several ways of solving the issue, of which the simplest is to create your own computed inside the init method of your binding and extend it to be deferred.

    const someVM = ko.observable({
      a: ko.observable(1),
      b: ko.observable(2),
      c: ko.observable(3)
    });
    
    const getABC = function(vm) {
      return [vm.a(), vm.b(), vm.c()].join(", ");
    }
    
    ko.bindingHandlers.renderABC = {
      init: function(el, va) {
        el.innerText += "Init.\n";
        
        // This ensures any inner unwrapping gets deferred
        var updateSub = ko.computed(function update() {
          el.innerText += getABC(ko.unwrap(va())) + "\n";
        }).extend({ deferred: true });
        
        ko.utils.domNodeDisposal.addDisposeCallback(el, function() {
          updateSub.dispose();
        });
      }
    }
    
    ko.applyBindings({ someVM: someVM });
    
    // Change VM
    someVM(someVM());
    
    // Change a, b, and c
    someVM().a("A");
    someVM().b("B");
    someVM().c("C");
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    
    <pre data-bind="renderABC: someVM"></pre>