Darksbane Darksbane - 16 days ago 6
Javascript Question

KnockoutJS showing a sorted list by item category

I just started learning knockout this week and everything has gone well except for this one issue.

I have a list of items that I sort multiple ways but one of the ways I want to sort needs to have a different display than the standard list. As an example lets say I have this code

var BetterListModel = function () {
var self = this;
food = [
{
"name":"Apple",
"quantity":"3",
"category":"Fruit",
"cost":"$1",
},{
"name":"Ice Cream",
"quantity":"1",
"category":"Dairy",
"cost":"$6",
},{
"name":"Pear",
"quantity":"2",
"category":"Fruit",
"cost":"$2",
},{
"name":"Beef",
"quantity":"1",
"category":"Meat",
"cost":"$3",
},{
"name":"Milk",
"quantity":"5",
"category":"Dairy",
"cost":"$4",
}];
self.allItems = ko.observableArray(food); // Initial items
// Initial sort
self.sortMe = ko.observable("name");
ko.utils.compareItems = function (l, r) {
if (self.sortMe() =="cost"){
return l.cost > r.cost ? 1 : -1
} else if (self.sortMe() =="category"){
return l.category > r.category ? 1 : -1
} else if (self.sortMe() =="quantity"){
return l.quantity > r.quantity ? 1 : -1
}else {
return l.name > r.name ? 1 : -1
}

};
};
ko.applyBindings(new BetterListModel());


and the HTML


<p>Your values:</p>
<ul class="deckContents" data-bind="foreach:allItems().sort(ko.utils.compareItems)">
<li><div style="width:100%"><div class="left" style="width:30px" data-bind="text:quantity"></div><div class="left fixedWidth" data-bind="text:name"></div> <div class="left fixedWidth" data-bind="text:cost"></div> <div class="left fixedWidth" data-bind="text:category"></div><div style="clear:both"></div></div></li>
</ul>
<select data-bind="value:sortMe">
<option selected="selected" value="name">Name</option>
<option value="cost">Cost</option>
<option value="category">Category</option>
<option value="quantity">Quantity</option>
</select>
</div>


So I can sort these just fine by any field I might sort them by name and it will display something like this

3 Apple $1 Fruit
1 Beef $3 Meat
1 Ice Cream $6 Dairy
5 Milk $4 Dairy
2 Pear $2 Fruit


Here is a fiddle of what I have so far http://jsfiddle.net/Darksbane/X7KvB/

This display is fine for all the sorts except the category sort. What I want is when I sort them by category to display it like this

Fruit
3 Apple $1 Fruit
2 Pear $2 Fruit

Meat
1 Beef $3 Meat

Dairy
1 Ice Cream $6 Dairy
5 Milk $4 Dairy


Does anyone have any idea how I might be able to display this so differently for that one sort?

Answer
  • Your view shouldn't contain logic beyond that necessary to render it. Thus, your foreach binding data-bind="foreach:allItems().sort(ko.utils.compareItems)" should become a computed observable.

  • You should move the <option> data into your model and take advantage of the options data-bind.

To address the actual question, you'll take advantage of template binding and containerless if binding.

The template binding will allow you to change the look/feel of the view based on the selected sort type. So 2 templates are available, the default-template which handles the regular display and the category-template specifically for category based rendering.

<script type="text/html" id="category-template">
 <ul class="deckContents" data-bind="foreach:sortedItems">
     <li>
         <!-- ko if: $root.outputCategory($index()) -->
         <div data-bind="text:category"></div>
         <!-- /ko -->
          <span class="indented" data-bind="text:name"></span> 
          <span  class="indented" data-bind="text:cost"></span> 
          <span  class="indented" data-bind="text:category"></span>
     </li>
 </ul>

The html usage: <div data-bind='template: { name: currentTemplate, data: $data}'></div> where currentTemplate is an computed observable that returns the template id based on sort type.

In some way or another you must assign priority to the categories. I have done this by declaring var categoryPriority = ["Fruit", "Meat", "Dairy"].

Have a look at my fiddle. I didn't address the fixedWidth used by the default-template so you'll need to handle the CSS styling to line it up the way you want.

Edit: Is there a way to dynamically add an item to the list and have it show up in the sorted list automatically?

  1. Pushing a new item: When you want knockout to "notify" other elements then you don't want to read the observableArray by using allItems() before performing the push. Instead, you'll push into the observableArray using allItems.push which in-turn will cause knockout to trigger computed observables (that depend on this observable) to evaluate, subscriptions to execute, DOM elements to update ... etc.

  2. Computed Dependencies: In order for a computed to "depend" on another observable it has to be read inside of the provided evaluator function. Since, sortedItems only reads sortType that is the only "trigger" for re-evaluation. Thus, changing the allItems.sort to allItems().sort causes sortedItems to evaluate whenever changes are made to allItems.

See How Dependency Tracking Works