Creating An Isolate-Scope Directive With Multiple Transclusion Points In AngularJS
When creating a component directive, in AngularJS, transcluding a single chunk of content is fairly easy; you define the directive as using transclusion and the AngularJS $compile() function takes care of the rest, creating an isolate scope and providing a transclude() function to your linker. But what if you wanted to create more of a "layout component" that coordinates several chunks of content? This is not such an obvious task with the current directive options. But, as a fun experiment, I wanted to see if I could create an AngularJS directive that included multiple points of transclusion.
As a side note, when I was noodling on this problem, I happen to come across this recent commit in the AngularJS code base. It looks like Pete Bacon Darwin is actually building this very concept into the AngularJS core. So, in the near future, this should be a much more straightforward task using an object-based trasnclude definition.
When it comes to AngularJS directives, the moment you use "template" or "templateUrl" in your configuration object, you lose access to the content of the element as it appears in the HTML. This is because, at that point, all references to the template-element or the linked element pertain to the "template" and not to the contents (ie, children) of the directive.
In a previous post, however, I demonstrated that we could compile transcluded content by defining a single directive at two different priorities. Using this same approach, we can extract sub-content areas with one priority and then transclude them within a lower priority of the same directive.
This is a bit complicated, but after a few mornings of trial-and-error, I finally got something "somewhat satisfactory" working. I have a Layout component that lets multiple content areas be transcluded through the use of "panels." In order to get this working, I had to break the "layout" component directive into two priorities:
Priority -1: Defines a terminal directive that provides the controller and defines the isolate scope bindings, but not the template. By omitting the template, I retain access to the content of the natural element, which I can query and extract. The use of terminal prevents the children (and lower priority directives) from being compiled and linked. This makes sense since we don't want the contents compiled as-is since they will be transcluded.
Priority -2: Defines the directive template and transcludes the sub-content areas using the linking functions provided by the higher priority.
The higher priority directive defines the controller and then, during the linking phase, injects the sub-content linking functions into the controller. The lower priority directive then requires that controller and uses the provided linking functions to transclude the sub-content areas. Each linking function is keyed by the "role" attribute found in the original content:
Since this was just a fun little exploration, I won't try to offer up too much more explanation. Nothing about this code is terribly obvious - hence why it took me like 3 mornings to figure out. But, when we run this code, you can see that each "panel" is successfully transcluded into the layout component:
Based on the commit that I found in the AngularJS code base, it looks like this kind of approach is going to become much easier in the near future. So, I'm looking forward to that. But, in the meantime, this was a fun experiment that stretched my understanding of isolate scopes and transclusion.
Want to use code from this post? Check out the license.
Interesting approach Ben.
I think your approach is way to complicated. Check this out:
just simple service implementation.
anyway it has already landed to angular repo
Thank once again for a great post!
Interesting and educational approach..
Are You coming to angular-connect This week? Would like to buy You a beer or somerthing!
Ah, very interesting. I had somewhat considered doing something like that, but I had concerns about pulling out "portions" of a cloned DOM tree. Somehow, it felt cleaner to be appending the entire cloned element. But, it's definitely possible that I just had trouble getting it working. Anyway, great link - that is definitely much more straightforward than what I had.
As far as the core Angular stuff, I actually link directly to Pete's commit in the blog post.
I wish - I haven't been able to make it to much of anything lately. Hoping to change that!
FYI, put a call-out to your linked solution at the top of the post. Thanks again :)
wow thanks! I'm glad it was helpful :)
Ok, something wasn't sitting right in my head. So, I went back and ran a sanity check. I could have sworn that I tried the approach outlined in the AirPair article. And, I think I did, and I think there's a reason that I ultimately didn't go that way. That approach seems to work if you rip out a transcluded *ELEMENT*; but, it doesn't work if you rip out transcluded *CONTENTS*.
In that approach, the author is basically doing (pseudo code):
clone.filter( ".to-header" ).appendTo( element.find( ".header" ) )
... taking an entire ELEMENT (.to-header) and appending it to the template element (.header). And that works. But, in my scenario, that would pull in the <layout-panel> element into my rendering, which would lead to something like this:
. . . . <layout-panel role="body"> .... </layout-panel>
Which is not what I want. I wanted to the *content* of the layout-panel, not the layout-panel itself.
So, if you go back and take the author's approach, but rip out the .contents() of the transcluded element:
element.find( ".header" ).append( clone.filter( ".to-header" ).CONTENTS() )
... you end up getting a couple of different AngularJS errors:
> TypeError: Cannot set property 'nodeValue' of undefined
> TypeError: Cannot read property 'childNodes' of undefined
... depending on the contents() that is being appended. I think this happens because AngularJS doesn't know how to deal with the embedded interpolation properly once the separate the contents from its parent element (just my guess, I haven't confirmed in the source code).
So, while the AirPair article definitely has a more straightforward approach, it doesn't quite do what I was trying to do. That said, if you don't mind pulling in the top-level elements, it is definitely an easier approach.