Reputation: 1618
Is there a way to manually increment a ViewModel property from within a Knockout foreach
binding?
I want to do something logically equivalent to:
var inc = 0;
for (var i = 0; i < 3; i++)
{
for (var j = 0; j < 3; j++)
{
inc++;
alert(inc + ': ' + i + ',' + j); // but write this to the dom
}
}
I have tried to use $index
, but that just relates to the current loop, not the inner or parent loop.
The arrays may be different sizes, so I cannot simply calculate the $parentContext.$index * [count of child items] + current $index
.
What I am trying to achieve:
<div data-bind="foreach: Categories">
<div data-bind="foreach: SubCategories" >
<div data-bind="text: [someIncrementer] +': ' + $parent.Category + ',' + $data></div>
<!-- should output:
1: A,1
2: A,2
3: A,3
4: B,1
5: B,3
-->
</div>
</div>
var ViewModel = function()
{
var self = this;
self.Categories = ko.observableArray([
{Category: 'A', SubCategories: [1,2,3]},
{Category: 'B', SubCategories: [1,3]}
]);
}
Upvotes: 3
Views: 2768
Reputation: 23372
Another alternative:
Like you said, the knockout binding context exposes an $index
observable that holds the index in the loop. There's also the $parentContext
to refer to the context that's higher up in the chain.
For a simple, 2 level nested loop, you could create a helper method that uses the current $index
and the parent's $index
to calculate the total index:
// cIndex = current category's index
// sIndex = current subcategory's index
self.getSubIndex = function(cIndex, sIndex) {
return sIndex + self.Categories
.slice(0, cIndex)
.reduce(function(count, c) {
return count + c.SubCategories.length;
}, 0);
}
It'll be a bit annoying to write though:
<span data-bind="text: $root.getSubIndex($parentContext.$index(), $index())"> </span>
You could also pass $context
and get the indexes inside the getSubIndex
method. You could even go through the context chain recursively until there's no more $index
property to be found.
Personally, I'd rather create an index
computed in the SubCategory
viewmodels, which would feel a bit less hacky. However, it would require the SubCategory
to have access to its parent...
var ViewModel = function() {
var self = this;
self.Categories = ko.observableArray([{
Category: 'A',
SubCategories: [1, 2, 3]
}, {
Category: 'B',
SubCategories: [1, 3]
}]);
self.getSubIndex = function(cIndex, sIndex) {
return sIndex + self.Categories
.slice(0, cIndex)
.reduce(function(count, c) {
return count + c.SubCategories.length;
}, 0);
}
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: Categories">
<!-- ko foreach: SubCategories -->
<li>
<span data-bind="text: $root.getSubIndex($parentContext.$index(), $index()) + ': ' + $parent.Category + ', ' + $data"></span>
</li>
<!-- /ko -->
</ul>
Upvotes: 0
Reputation: 338326
You could use a <ol>
/ <li>
with a numerical counter or a CSS counter.
This works if the number is purely for display purposes and nothing depends on it.
var ViewModel = function()
{
var self = this;
self.Categories = ko.observableArray([
{Category: 'A', SubCategories: [1,2,3]},
{Category: 'B', SubCategories: [1,3]},
{Category: 'C', SubCategories: [7,8,9]},
{Category: 'D', SubCategories: [10,11,12,13]}
]);
}
ko.applyBindings(new ViewModel());
.categories {
counter-reset: category;
}
.subcategory::before {
counter-increment: category;
content: counter(category) ": ";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="categories" data-bind="foreach: Categories">
<div data-bind="foreach: SubCategories">
<div>
<span class="subcategory" data-bind="text: $parent.Category + ',' + $data"></span>
</div>
</div>
</div>
Upvotes: 1
Reputation: 638
No need to make this any more complicated than it has to be. You could try something like this:
var ViewModel = function() {
var self = this;
var index = 0;
self.inc = function(val) {
return ++index;
}
self.Categories = ko.observableArray([{
Category: 'A',
SubCategories: [1, 2, 3]
}, {
Category: 'B',
SubCategories: [1, 3]
}]);
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: Categories">
<!-- ko foreach: SubCategories -->
<li>
<span data-bind="text: $root.inc() + ': ' + $parent.Category + ', ' + $data"></span>
</li>
<!-- /ko -->
</ul>
Upvotes: 3
Reputation: 63800
Create a computed observable with the flattened array for that:
var ViewModel = function() {
var self = this;
self.Categories = ko.observableArray([
{Category: 'A', SubCategories: [1,2,3]},
{Category: 'B', SubCategories: [1,3]}
]);
self.FlattenedSubcategories = ko.computed(function() {
var result = [], cats = self.Categories(), index = 1;
for (var i = 0; i < cats.length; i++) {
for (var j = 0; j < cats[i].SubCategories.length; j++) {
result.push({
Idx: index++,
Category: cats[i].Category,
Subcat: cats[i].SubCategories[j]
});
}
}
return result;
});
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<div data-bind="foreach: FlattenedSubcategories">
<div>
<span data-bind="text: Idx"></span>:
<span data-bind="text: Category"></span>,
<span data-bind="text: Subcat"></span>
</div>
</div>
Upvotes: 2