Jakob Jingleheimer
Jakob Jingleheimer

Reputation: 31580

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.

Upvotes: 7

Views: 8532

Answers (1)

Jakob Jingleheimer
Jakob Jingleheimer

Reputation: 31580

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)

Upvotes: 2

Related Questions