3

I'm in angular and i have a object like this.

var items = [{
    title: 'Something',
    children: [
        { title: 'Hello World' },
        { title: 'Hello Overflow' },
        { title: 'John Doe', children: [
            { title: 'Amazing title' },
            { title: 'Google it' },
            { title: 'I'm a child', children: [
                { title: 'Another ' },
                { title: 'He\'s my brother' },
                { title: 'She\'s my mother.', children: [
                    {title: 'You never know if I'm going to have children'}
                ]}
            ]}
        ]}
    ]
}];

I wan't to loop through all of these so i have something like this.

    • Something

       • Hello World

       • Hello Overflow

       • John Doe

          • Amazing Title

          • Google it

          • I'm a child

              • Another

              • He's my brother

              • She's my mother

                  • You never know if I'm going to have children

The problem is I wouldn't know how deep this object will go or what's in it. so I wouldn't be able to do it manually. I have done a basic loop with ng-repeat in the fiddle provided at the bottom, but i can't figure out how I can automatically loop through these and create nested <ul>'s and <li>'s.

What would be the best way to accomplish this?

Demo: http://jsfiddle.net/XtgLM/

Community
  • 1
  • 1
iConnor
  • 19,997
  • 14
  • 62
  • 97

3 Answers3

9

You don't need to make a custom directive, what you want is to use an inline template that calls it's self.

I forked your fiddle.

http://jsfiddle.net/MasterMorality/E99Gh/2/

basically it looks like this:

<script type='text/ng-template' id="item.html">
    ...
    <div ng-repeat="x in x.childrens" ng-include="'item.html'"></div>
</script>
...
<div ng-repeat="x in things" ng-include="'item.html'"></div>

I should note that you are not actually overwriting x, since angular creates a new scope for each repeated item.

Master Morality
  • 5,837
  • 6
  • 31
  • 43
8

Here you go:

html

<div ng-app="app" ng-controller="test">
   <ul>
       <li nested-item ng-repeat="item in items">{{item.title}}</li>
   </ul>         
</div>

JavaScript

var items = [{
    title: 'Something',
    children: [
        { title: 'Hello World' },
        { title: 'Hello Overflow' },
        { title: 'John Doe', children: [
            { title: 'Amazing title' },
            { title: 'Google it' },
            { title: 'Im a child', children: [
                { title: 'Another ' },
                { title: 'He\'s my brother' },
                { title: 'She\'s my mother.', children: [
                    {title: 'You never know if im going to have children'}
                ]}
            ]}
        ]}
    ]
}];

var app = angular.module('app', []);

app.controller('test', function( $scope ) {
    $scope.items = items;
});


app.directive('nestedItem', ['$compile', function($compile){
    return {
        restrict: 'A',
        link: function(scope, element){
        console.log(element);
            if (scope.item.children){
                var html = $compile('<ul><li nested-item ng-repeat="item in item.children">{{item.title}}</li></ul>')(scope);
                element.append(html);
            }
        }
    };
}]);

I forked your fiddle:

http://jsfiddle.net/c4Kp8/

Actually I must confess that I like Master Morality's approach but you can also go with a custom directive. The key thing to know if you go that route is that you need to intercept on the item level to manually check if the current item has children, and if so, $compile the directive for the node yourself.

UPDATE

However, there is one thing that should bother us in the above code. The duplication of html code (inlined in the directive) is a code smell. If you like, you can get really funky and fix this by introducing a generic template-code directive which doesn't do anything else but providing the code of the node where it is applied on as a template for other directives.

So then our solution would look like this:

html

<div ng-app="app" ng-controller="test">
   <ul template-code>
       <li nested-item ng-repeat="item in items">{{item.title}}</li>
   </ul>         
</div>

JavaScript

var items = [{
    title: 'Something',
    children: [
        { title: 'Hello World' },
        { title: 'Hello Overflow' },
        { title: 'John Doe', children: [
            { title: 'Amazing title' },
            { title: 'Google it' },
            { title: 'Im a child', children: [
                { title: 'Another ' },
                { title: 'He\'s my brother' },
                { title: 'She\'s my mother.', children: [
                    {title: 'You never know if im going to have children'}
                ]}
            ]}
        ]}
    ]
}];

var app = angular.module('app', []);

app.controller('test', function( $scope ) {
    $scope.items = items;
});

app.directive('templateCode', function(){
    return {
        restrict: 'A',
        controller: function(){},
        compile: function(element){
            element.removeAttr('template-code');
            //ATTENTION: We need to trim() here. Otherwise AngularJS raises an exception
            //later when we want to use the templateCode in a $compile function. 
            //Be aware that we assume a modern browser 
            //that already ships with a trim function.
            //It's easy to secure that with a polyfill.
            var templateCode = element.parent().html().trim();
            return function(scope, iElement, iAttrs, controller){
                controller.templateCode = templateCode;
            }
        }
    }
});

app.directive('nestedItem', ['$compile', function($compile){
    return {
        restrict: 'A',
        require: '^templateCode',
        link: function(scope, element, iAttr, controller){ 
            if (scope.item.children){
                scope.items = scope.item.children;         
                var html = $compile(controller.templateCode)(scope);
                element.append(html);
            }
        }
    };
}]);

Plunker: http://jsfiddle.net/2rQWf/

Christoph
  • 26,519
  • 28
  • 95
  • 133
  • Thanks for you answer, it's great I am just thinking about which approach would be better. It's hard to tell, but i think this approach might be doing a lot less work than the other one behind the scenes – iConnor Aug 23 '13 at 18:28
  • The only thing that bugs me about this approach is the duplication of html (the inlined html in the directive). However, I think there's something that can be done about that. I might post an update later. – Christoph Aug 23 '13 at 18:56
0

You're probably going to need to create your own directive passing in the object to iterate over. Put a watch on the object passed in and when that fires run some recursive function that appends elements to the element that the directive is on.

You can get at the DOM element from the element parameter on the directive link function. You can obviously append DOM elements using that same element parameter.

BoxerBucks
  • 3,124
  • 2
  • 21
  • 26