Search code examples
knockout.jsko-custom-binding

Custom Binding - init keeps getting called


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.

enter image description here

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>


Solution

  • 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>