Search code examples
jsonknockout.jsknockout-mapping-plugin

How do I use knockout to bind nested foreach loops on dynamically added properties?


I have a collection of items. The items are broken down into "types" and then further divided within the type into "categories". I do not know the names of the "types" or the "categories" before hand.

I would like to do some nested foreach binding to represent the data hierarchically. Something like this:

<ul data-bind="foreach: OrderItems.Types">
<li>
    ItemType: <span data-bind='text: $data'></span>
    <ul data-bind="foreach: Categories">
      <li>
        Category: <span data-bind='text: $data'></span>
        <ul data-bind="foreach: OrderItems">
          <li>
            Item: <span data-bind="text: Name"> </span>
          </li>
        </ul>              
      </li>
    </ul>
</li>

var order = {
"OrderNumber": "394857",
"OrderItems": {
    "Types": {
        "Services": {
            "Categories": {
                "carpet cleaning": {
                    "OrderItems": [
                        {
                            "OrderItemID": "9d398f88-892c-11e3-8f31-18037335d26a",
                            "Name": "ARug-Oriental Rugs (estimate on site)"
                        },
                        {
                            "OrderItemID": "9d398f53-892c-11e3-8f31-18037335d26a",
                            "Name": "C1-Basic Cleaning  (per room)"
                        },
                        {
                            "OrderItemID": "9d398f54-892c-11e3-8f31-18037335d26a",
                            "Name": "C2-Clean & Protect  (per room)"
                        },
                        {
                            "OrderItemID": "9d398f55-892c-11e3-8f31-18037335d26a",
                            "Name": "C3-Healthy Home Package (per room)"
                        }
                    ]
                },
                "specialty": {
                    "OrderItems": [
                        {
                            "OrderItemID": "9d398f8f-892c-11e3-8f31-18037335d26a",
                            "Name": "SOTHR-Other"
                        }
                    ]
                },
                "tile & stone": {
                    "OrderItems": [
                        {
                            "OrderItemID": "9d398f8e-892c-11e3-8f31-18037335d26a",
                            "Name": "TILE-Tile & Stone Care"
                        }
                    ]
                },
                "upholstery": {
                    "OrderItems": [
                        {
                            "OrderItemID": "9d398f7b-892c-11e3-8f31-18037335d26a",
                            "Name": "U3S1-Upholstery - Sofa (Seats 3: 7 linear feet)"
                        },
                        {
                            "OrderItemID": "9d398f7c-892c-11e3-8f31-18037335d26a",
                            "Name": "U3S2-Upholstery - Sofa - Clean & Protect (Seats 3: 7 linear feet"
                        }
                    ]
                }
            }
        },
        "Products": {
            "Categories": {
                "carpet cleaning": {
                    "OrderItems": [
                        {
                            "OrderItemID": "9d398f84-892c-11e3-8f31-18037335d26a",
                            "Name": "PLB-Leave Behind Item"
                        }
                    ]
                }
            }
        }
    }
}
};

var viewModel = ko.mapping.fromJS(order);
ko.applyBindings(viewModel);

here's a fiddle with the above code: http://jsfiddle.net/mattlokk/6Q5f7/5/


Solution

  • To bind against your structure, you would need to turn the objects into arrays. Given that you are using the mapping plugin, the easiest way would likely be to use a binding that translates an object with properties to an array of key/values.

    Here is a sample binding:

    ko.bindingHandlers.objectForEach = {
        init: function(element, valueAccessor, allBindings, data, context) {
            var mapped = ko.computed({
                read: function() {
                    var object = ko.unwrap(valueAccessor()),
                        result = [];
    
                    ko.utils.objectForEach(object, function(key, value) {
                        var item = {
                            key: key,
                            value: value
                        };
    
                        result.push(item);
                    });
    
    
                    return result;
                },
                disposeWhenNodeIsRemoved: element
            });
    
            //apply the foreach bindings with the mapped values
            ko.applyBindingsToNode(element, { foreach: mapped }, context);
    
            return { controlsDescendantBindings: true };   
        }
    };
    

    This will create a computed on-the-fly that maps the object to an array of key/values. Now you can use objectForEach instead of foreach against your objects.

    Here is a basic sample: http://jsfiddle.net/rniemeyer/nn3jg/ and here is an example with your fiddle: http://jsfiddle.net/rniemeyer/47Wbe/