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.
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!
Most helpful comment
The update function is just a shortcut for a computed, so you can capture state in the closure of init.
This is a extremly common pattern, so I've made a binding handler factory to handle this.
Which you can use like so: