I've been trying to use both a custom component, and then a custom binding within Knockout to render a small visualization. I got this working, but realized the solution wasn't ideal. My custom binding is supposed to handle changes to the data by using transitions.
If a new item appears, say E
, I'd like it to transition in. I also need to update the green stroke round each circle everytime the binding changes.
My issue is that when my context
binding updates in the code below, that the whole binding seems to re-initialize. I think Knockout underneath is removing the DOM for the previous context
and re-renders the entire lot - this causes each circle to grow and triggers the new item animation.
ko.components.register("context", {
// Assume that the view model given to us is already observable, having had a ko.mapping.fromJS() applied or similar
viewModel: function (vm) {
this.context = vm;
},
template: '<div class="card context-card" data-bind="contextViz: context">\
<!-- ko if: context && context.types.length === 0 -->\
<div>Please make a selection to view contextual information here.</div>\
<!-- /ko -->\
</div>\
'
});
I'm wondering if there's anything I can do to prevent this. Essentially I want the old context
binding to just update but I'm not sure if there's a way to do so.
I've added a code snippet to illustrate. Here init called
is logged each time an update occurs, I only want this to log once, and update called
to be logged many times.
var count = 0;
ko.bindingHandlers.contextViz = {
init: function() {
console.log("init called");
},
update: function() {
console.log("update called");
}
};
ko.components.register("context", {
viewModel: function (vm) {
this.context = vm;
},
template: '<div class="card context-card" data-bind="contextViz: context">'
});
var vm = {
context: ko.observable({ count: count })
};
ko.applyBindings(vm);
setInterval(function() {
vm.context({ count: count + 1 });
}, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: { name: 'context', params: $root.context }"></div>
Because you are directly using your context
property as the params
of your computed KO will re-render the whole computed when the context
changes.
The usual practice is to pass in an object with some properties as the params
so when the properties of this object changes the component does not completely re-rendered.
So you need to change your HTMl to:
<div data-bind="component: { name: 'context', params: { context: $root.context }}"></div>
And in your computed constructor just write:
viewModel: function (params) {
this.context = params.context;
},
var count = 0;
ko.bindingHandlers.contextViz = {
init: function() {
document.getElementById("log").innerHTML += "init called\n";
},
update: function(e,v) {
console.log(ko.unwrap(v())) // make a dependency so it will be called when the bound observable changes
document.getElementById("log").innerHTML += "update called\n";
}
};
ko.components.register("context", {
viewModel: function (params) {
this.context = params.context;
},
template: '<div class="card context-card" data-bind="contextViz: context">'
});
var vm = {
context: ko.observable({ count: count })
};
ko.applyBindings(vm);
setInterval(function() {
vm.context({ count: ++count });
}, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: { name: 'context', params: { context: $root.context }}"></div>
<div>log:</div>
<pre id="log"></pre>