Knockout: Custom Bindings - passing state between init and update

Created on 10 Feb 2016  路  7Comments  路  Source: knockout/knockout

At the moment there isn't a way built into Knockout for sharing state between the init function and the update function of a binding handler. For example in the following:

ko.bindingHandlers["myViz"] = {
   init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
      var bubble = d3.layout.pack().sort(null).padding(1);
   },
   update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
      var data = ko.unwrap(valueAccessor()).data;

      var nodes = d3.select(element)
                             .selectAll(".node")
                             .data(bubble.nodes(data));
   }
};

I need to be able to re-use bubble between init and update. I only want to create and initialize bubble once, but I do need it every time I receive a data update. It'd be really nice if there was some sort of state based object that got passed from init (possibly it could return it) to update.

Most helpful comment

The update function is just a shortcut for a computed, so you can capture state in the closure of init.

ko.bindHandlers.myViz = {
    init: function(element, valueAccessor) {
        var bubble =  d3.layout.pack().sort(null).padding(1);
        ko.computed({
            read: function () {
                var data = ko.unwrap(valueAccessor()).data;

                var nodes = d3.select(element)
                    .selectAll(".node")
                    .data(bubble.nodes(data));
            },
            disposeWhenNodeIsRemoved: element
        });
    }
}

This is a extremly common pattern, so I've made a binding handler factory to handle this.

function createBindingHandler (obj) {
    return {
        init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
            var newObj = Object.create(obj);

            if (obj.dispose) {
                ko.utils.domNodeDisposal.addDisposeCallback(element, obj.dispose.bind(newObj, element, valueAccessor, allBindings, viewModel, bindingContext));
            }

            if (obj.init) {
                obj.init.call(newObj, element, valueAccessor, allBindings, viewModel, bindingContext);
            }

            if (obj.update) {
                ko.computed({
                    read: obj.update.bind(newObj, element, valueAccessor, allBindings, viewModel, bindingContext),
                    disposeWhenNodeIsRemoved: element
                });
            }

            ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
                newObj = null;
            });
        }
    };
}

Which you can use like so:

ko.bindingHandlers.myViz = createBindingHandler({
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        this.bubble = d3.layout.pack().sort(null).padding(1);
    },

    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var data = ko.unwrap(valueAccessor()).data;

        var nodes = d3.select(element)
            .selectAll(".node")
            .data(this.bubble.nodes(data));
    },

    dispose: function () {
        // cleanup logic
    }
});

All 7 comments

The update function is just a shortcut for a computed, so you can capture state in the closure of init.

ko.bindHandlers.myViz = {
    init: function(element, valueAccessor) {
        var bubble =  d3.layout.pack().sort(null).padding(1);
        ko.computed({
            read: function () {
                var data = ko.unwrap(valueAccessor()).data;

                var nodes = d3.select(element)
                    .selectAll(".node")
                    .data(bubble.nodes(data));
            },
            disposeWhenNodeIsRemoved: element
        });
    }
}

This is a extremly common pattern, so I've made a binding handler factory to handle this.

function createBindingHandler (obj) {
    return {
        init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
            var newObj = Object.create(obj);

            if (obj.dispose) {
                ko.utils.domNodeDisposal.addDisposeCallback(element, obj.dispose.bind(newObj, element, valueAccessor, allBindings, viewModel, bindingContext));
            }

            if (obj.init) {
                obj.init.call(newObj, element, valueAccessor, allBindings, viewModel, bindingContext);
            }

            if (obj.update) {
                ko.computed({
                    read: obj.update.bind(newObj, element, valueAccessor, allBindings, viewModel, bindingContext),
                    disposeWhenNodeIsRemoved: element
                });
            }

            ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
                newObj = null;
            });
        }
    };
}

Which you can use like so:

ko.bindingHandlers.myViz = createBindingHandler({
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        this.bubble = d3.layout.pack().sort(null).padding(1);
    },

    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var data = ko.unwrap(valueAccessor()).data;

        var nodes = d3.select(element)
            .selectAll(".node")
            .data(this.bubble.nodes(data));
    },

    dispose: function () {
        // cleanup logic
    }
});

@kimgronqvist ah I hadn't realized that. Your factory pattern is really nice in that case (mind if I use it?). Can I ask what the final domNodeDisposal call is used for setting newObj to null?

Feel free to use it :) The domNodeDisposal call is mainly there because I'm paranoid about memory leaks, but it might be superfluous.

@kimgronqvist OK - I'm happy to share in paranoia and thanks!

Can you explain one thing about your binding that doesn't have the 'update':

How is the update actions invoked? Is it that the ko.computed() that you are initializing attaches to the dependency tracker such that when the observable referenced by valueAccessor() signals a change, the computed will invoke the 'read' function?

@chrisknoll - that's right. The ko.computed will subscribe to accessed observables and rerun its code upon changes. This gives you the equivalent of an "update" function.

Note that it's _not_ a pure computed.
In my code, to drive home the fact a computed is being used more for its side-effects than to generate a value, I alias ko.computed as ko.sideEffects. This makes it really clear IMHO and means I pretty much have ko.sideEffects and ko.pureComputed in my code and the use of ko.computed directly, in my code base, is a bit of a code smell or an indication of an older pattern (before pureComputed came along).

And unless you have a way for the computed to be disposed it will be kept alive by the objects to which it has subscribed (unlike a pure computed) -> possible memory leak!

Was this page helpful?
0 / 5 - 0 ratings