Reputation: 205
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?
Upvotes: 1
Views: 1478
Reputation: 1282
By using sort method we can sort the list by ascending order.
I am taking your example and explain 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);
Here you have an array. By using below code to sort this list
self.allItems.sort(function (left, right) {
return left.name() == right.name() ? 0 : (left.name() < right.name() ? -1 : 1)
});
like wise if u want to sort by cost ..is the same..Replace name to cost..u will get it.
Upvotes: 0
Reputation: 3118
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?
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.
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
Upvotes: 5