mhelvens
mhelvens

Reputation: 4313

AngularJS : How to create a transcluding element directive that keeps attribute directives and can add new ones?

I've been working on this issue for two days now. I get the feeling it should be a lot simpler.

Problem Description

I'd like to create a directive that is used as follows:

<my-directive ng-something="something">
    content
</my-directive>

and has as output:

<my-directive ng-something="something" ng-more="more">
    content
</my-directive>

Naturally it would have a linking function and controller that do some work, but the main concerns are:

Example

For example, say I want to create an element that does something internally when it is clicked on:

<click-count ng-repeat="X in ['A', 'B', 'C']"> {{ X }} </click-count>

which could compile into something like this:

<click-count ng-click="internalFn()"> A </click-count>
<click-count ng-click="internalFn()"> B </click-count>
<click-count ng-click="internalFn()"> C </click-count>

where internalFn would be defined in the internal scope of the clickCount directive.

Attempt

One of my attempts in is this Plunker: http://plnkr.co/edit/j9sUUS?p=preview

Since Plunker seems to be down, here's the code:

angular.module('app', []).directive('clickCount', function() {
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    scope: {
      ccModel: '='
    },
    compile: function(dElement) {
      dElement.attr("ngClick", "ccModel = ccModel + 1");

      return function postLink($scope, iElement, iAttrs, controller, transclude) {
        transclude(function(cloned) { iElement.append(cloned); });
      };
    },
    controller: function ($scope) {
        $scope.ccModel = 0;
    }
  };
});

This is some HTML using the directive:

<!DOCTYPE html>
<html>
<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.13/angular.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>
<body ng-app="app">
  <hr> The internal 'ng-click' doesn't work:
  <click-count ng-repeat="X in ['A', 'B', 'C']" cc-model="counter">
    {{ X }}, {{ counter }}
  </click-count>
  <hr> But an external 'ng-click' does work:
  <click-count ng-repeat="X in ['A', 'B', 'C']" cc-model="bla" ng-init="counter = 0" ng-click="counter = counter + 1">
    {{ X }}, {{ counter }}
  </click-count>
  <hr>
</body>
</html>

And because the element keeps its name, css can be used as follows:

click-count {
  display: block;
  border: solid 1px;
  background-color: lightgreen;
  font-weight: bold;
  margin: 5px;
  padding: 5px;
}

I have several ideas on what might be wrong with it, but I've tried a number of alternative approaches. If I mess around in the linker, perhaps try to $compile again, the controller function has to be called twice too. Anyway, an example of how to do it properly would be most appreciated.

Upvotes: 4

Views: 743

Answers (1)

Vadim
Vadim

Reputation: 8789

As fas as I understand, you are trying to modify DOM element and add some directives using attributes. That means that your directive should run before all other directives run. In order to control directives execution order Angular provides priority property. Most of directives runs on priority 0, that means that if your directive have larger priority it will run before. But unfortunately ngRepeat has not only priority 1000, but also defined with terminal:true, which means that once element has ngRepeat you can't specify on the same element directive with higher priority. You can add attributes and behavior, but not directives that should run before ngRepeat. However there is a workaround for ngClick:

angular.module('app', []).directive('clickCount', function() {
  return {
    restrict: 'E',
    replace: true,
    compile: function(tElement) {
      return {
        pre: function(scope, iElement) {
          iElement.attr('ng-click', 'counter = counter +1'); // <- Add attribute
        },
        post: function(scope, iElement) {
          iElement.on('click', function() { // <- Add behavior
            scope.$apply(function(){ // <- Since scope variables may be modified, don't forget to apply the scope changes
              scope.$eval(iElement.attr('ng-click')); // <- Evaluate expression defined in ng-click attribute in context of scope
            });
          });
        }
      }
    }
  };
});

JSBin: http://jsbin.com/sehobavo/1/edit

Another workaround is to recompile your directive without ngRepeat:

angular.module('app', []).directive('clickCount', function($compile) {
  return {
    restrict: 'E',
    replace: true,
    compile: function(tElement) {
      return {
        pre: function(scope, iElement) {
          if(iElement.attr('ng-repeat')) { // <- Avoid recursion
            iElement.attr('ng-click', 'counter = counter +1'); // <- Add custom attributes and directives
            iElement.removeAttr('ng-repeat'); // <- Avoid recursion
            $compile(iElement)(scope); // <- Recompile your element to make other directives work
          }
        }
      }
    }
  };
});

JSBin: http://jsbin.com/hucunuqu/4/edit

Upvotes: 1

Related Questions