Replace one directive with another without compiling the child nodes twice

advertisements

In a project, I have built a directive aDir that is replaced by another one bDir using $compile in its post-link function. It creates a "shortcut" for bDir which is useful because bDir has many arguments and I use it all accross the project.

In my templates:

<p>
    <button a-dir></button>
</p>

Which is compiled in:

<p>
    <button b-dir arg1="" arg2="" ... ></button>
</p>

It works great with a code like this:

function aDir($compile){
  return {
    restrict: 'A',
    link: function(scope, iElem, iAttrs){
      iElem.attr('b-dir', '');
      iElem.attr('arg1', '');
      iElem.attr('arg2', '');
      [...]
      iElem.removeAttr('a-dir'); // To avoid infinite loop
      $compile(iElem)(scope);
    }
  }
}

Nevertheless, if the element on which aDir is apply has children, they will be compiled twice. Once by the $compile function initiated by Angular and once by the $compile function I call in aDir post-link.

Consider this plunker. This is the HTML:

<outer-level>
  <p replace-by-another>
    <inner-level>Hello World!</inner-level>
  </p>
</outer-level>

replaceByAnother is replaced by the directive called another. outerLevel and innerLevel are directives that does nothing but log in the console when their compile, pre and post link functions are called.

The console logs are:

outerLevel: compile
replaceByAnother: compile
innerLevel: compile
outerLevel: pre link
replaceByAnother: pre link
innerLevel: pre link
innerLevel: post link
replaceByAnother: post link
    another: compile
    innerLevel: compile
    another: pre link
    innerLevel: pre link
    innerLevel: post link
    another: post link
outerLevel: post link

So we have two calls of compile, pre link and post link functions of innerLevel. In my case that's totally ok but I'm interested in understanding entirely what $compile does and if such a behavior could be avoided.

I tried a couple of things by defining a compile function in replaceByAnother directive but I just manage to change the order of execution and not to compile just once innerLevel directive:

http://plnkr.co/edit/ZnBRaskb1WPkRZv36giS

function replaceByAnother($compile){
  return {
    restrict: 'A',
    compile: function(tElem, tAttrs){
      console.log('replaceByAnother: compile');
      tElem.attr('another', '');
      tElem.removeAttr('replace-by-another');
      var anotherLinkFunc = $compile(tElem);

      return {
        pre: function(scope, iElem, iAttrs){
          console.log('replaceByAnother: pre link');
        },
        post: function(scope, iElem, iAttrs){
          console.log('replaceByAnother: post link');
          anotherLinkFunc(scope);
        }
      }
    }
  }
}

And the results:

outerLevel: compile
replaceByAnother: compile
  another: compile
  innerLevel: compile
innerLevel: compile
outerLevel: pre link
replaceByAnother: pre link
innerLevel: pre link
innerLevel: post link
replaceByAnother: post link
  another: pre link
  innerLevel: pre link
  innerLevel: post link
  another: post link
outerLevel: post link

Do you have any idea?

SOLUTION

With the answers of @georgeawg and @AndrésEsguerra, I found a satisfacting solution:

  • Use terminal: true and a high priority to prevent Angular from compiling twice children nodes.
  • Modify the template element in the compile function and call $compile on it. Store the output of $compile.
  • Call this output in the link function to bind the template element to the scope.

function replaceByAnother($compile){
  return {
    restrict: 'A',
    terminal: true,
    priority: 100000,
    compile: function(tElem, tAttrs){
      tElem.attr('another', '');
      tElem.removeAttr('replace-by-another');
      var anotherLinkFunc = $compile(tElem);

      return {
        pre: function(scope, iElem, iAttrs){
          // pre link
        },
        post: function(scope, iElem, iAttrs){
          anotherLinkFunc(scope, function cloneAttachedFn(clone) {
              iElem.replaceWith(clone);
          });
        }
      }
    }
  }
}


You can avoid the first inner compile by cloning tElem and emptying it during the compile phase.

//replaceByAnother Directive
function replaceByAnother($compile){
  return {
    restrict: 'A',
    compile: function(tElem, tAttrs){
      console.log('replaceByAnother: compile');
      //clone tElem
      var tClone = tElem.clone();
      //empty tElem
      tElem.empty();
      return {
        pre: function(scope, iElem, iAttrs){
          //console.log('replaceByAnother: pre link');
        },
        post: function(scope, iElem, iAttrs){
          //console.log('replaceByAnother: post link');
          //modify and compile cloned elements
          tClone.attr('another', '');
          tClone.removeAttr('replace-by-another');
          var linkFn = $compile(tClone);
          linkFn(scope, function transclude(clone) {
              iElem.append(clone);
          });
        }
      }
    }
  }
}

Then modify and compile the cloned elements in the postLink phase.

The DEMO on PLNKR