AngularJS: traversing nested arrays

All we need is an easy explanation of the problem, so here it is.

I am working with a nested array with the structure…

$scope.items = [{attr1: val1, 
  attr2: val2,
  items: [{
     attr1: val1, 
     attr2: val2,
     items: [{
     ... 
     }, ...]
  }, ...]
}, ...];

which goes into an ng-repeat with ng-include like this

<div ng-repeat="item in items" ng-include="'/path/to/template.tpl.html'"></div>

and template.tpl.html is

<div>{{item.attr1}}<\div>
<div>{{item.attr2}}<\div>
<div ng-click="fnAddNewItemBelow(item, $parent)"><\div>
<div ng-repeat="item in item.items" ng-include="'/path/to/template.tpl.html'"><\div>

Now, in the controller, I commonly want to do things like

  • find an item’s parent
  • find an item’s sibling
  • make counts of siblings
  • find out how many levels deep an item is nested
  • insert or delete items at any level of the nest

But I’m not sure how to do this elegantly. Eg imagine I wanted to implement fnAddNewItemBelow. The two options I can work out are

Traverse scopes

Use the nested scopes structure that Angular provides

// pseudo-code only
$scope.fnAddNewItemBelow = function (item, parent) {
  var newItem = ...;

  // add newItem as a sibling after the item that was ng-clicked
  // parent.$parent is necessary because the ng-include adds another scope layer (I think)
  parent.$parent.item.items.push(newItem);

  // (probably need to use .splice in case there are items after item, 
  //  but I'm keeping it simple)
}

But this is ugly because it assumes too much about the structure (what if I put an ng-if onto the <div ng-click..., which added another scope level… then I’d need parent.$parent.$parent.item.items.push(newItem)).

Iterate nested array recursively until item.id is found

The alternative is to operate directly on $scope.items, since Angular will update UI and scopes associated with it. I can iterate recursively through $scope.items using for loops and after locating item by some unique id that it has, insert newItem after it

// pseudo-code only
$scope.fnAddNewItemBelow = function (item) {
  var newItem = ...;

  // add newItem as a sibling after the item that was ng-clicked
  fnSomeFunctionToFindItemAndInsertItemAfterIt(item.id, newItem);
}

fnSomeFunctionToFindItemAndInsertItemAfterIt (itemId, newItem) {
  // fancy recursive function that for loops through each item, and calls 
  // itself when there are children items. When it finds item with itemId, it 
  // splices in the newItem after
}

I don’t like this because it requires iterating through the entire items tree every time I want to do something with the nested array.

Are there more elegant solutions?

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

If you alias item.items in the ng-repeat expression, angular will keep track of the array structure and hierarchical relationships for you.

<div ng-repeat="item in items = item.items">

Then, operations on the tree can simply pass in the item, the $index, or the array of items – without knowledge of the full array structure:

  <button ng-click="addItem(item)">Add to my items</button>
  <button ng-click="addSiblingItem(items, $index)">Add a sibling item</button>
  <button ng-click="deleteMe(items, $index)">Delete Me</button>

js:

$scope.addItem = function(item) {
  item.items.push({
    attr1: 'my new - attr1',
    attr2: 'my new - attr2',
    items: []
  });
}
$scope.addSiblingItem = function(items, position) {
  items.splice(position + 1, 0, {
    attr1: 'sibling - new attr1',
    attr2: 'sibling - new attr2',
    items: []
  });
}
$scope.deleteMe = function(items, position) {
  items.splice(position, 1);
}

To get the number of siblings, you can refer to items.length:

<h3>Item #{{$index + 1}} of {{items.length}}</h3>

If you really need to access the parent siblings from child items, you can add another alias for parent = item and add it to the item using ng-init:

ng-repeat="item in items = (parent = item).items" ng-init="item.parent = parent"

Then you have access to the grandparent (parent.parent) and its items (the parent siblings).

In addition, you can keep track of the current nest level using ng-init:

ng-init="item.parent = parent; item.level = parent.level + 1"

Here is a working demo: http://plnkr.co/xKSwHAUdXcGZcwHTDmiv

Method 2

Before rendering data, you can make some preparations. One recursive run over your data to set level value and a link to the parent to each item.
Example with your data using LoDash:

var level = 0;
_.each($scope.items, function(item){recursive(item, level)});

function recursive(item, level){
    item.level = level;
    _.each(item.items, function(innerItem){
        innerItem.parent = item;
        recursive(innerItem, level+1);
    });
}

So now you can easily get parent and siblings of each item.

find an item’s parent -> item.parent

find an item’s sibling -> item.parent.items[i]

make counts of siblings -> item.parent.items.length

find out how many levels deep an item is nested -> item.level

insert or delete items at any level of the nest (move operation example) ->

newParent.items.push(item);
_.remove(item.parent.items, function(child){return child == item;});

The only minus of this approach which i met – you can not easily clone whole tree without going into endless recursion. But you can make custom cloning function which will not copy links.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply