5

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?

2 Answers 2

7

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

Sign up to request clarification or add additional context in comments.

6 Comments

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?
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".
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! :)
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
@CaptainFantastic the plunker link still works for me. Maybe try forcing it to open in the legacy version? plnkr.co/edit/xKSwHAUdXcGZcwHTDmiv?plnkr=legacy&p=preview
|
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.

2 Comments

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.
Good luck and write your thoughts! Using it almost for a year, seems ok.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.