Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS: multiple directives with transclusion on same element

I'm trying to inject 2 templates into an element and operate on them:

<div
  ic-first="foo"
  ic-second="bar"
  ic-third="baz"
  ic-fourth="qux"
>
</div>

icFirst should inject via a template an empty div as a child of its element. icSecond should inject a second div (with a bunch of content) as the second child of its element, so the resulting html would look like:

<div
  ic-first="foo"  // priority: 100
  ic-second="bar" // priority: 50
  ic-third="baz"  // priority: 0
  ic-fourth="qux" // priority: 0
>
  <div id="foo"></div>
  <div> <!-- a bunch of stuff from the templateUrl --> </div>
</div>

Both icFirst and icSecond will inject other elements into the newly created containers.

When I specify a directive template property on both directives, I get an error:

Error: Multiple directives [icFirst, icSecond] asking for template on: <div ic-first

When I add transclude: true to both directives, icFirst executes just fine…but then the other directives on the same element are not executed. When I set transclude: 'element', the other directives execute but I get an error that the first child ($scope.firstObj) is undefined.

All four directives need access to each other's scope, so I'm doing most of my work in their controllers:

app.directive('icFirst', ['ic.config', function (icConfig) {
  return {
    restrict: 'A',
    priority: 100,
    template: '<div id="{{firstId}}"></div>',
    replace: false,
    transclude: 'element',
    controller: function icFirst($scope, $element, $attrs) {
      // …
      $scope.firstId = $scope.opts.fooId;
      $scope.firstElm = $element.children()[0];
      $scope.firstObj = {}; // this is used by the other 3 directives 
    },
    link: function(scope, elm, attrs) { … } // <- event binding
  }
);
app.directive('icSecond', ['ic.config', function (icConfig) {
  return {
    restrict: 'A',
    priority: 0,
    templateUrl: 'views/foo.html',
    replace: false,
    transclude: 'element',
    controller: function icSecond($scope, $element, $attrs) {
      // …
      $scope.secondElm = $element.children()[1];
      $scope.secondObj = new Bar( $scope.firstObj );
      // ^ is used by the remaining 2 directives & requires obj from icFirst
    },
    link: function(scope, elm, attrs) { … } // <- event binding
  }
);

Note I have corrected the behaviour of replace: false to match the documented behaviour, as described in pull request #2433.

I tried instantiating $scope.firstObj in the controller, and setting it in the linkFn (hoping the transclusion would have completed by the time the linkFn executes), but I get the same problem. It appears first-child is actually a comment.

like image 918
Jakob Jingleheimer Avatar asked Apr 18 '13 00:04

Jakob Jingleheimer


People also ask

Can we use multiple directives in AngularJS?

... is quite illustrative as AngularJS doesn't allow multiple directives (on the same DOM level) to create their own isolate scopes. According to the documentation, this restriction is imposed in order to prevent collision or unsupported configuration of the $scope objects.

What is transclusion in AngularJS?

Essentially, transclusion in AngularJS is/was taking content such as a text node or HTML, and injecting it into a template at a specific entry point. This is now done in Angular through modern web APIs such as Shadow DOM and known as “Content Projection”.


1 Answers

The only reason I can come up with that explains throwing this error is that the AngularJS team was trying to avoid needless overwrites/DOM manipulation:

Considering the actual behaviour of replace: false vs the documented behaviour, I think the actual is in fact the intended behaviour. If this is true, then allowing multiple templates/templateUrls to be used on the same element will cause subsequent templates to overwrite previous ones.

Since I already modified the source to match the documented behaviour, as a quick fix, I modified the source again (/src/ng/compile.js:700) to remove the assertNoDuplicate check (which corresponds to angular.js:4624). Now I return the following 2 objects, and it works, and I can't find any negative repercussions:

// directive icFirst
return {
  restrict: 'A',
  priority: 100,
  replace: false,
  template: '<div id="{{firstId}}"></div>',
  require: ["icFirst"],
  controller: Controller,
  link: postLink
};
// directive icSecond
return {
  restrict: 'A',
  require: ['icFirst'],
  replace: false,
  templateUrl: 'views/bar.html',
  priority: 50,
  controller: Controller,
  link: postLink
};

If made permanent, the check should probably be
if (directive.templateUrl && directive.replace)
(and similar for directive.template)

like image 157
Jakob Jingleheimer Avatar answered Sep 20 '22 11:09

Jakob Jingleheimer