Knockout afterRender, but just once

2019-02-04 06:55发布

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.

标签: knockout.js
8条回答
Rolldiameter
2楼-- · 2019-02-04 07:23

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);
查看更多
Anthone
3楼-- · 2019-02-04 07:26

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.
            });
        }
}
查看更多
淡お忘
4楼-- · 2019-02-04 07:34

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

<!--ko foreach: { data: ['1'], afterRender: YourAfterRenderFunction } -->
<!--/ko-->
查看更多
一夜七次
5楼-- · 2019-02-04 07:37

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

查看更多
孤傲高冷的网名
6楼-- · 2019-02-04 07:38

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);
查看更多
等我变得足够好
7楼-- · 2019-02-04 07:41

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})
查看更多
登录 后发表回答