I've seen this question and this other one about the topic, but none of them seem to solve my problem.
I'm trying to initialize a javascript plugin (BootStrap javascript tooltip) for an element within each row of a table.
To do it, the table has to be rendered in the DOM. And that's where the problem is.
As far as I know, knockout.js doesn't provide a proper way to know when a foreach
has render the whole table.
I've tried to solve it by applying the solution provided in the first link I posted, which is by creating a custom binding, but this doesn't work in my case. I guess because the data is being displayed by calling an API from an AJAX call.
ko.bindingHandlers.doSomething = {
update: function (element) {
console.log("table loaded???");
//applying the tooltip
$('.actions').tooltip({
title: 'Actions'
});
}
};
function ViewModel(){
var self = this;
self.myItems= ko.observableArray(['A', 'B', 'C']);
//emulating the delay of calling an external API
self.getdata = function(){
setTimeout(function(){
self.myItems.push('D');
self.myItems.push('E');
self.myItems.push('F');
}, 200);
};
self.addItem = function () {
this.myItems.push('New item');
};
//getting the data on loading
self.getdata();
}
ko.applyBindings(new ViewModel());
I would prefer not to use the afterRender
callback provided by Knockout.js for foreach
cases because that way the plugin would have to be initialized on every iteration rather than just once at the end.
The only solution I found for it was to apply a setTimeout
, before applying the tooltip plugin, but it is far from ideal:
ko.bindingHandlers.doSomething = {
update: function (element) {
console.log("table loaded???");
setTimeout(function(){
//applying the tooltip
$('.actions').tooltip({
title: 'Actions'
});
}, 2000);
}
};
The other solution was to call the tooltip
plugin after getting the data from the API:
//emulating the delay of calling an external API
self.getdata = function(){
setTimeout(function(){
//simulating
$.get(url, function(data){
self.orders(data);
self.loading(false);
$('.actions').tooltip({
title: 'Actions'
});
});
}, 200);
};
Is there any better approach to it?
Here is an option that mixes some of what you already know in a different way that might be more palatable.
http://jsfiddle.net/xveEP/260/
I first added a variable to the viewmodel called tooltipHandle
. This variable will be a reference (or pointer) to any setTimeout
calls that are made for applying the tooltips.
I created a custom binding for the <li>
elements rather than for the parent <ul>
element. This custom binding first clears out any existing setTimeout
instances for adding the tooltip. Then it creates a new setTimeout
instance that will run in 5ms. The function in this setTimeout
will apply the tooltip to all elements with the .actions
class that have not already had the tooltip applied to them.
In your example, there are 6 total elements added: 3 immediately and 3 200ms later. My binding will execute 6 times, however, it will only apply the tooltips twice: once for the first 3 and once for the second 3. Since I am clearing out the tooltipHandle
before it can execute the setTimeout
function, it only runs after the 3rd item for each of the 2 sets has finished binding. I added a console.log
call in the setTimeout
function so that you will see that it is only called 2 times in your previous example.
In addition the binding will execute when you click the Add button and will only apply the tooltip to the newly added element.
<ul data-bind="foreach: myItems">
<li data-bind="text: $data, doSomething2: true" class="actions"></li>
</ul>
ko.bindingHandlers.doSomething2 = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
clearTimeout(bindingContext.$parent.tooltipHandle);
bindingContext.$parent.tooltipHandle = setTimeout(function() {
$('.actions:not([data-original-title])').tooltip({
title: 'Actions'
});
window.console.log('tooltip applied');
}, 5);
}
};
function ViewModel(){
var self = this;
self.tooltipHandle = undefined;
self.myItems= ko.observableArray(['A', 'B', 'C']);
//emulating the delay of calling an external API
self.getdata = function(){
setTimeout(function(){
self.myItems.push('D');
self.myItems.push('E');
self.myItems.push('F');
}, 200);
};
self.addItem = function () {
this.myItems.push('New item');
};
//getting the data on loading
self.getdata();
}
ko.applyBindings(new ViewModel());