Knockout afterRender, but just once

2019-02-04 06:45发布

问题:

I have a simple observableArray which contains a lot of user-models. In the markup, there is a template with a foreach loop which loops the users and outputs them in a simple table. I additionally style the table with a custom scrollbar and some other javascript. So now I have to know when the foreach loop is finished and all the models are added to the DOM.

The problem with the afterRender callback is that it gets called every time something is added, but I need kind of a callback which fires only once.

回答1:

Your best bet is to use a custom binding. You can either place your custom binding after foreach in the list of bindings in your data-bind or you could execute your code in a setTimeout to allow foreach to generate the content before your code is executed.

Here is a sample that shows running code a single time and running code each time that your observableArray updates: http://jsfiddle.net/rniemeyer/Ampng/

HTML:

<table data-bind="foreach: items, updateTableOnce: true">
    <tr>
        <td data-bind="text: id"></td>
        <td data-bind="text: name"></td>
    </tr>
</table>

<hr/>

<table data-bind="foreach: items, updateTableEachTimeItChanges: true">
    <tr>
        <td data-bind="text: id"></td>
        <td data-bind="text: name"></td>
    </tr>
</table>

<button data-bind="click: addItem">Add Item</button>

JS:

var getRandomColor = function() {
   return 'rgb(' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ')';  
};

ko.bindingHandlers.updateTableOnce = {
    init: function(element) {
        $(element).css("color", getRandomColor());            
    }    
};

//this binding currently takes advantage of the fact that all bindings in a data-bind will be triggered together, so it can use the "foreach" dependencies
ko.bindingHandlers.updateTableEachTimeItChanges = {
    update: function(element) {    
        $(element).css("color", getRandomColor());  
    }    
};


var viewModel = {
    items: ko.observableArray([
        { id: 1, name: "one" },
        { id: 1, name: "one" },
        { id: 1, name: "one" }
    ]),
    addItem: function() {
        this.items.push({ id: 0, name: "new" });   
    }
};

ko.applyBindings(viewModel);


回答2:

I came up with an elegent cheat. Immediately after your template / foreach block, add this code:

<!--ko foreach: { data: ['1'], afterRender: YourAfterRenderFunction } -->
<!--/ko-->


回答3:

A quick and simple way is to, in your afterRender handler, compare the current item with the last item in your list. If it matches, then this is the last time afterRender is run.



回答4:

Jaffa's answer has contains an error so I decided to create a new answer instead of commenting. It is not possible to use with and template at the same time. So just move your model to template's data tag

Html

<div data-bind="template: {data: myModel, afterRender: onAfterRenderFunc }" >
   <div data-bind="foreach: observableArrayItems">
       ...
   </div>
</div>

Javascript

var myModel = {      
  observableArrayItems : ko.observableArray(),

  onAfterRenderFunc: function(){
    console.log('onAfterRenderFunc')
  }

}
ko.applyBinding(myModel);


回答5:

I am not sure if the accepted answer will work in knockout-3.x (as data-bindings no longer are run in the order you declare them).

Here is another option, it will only fire exactly once.

ko.bindingHandlers.mybinding {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            var foreach = allBindings().foreach,
                subscription = foreach.subscribe(function (newValue) {
                // do something here 
                subscription.dispose(); // dispose it if you want this to happen exactly once and never again.
            });
        }
}


回答6:

Just off the top of my head you could either:

  • Instead of hooking into the afterRender event, simply call your function after you have push/popped an item on the array.
  • Or possibly wrap the observableArray in an observable which in itself has the child items underneath with its own afterRender event. The foreach loop would need to refer to the parent observable like so:

Example:

 <div>
     <div data-bind="with: parentItem(), template: { afterRender: myRenderFunc }" >
      <div data-bind="foreach: observableArrayItems">
          ...
      </div>
    </div>
   </div>

Not tested this so just guessing...



回答7:

In order to find out when the foreach template has completed rendering, you can make a "dummy" binding and pass the current rendered item index to your handler and check if its match the array length.

HTML:

<ul data-bind="foreach: list">
   <li>
   \\ <!-- Your foreach template here -->
   <div data-bind="if: $root.listLoaded($index())"></div> 
   </li>
</ul>

ViewModel - Handler:

this.listLoaded = function(index){
    if(index === list().length - 1)
       console.log("this is the last item");
}

Hold extra flag if you intend to add more items to the list.



回答8:

How about using a debouncer?

var afterRender = _.debounce(function(){    

    // code that should only fire 50ms after all the calls to it have stopped

}, 50, { leading: false, trailing: true})


标签: knockout.js