I have a html section element which has a Knockout foreach binding to a collection of items on my viewmodel. This works fine rendering each item of the collection onto a div vertically down the page. I now want the items to group themselves in rows based on window size, so items appear in say, rows of 4 on a desktop browser but only 1 per row on mobile.
I did actually achieve this by creating the groups in the viewmodel and having my view element bind with a foreach to this groups property. The problem with this approach is that my viewmodel now has what I would consider a bunch of view logic in it and references the window object directly. Which is not right I don't think.
I already have a separate js file which has view specific logic in it, namely custom Knockout bindings for things like 'slideVisible'. How can I move the grouping logic out of my viewmodel and into this file? I'm guessing I won't be able to use Knockout's foreach binding if the grouping isn't done in the viewmodel?
If you need to do this dynamically in KO, then here is an example of a binding that wraps the normal foreach
binding and creates a computed on the fly that returns a structure with rows/columns based on a "count" observable.
ko.bindingHandlers.foreachGroups = {
init: function(element, valueAccessor) {
var groupedItems,
options = valueAccessor();
//create our own computed that transforms the flat array into rows/columns
groupedItems = ko.computed({
read: function() {
var index, length, group,
result = [],
count = +ko.utils.unwrapObservable(options.count) || 1,
items = ko.utils.unwrapObservable(options.data);
//create an array of arrays (rows/columns)
for (index = 0, length = items.length; index < length; index++) {
if (index % count === 0) {
group = [];
result.push(group);
}
group.push(items[index]);
}
return result;
},
disposeWhenNodeIsRemoved: element
});
//use the normal foreach binding with our new computed
ko.applyBindingsToNode(element, { foreach: groupedItems });
//make sure that the children of this element are not bound
return { controlsDescendantBindings: true };
}
};
You would use it like:
<div data-bind="foreachGroups: { data: items, count: count }">
<ul data-bind="foreach: $data">
<li data-bind="text: $data"></li>
</ul>
</div>
Here is a sample: http://jsfiddle.net/rniemeyer/F48XU/
For your specific case though, I would probably:
- remove the
count
option and just pass in the items
- create your own
count
observable in the init
function.
- add a
resize
event handler that runs your logic and updates the count
observable appropriately.
It might look like (fill in your specific resize logic):
ko.bindingHandlers.foreachGroups = {
init: function(element, valueAccessor) {
var groupedItems,
data = valueAccessor(),
count = ko.observable(1);
ko.utils.registerEventHandler(window, "resize", function() {
//run your calculation logic here and update the "count" observable with a new value
});
//create our own computed that transforms the flat array into rows/columns
groupedItems = ko.computed({
read: function() {
var index, length, group,
result = [],
itemsPerRow = +ko.utils.unwrapObservable(count) || 1,
items = ko.utils.unwrapObservable(data);
//create an array of arrays (rows/columns)
for (index = 0, length = items.length; index < length; index++) {
if (index % itemsPerRow === 0) {
group = [];
result.push(group);
}
group.push(items[index]);
}
return result;
},
disposeWhenNodeIsRemoved: element
});
//use the normal foreach binding with our new computed
ko.applyBindingsToNode(element, { foreach: groupedItems });
//make sure that the children of this element are not bound
return { controlsDescendantBindings: true };
}
};