Friday, April 25, 2014

AngularJS transcluding DOM Without transcluding scope

So I ran into an issue the other day where I needed to nest custom component directives inside a container component directive in my HTML markup and transclude the DOM but not the scope.  The reason I needed this was that the container component performs certain child component management, AND child components can be included programmatically via JSON object, as well as with markup.

The problem with normal transclusion is that the parent scope of the JSON included children is that of the container, whereas, the parent scope of the transcluded children ended up being the $rootScope, and the scopes of these children were at the sibling level of the container scope.  This meant that the container could manage some of the children but not others. It needed to manage all of the children.

A quick search of the web revealed a lot comments about the necessity for keeping the transcluded DOM fragments bound to their pre transclusion scope, and of course the reasons make sense for 98% of the use cases out there.  However, this situation definitely fell in the other 2%, and I was not able to find any posts related to what I needed to accomplish and how.

Luckily, the solution turned out to be fairly simple.  All you need to do is NOT use the ngTransclude directive in the container directive, but DO use the transclude:true option in the directive definition object.  We then use a replacement directive for ngTranclude that is nearly identical except we include the optional first argument that takes a scope other than the default.  Since this tiny directive doesn't fully transclude, I called it uicInclude, the prefix being from the component library I am currently building.

Here's the code:

// Container Component
  .directive('uicContainer', [...,
      function( ... ){
          return {
              scope: {},
              transclude: true,

<ul class="container-template" uic-include></ul>

// ngTransclude replacement
  .directive('uicInclude', function(){
      return {
          restrict: 'A',
          link: function(scope, iElement, iAttrs, ctrl, $transclude) {
              $transclude(scope, function(clone) {

The scope used in the uicInclude directive will be whatever scope is bound to the containing DOM, in this case, it is the isolate scope created by the container directive.  Now all of the child components of the container component can be managed by that container's controller since we are handling much of the management via $scope.$on() / $scope.$emit(). 

I've always wondered when or why I'd ever need to use the transclude linking function ($transclude) mentioned in the AngularJS API docs.