AndrewP
AndrewP

Reputation: 1618

Incrementing a counter in nested foreach loop

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

Answers (4)

user3297291
user3297291

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

Tomalak
Tomalak

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

Stuart Bourhill
Stuart Bourhill

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

Jeroen
Jeroen

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

Related Questions