Drew Turner
Drew Turner

Reputation: 870

What is the preferred way to store state between init and update for custom knockout binding?

Currently I am storing the state using jQuery data for the dom element.

ko.bindingHandlers.customValue = {

    init: function init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var state = { isEditing: false };        
        $(element).focus(function focus() {
            state.isEditing = true;
        }).blur(function blur() {
            state.isEditing = false;            
        }).data("customBinding", state);

    },

    update: function update(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        // ignore if updating
        if (!$(element).data("customBinding").isEditing) {
            // handle update if they are not updating                                
        }
    }

};​

Is there a better place to store state per binding that does not require the dom? Can the bindingContext be used to store state for the each instance of the binding?

Upvotes: 57

Views: 9158

Answers (5)

肉肉Linda
肉肉Linda

Reputation: 588

I use an function to create an common scope for init and update, by defining the common data inside the function.

Upvotes: -2

Dave Jellison
Dave Jellison

Reputation: 933

I realize this question is old but I stumbled here looking for the approach so thought I'd chip in on a more modern ko method.

You could simply add properties to the bindingContext.$data directly. I chose an annoying variable name, "___IsEditing", to avoid potential collisions, but you get the idea...

ko.bindingHandlers.customValue = {

init: function init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    bindingContext.$data.___IsEditing = false;        
    $(element).focus(function focus() {
        bindingContext.$data.___IsEditing = true;
    }).blur(function blur() {
        bindingContext.$data.___IsEditing = false;            
    }).data("customBinding", state);

},

update: function update(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    // ignore if updating
    if (bindingContext.$data.___IsEditing) {
        // handle update if they are not updating                                
    }
}

};​

Upvotes: 2

Blago
Blago

Reputation: 4737

I often use this pattern:

define(['knockout'], function(ko) {
  var interInstanceVariable = null;

  function Tree(element) {
    var privateInstanceVariable = null;

    function privateInstanceMethod() {}

    this.publicInstanceMethod = function() {}
  }


  ko.bindingHandlers.cannDendrogram = {
    init: function(element, valueAccessor) {
      $(element).data('tree', new Tree(element));
    },
    update: function(element, valueAccessor) {
      var tree = $(element).data('tree');
      tree.publicMethod();
    }
  };
});

Upvotes: 2

Michael Best
Michael Best

Reputation: 16688

Attaching data to the element is fine, and Knockout uses this method internally for the control-flow bindings (if, with, etc.), for example.

Another method is to only use the init function and use a computed observable to handle updates. I use this method in my repeat binding. Here are the important parts:

ko.bindingHandlers['repeat'] = {
    'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        ...
        // set up persistent data
        var lastRepeatCount = 0;
        ...
        ko.computed(function() {
            var repeatCount = ko.utils.unwrapObservable(valueAccessor());
            ...
            // Remove nodes from end if array is shorter
            for (; lastRepeatCount > repeatCount; lastRepeatCount--) {
                ...
            }
            ...
            // Add nodes to end if array is longer (also initially populates nodes)
            for (; lastRepeatCount < repeatCount; lastRepeatCount++) {
                ...
            }
        }, null, {'disposeWhenNodeIsRemoved': placeholder});
        ...
    }
};

Upvotes: 11

RP Niemeyer
RP Niemeyer

Reputation: 114792

The bindingContext is a possibility, but only for passing data from init to update the first time that the binding is triggered. The next time that update fires it would no longer be there.

There are really two choices for where to store this type of state:

1- On the element, as you stated. You can use jQuery's $.data or KO includes APIs for doing this as well ko.utils.domData.get(element, key) and ko.utils.domData.set(element, key, value).

2- Put this type of information in your view model, if appropriate. A flag to indicate isEditing is not necessarily out of place in a view model. I personally like to put this type of "meta-data" as sub-observables off of an observable like:

var name = ko.observable("Bob");
name.isEditing = ko.observable(false);

You would be able to bind against name and name.isEditing.

This has some advantages:

  • keeps the view model fairly clean, as you are not introducing new top-level properties
  • keeps the sub-observable tied to its parent observable (no need for nameIsEditing, etc.)
  • when turned into JSON with something like ko.toJSON the isEditing sub-observable will simply be dropped when its parent is unwrapped. So, you won't be sending unnecessary values back to the server.
  • in this case, it can also have the advantage of being available for other calculations in your view model or to bind against multiple elements in your UI.

Upvotes: 49

Related Questions