Ismael
Ismael

Reputation: 2330

AngularJS issues with compiler attribute in directive

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

The problem is that it works with an array with one element, but not with two 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 searches 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...


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, and I just need to remove the <inner>, but no luck until now. I tried to replace the element tag, but 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);
                };
            }
        };
    }
]);

Upvotes: 2

Views: 1902

Answers (2)

Ismael
Ismael

Reputation: 2330

I was able to rebuilt it.

I've learnt a lot since my first attempt.

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

Upvotes: 0

Caio Cunha
Caio Cunha

Reputation: 23394

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.

Upvotes: 2

Related Questions