ptolbert
ptolbert

Reputation: 49

KnockoutJS Grouping ObservableArray entries by child Property array

I've been pondering the best way to achieve this outcome for some time and have had success really tweaking my display templates and JS, but it's not scalable.

I have an observableArray, ContentGrid[], each item in the array has multiple objects including an object Taxonomies{} which contains objects Product & Docs. Both Product and Docs contain an array of items matching their classification, for example Product contains [ProductA] and Docs:[Photos].

I'd like to be able to group results by Product A, Product B, Product C, etc, however, I can't seem to find the right syntax to make this possible.

Upvotes: 0

Views: 20

Answers (1)

user3297291
user3297291

Reputation: 23372

You can use a pureComputed that loops over the ContentGrid array to create a group per unique product.

The example below shows:

  • Create a Map to hold the groups. Its keys refer to a product and its values will be parts of your grid (i.e. partial ContentGrids)
  • Iterate over the rows of the grid using for ... of
  • Iterate over all products in the row using for ... of
  • Ensure there's an empty array (= content grid) in the Map
  • Ensure this row gets logged in this product's group

To make it easier to render the groups to the UI, I transform the Map instance in to an array of viewmodels. Each group gets a model with a

  • title referring to the product
  • rows referring to the parts of the grid that matched this product.

As you can see this grouping will duplicate rows that have multiple products. If you want to implement a different grouping strategy, you'll likely want to update the inner loop.

const contentGrid = ko.observableArray([
  { id: "Grid row 1", taxonomies: { product: [{ id: "Product C" }] } },
  { id: "Grid row 2", taxonomies: { product: [{ id: "Product B" }] } },
  { id: "Grid row 3", taxonomies: { product: [{ id: "Product A" }] } },
  { id: "Grid row 4", taxonomies: { product: [{ id: "Product A" }, { id: "Product B" }] } },
  { id: "Grid row 5", taxonomies: { product: [{ id: "Product A" }, { id: "Product B" }, { id: "Product C" }] } }
]);

// A computed list of all rows
const rowsByProduct = ko.pureComputed(() => {
  const rowsByProductId = new Map();
  
  for (const contentRow of contentGrid()) {
    for (const product of contentRow.taxonomies.product) {
      if (!rowsByProductId.has(product.id)) {
        rowsByProductId.set(product.id, []);  
      }
      
      // Add the content row for this product
      rowsByProductId.get(product.id).push(contentRow);
    }
  }

  // Format our map's entries to viewmodels
  return Array
    .from(rowsByProductId.keys())
    .sort()
    .map(productId => ({
      title: productId,
      rows: rowsByProductId.get(productId)
    }));
});

ko.applyBindings({ contentGrid, rowsByProduct })
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<h2>By product</h2>
<ul data-bind="foreach: rowsByProduct">
  <li>
    <p data-bind="text: title"></p>
    <ul data-bind="foreach: rows">
      <li data-bind="text: id"></li>
    </ul>
  </li>
</ul>

Upvotes: 1

Related Questions