Creating groups with Knockout.js foreach

2020-07-27 02:59发布

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?

1条回答
疯言疯语
2楼-- · 2020-07-27 03:37

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