Take the 2-minute tour ×
Stack Overflow is a question and answer site for professional and enthusiast programmers. It's 100% free, no registration required.

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?

share|improve this question
add comment

2 Answers

up vote 2 down vote accepted

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

share|improve this answer
    
The items trick is great, and solves the "siblings" problems (add sibling, count siblings, delete sibling). Thanks! And I had already solved the "children" problems the way you did. But I don't see how your solution helps with the "parent" problems. Eg how do I find parent, or find the nest level, or insert an item as, say, an "aunt"? Did I miss something? –  poshest Apr 28 at 21:42
    
you could save off the parent with ng-repeat="item in items = (parent = item).items". then use ng-init to add the parent to the item: ng-init="item.parent = parent". –  j.wittwer Apr 28 at 23:33
    
Ha! This solution is as incredible and crazy as gorpacrate's solution. I'm going with this one for now because it's more convenient - Angular can manage the changes to the structure (moves of objects from one place to another, etc). Great work thanks! :) –  poshest May 1 at 23:42
    
If anyone's using this with an ng-repeat filter on the items, don't forget that items when passed into function calls is the PRE-filtered. I assumed the opposite => couple of hours of hurt –  poshest May 1 at 23:46
add comment

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.

share|improve this answer
    
This structure you created is beautiful... and frightening. I had no idea you could do this without big recursion issues. Thanks for pointing out the one you found - I don't yet have a need to clone my structure. I will experiment with this in my project and see what other demons appear. –  poshest Apr 28 at 21:50
    
Good luck and write your thoughts! Using it almost for a year, seems ok. –  gorpacrate Apr 29 at 3:37
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.