What I'm trying to achieve is to visually filter table rows generated by the foreach
binding in a way that tr
elements of the rows that are filtered out would be hidden instead of removed from the DOM.
This approach significantly improves rendering performance when a user changes filter conditions. This is why I don't want the foreach
to be bound to a computed observable array which is updated depending on the filter condition.
I want this solution to be a ready-to-use building block that I can use in other places of the project.
As far as I'm familiar with Knockout, the best way is to implement a custom binding.
The way I intended to use this binding is something like this:
<tbody data-bind="foreach: unfilteredItems, visibilityFilter: itemsFilter">
<tr>
...
</tr>
</tbody>
where itemsFilter
is a function returning boolean
depending of whether current row should be visible or not, like this:
self.itemsFilter = function (item) {
var filterFromDate = filterFromDate(), // Observable
filterDriver = self.filterDriver(); // Observable too
return item && item.Date >= filterFromDate && (!filterDriver || filterDriver === item.DriverKey);
};
Here is the binding implementation that I have so far:
/*
* Works in conjunction with the 'foreach' binding and allows to perform fast filtering of generated DOM nodes by
* hiding\showing them rather than inserting\removing DOM nodes.
*/
ko.bindingHandlers.visibilityFilter = {
// Ugly thing starts here
init: function (elem, valueAccessor) {
var predicate = ko.utils.unwrapObservable(valueAccessor());
predicate();
},
// Ugly thing ends
update: function (elem, valueAccessor) {
var predicate = ko.utils.unwrapObservable(valueAccessor()),
child = ko.virtualElements.firstChild(elem),
visibleUpdater = ko.bindingHandlers.visible.update,
isVisible,
childData,
trueVaueAccessor = function () { return true; },
falseVaueAccessor = function () { return false; };
while (child) {
if (child.nodeType === Node.ELEMENT_NODE) {
childData = ko.dataFor(child);
if (childData) {
isVisible = predicate(childData, child);
visibleUpdater(child, isVisible ? trueVaueAccessor : falseVaueAccessor);
}
}
child = ko.virtualElements.nextSibling(child);
}
}
};
ko.virtualElements.allowedBindings.visibilityFilter = true;
Do you see that ugly init
part with predicate invocation without passing an object to it?
Without this, if there is no rows generated by the foreach
binding by the first time Knockout calls the update
method, itemsFilter
filter function wouldn't be called.
Hence, no observables would be read and KO dependency tracking mechanism decides that this binding doesn't depend on any observables in my view model.
And when values of the filter observables (filterFromDate
and filterDriver
) get changed, the update
will never be called again and the whole filtering doesn't work.
How can I improve this implementation (or the whole approach to the problem) in order to not make that ugly call to the filter function which at least makes the function await an undefined
value as a paramenter?
You can use a visible
binding on the tr
and bind it to the result of a function call using $data
as the parameter. A little demo below. Whichever value you select is filtered out of the table.
var vm = {
rows: ko.observableArray(['One', 'Two', 'Three']),
selected: ko.observable('One'),
isVisible: function(row) {
return row !== vm.selected();
}
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="options:rows, value:selected"></select>
<table border="1" data-bind="foreach:rows">
<tr data-bind="visible:$parent.isVisible($data)">
<td data-bind="text:$data"></td>
</tr>
</table>