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

I'm learning angularjs and I'm training how to build a reusable directive.

The problem is that it works with an array with 1 element but not with 2 or more.

The html tag is just: <breadcrumb></breadcrumb> which in case, render as expected. But, I need to do manually what "replace:true" would do.

The error is: parent is null.

I exhausted all google search looking for an example.

But my case is peculiar, because there is an <inner ng-repeat> inside <breadcrumb> which is an another inner-directive instead an normal tag, that's why replace doesn't work and I need to do manually.

There is a problem related to "dynamic template load..."

obs: I tried to do in both "link:" and "compile:" the logic, same error...

[EDIT] I could make the code work, but, I'm unable to remove <inner> tag as transclude would do automatic.

Now the output is almost perfect, just need to remove the <inner> but no luck untill now, I tried to replace the element tag, still no luck.

<ul class="breadcrumb">

    <!-- I want to remove this <inner> and leave <li> alone! -->
    <inner data-ng-repeat="_item in items track by $index" class="ng-scope">
        <li class="ng-scope"><a href="#" class="ng-binding">1</a>
        </li>
    </inner>

    <!-- remove for clarity -->
</ul>

var myModule = angular.module("myModule", []);

myModule.directive('breadcrumb', function($timeout) {

    "use strict";

    var directiveDefinitionObject = {
        template: '<ul class="breadcrumb"><inner data-ng-repeat="_item in items track by $index"></inner></ul>',
        replace: true,
        // transclude: true,
        restrict: 'E',
        scope: {},
        controller: ["$scope", "$element", "$attrs", "$transclude", controller],
        link: ["scope", "iElement", "iAttrs", link]
    };

    function link(scope, iElement, iAttrs) {
        scope.addNewItem = function(new_item) {
            scope._push(new_item);
        }
    }

    function controller($scope, $element, $attrs, $transclude) {
        $scope.items = [1, 2, 3, 4, 5];

        $scope._push = function(item) {
            $scope.items.push(item);
        };

        $scope._pop = function() {
            $scope.items.pop();
        };

        $scope.is_last = function(item) {
            return $scope.items.indexOf(item) == ($scope.items.length - 1);
        }
    }

    return directiveDefinitionObject;
});

myModule.directive("inner", ["$compile",
    function($compile) {

        "use strict";

        function getItemTemplate(index) {
            return '<li><a href="#">{{ _item }}</a></li>';
        }

        return {
            require: "^breadcrumb",
            restrict: "E",                  
            compile: function compile(tElement, tAttrs) 
            { 
                return function postLink(scope, iElement, iAttrs) 
                {
                    iElement.html(getItemTemplate(0));

                    $compile(iElement.contents())(scope);
                };
            }
        };
    }
]);
share|improve this question
 
Can you setup a jsfiddle or plunkr to show this in action. –  shaunhusain Jul 19 at 17:17
 
I tried this out with a fiddle here jsfiddle.net/SwpzE and it appears the issue is probably happening because your code is replacing the element that has the ng-repeat directive on it, then the next time it tries to do the same thing that element is already replaced. I think you just need to re-think how to do this a bit. –  shaunhusain Jul 19 at 18:39
 
@shaunhusain Good point. I will check this ng-repeat overwrite. Btw, I have just updated the code. But still I cannot get rid of <inner> tag to be replaced by the template (which will be dynamic). –  Ismael Jul 19 at 18:43
 
@shaunhusain I guess you're right about my removal of ng-repeat. That's why it fails on the second element array. Now I just need to learn how to work around this. –  Ismael Jul 19 at 19:05
 
You just need to set 'replace: true' at your inner directive too. –  CaioToOn Jul 19 at 22:20
show 1 more comment

2 Answers

up vote 1 down vote accepted

You can just remove the compile function of yours in inner directive and set replace: true, because it's just mocking the default behavior of replace, as you stated. So you inner directive would become:

myModule.directive("inner", ["$compile",
  function($compile) {

    "use strict";

    return {
      replace: true
      require: "^breadcrumb",
      restrict: "E",
      template: '<li><a href="#">{{ _item }}</a></li>'
    };
  }
]);

But you have a problem that your items array is defined into your breadcrumb directive, what is wrong and will not let you make it reusable. You could bind it at scope definition with something like this:

<breadcrumb items="someItemsArrayFromParentScope"></breadcrumb>
...directive('breadcrumb', function() {
   ...

   return {
     ...
     scope: {
       items: '='
     }
   }
});

This would create a two directional binding between the array from parent and internal widget scope. But going further, you might want to let the user define the inner elements of the breadcrumb, it would be as following:

myModule.directive('breadcrumb', function($timeout) {
  var directiveDefinitionObject = {
    template: '<ul class="breadcrumb" ng-transclude></ul>',
    replace: true,
    transclude: true,
    restrict: 'E',
    scope: {},
    controller: ["$scope", "$element", "$attrs", "$transclude", controller]
  };

  function controller($scope, $element, $attrs, $transclude) {
    $scope.addNewItem = function(new_item) {
        $scope._push(new_item);
    }

    $scope._push = function(item) {
        $scope.items.push(item);
    };

    $scope._pop = function() {
        $scope.items.pop();
    };

    $scope.is_last = function(item) {
        return $scope.items.indexOf(item) == ($scope.items.length - 1);
    }
  }

  return directiveDefinitionObject;
});

myModule.directive("inner", function($compile) {
    return {
        require: "^breadcrumb",
        restrict: "E",
        template: '<li><a href="#" ng-transclude></a></li>',
        replace: true,
        transclude: true
    };
  }
);

And in your html you would come with:

<breadcrumb>
  <inner ng-repeat="item in items"><i>{{item}}</i></inner>
</breacrumb>

The trick is the ng-transclude directive inside the templates. It just get the contents of the element and "move it" to inside the element marked with ng-transclude and link it against the parent scope, what is awesome, as you can have dynamic naming for the items that would be based on parent scope. The items of the ng-repeat, for example, would be defined in the parent scope, as expected. This is the preferred way, and you would even get the possibility of use different inner templates (as I did with the <i> tag). You would even be able to not use an ng-repeat and hardcode the inner elements, if it's the case:

<!-- language: lang-html -->

<breadcrumb>
  <inner>First Path</inner>
  <inner>Second Path: {{someParentScopeVariable}}</inner>
</breacrumb>

Here is a working Plnker.

share|improve this answer
 
Now I make it work thanks to you. I never imagined angular works that way. Removing compile callback was wonderful. –  Ismael Jul 20 at 15:04
 
Yes, Angular has many tricks we're not used with a jQuery background. Take a look at this docs page, it's a very good source of info. The text is not that simple, it could be better, but it will explain a lot. –  CaioToOn Jul 20 at 15:09
 
Could you tell me if it would make more sense doing the get/set methods in controller level instead $scope? Inside controller this._push(item) instead $scope._push(item). –  Ismael Jul 21 at 14:26
 
Yes, it does. Just as ng-model does. You are creating a breadrumb controller API. And you can't assume inner's scope will be the same as breadcrumb's, as you can create other directive (different from inner) that would interact with breadcrumb but demand isolated scope. –  CaioToOn Jul 21 at 14:59
 
I found another way. Check my answer below, maybe you make some opinion. –  Ismael Sep 9 at 22:48
add comment

I was able to rebuilt it.

The full code is here: http://plnkr.co/ZQOHqao8aqksbVjGQTHj

I've learnt a lot since my first attempt.

The solution was simplified because dynamic template is rubbish to handle because ng-repeat does not re-draw the entire array. So, I did it my own way and it was a clean solution.

share|improve this answer
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.