Mixing Specific And Non-Specific ng-content Transclusion In Angular 2.4.1
When I first started digging into Angular 2, I remember thinking that the ng-content directive was a bit limited in how it would "project" or "transclude" content. But, the other day, I was looking through some of the Google Material Design code and saw ng-content being used in a way that I had not seen before - I was surprised to see both specific and non-specific ng-content projection working side-by-side. Clearly I'm behind in my learning of Angular 2; so I wanted to do a little experimenting.
Run this demo in my JavaScript Demos project on GitHub.
I could totally be wrong about this, but when I first experimented with ng-content, I could swear that it basically had two modes: project everthing; or, project a particular Angular 2 Directive type. It's hard to say - it was never really documented and I didn't do that much digging. That said, ng-content seems to be much more flexible these days. Jecelyn Yeen has a great article on the various ways "select" can be used in transclusion.
For this demo, however, I'm not so much interested in how the "select" input of the ng-content tag works; really, I'm more interested in how specific and non-specific ng-content tags coexist in the same Angular 2 component. To experiment with this, I created a simple App component that consumes a Layout component. The App component provides an articulated header and footer portion of the layout content; but, it also provides non-contained "body" content:
// Import the core angular services.
import { Component } from "@angular/core";
@Component({
moduleId: module.id,
selector: "my-app",
styleUrls: [ "./app.component.css" ],
template:
`
<my-layout>
<ng-container role="header">
This is my header content.
</ng-container>
<p>
This is some body content.
</p>
<p>
And some more content, which is so good.
</p>
<ng-container role="footer">
This is my footer content.
</ng-container>
</my-layout>
`
})
export class AppComponent {
// ...
}
As you can see, the App component is providing the Layout component with a header, a footer, and then "everything else."
In the Layout component, we can then project / transclude these three "areas of content" into the Layout view using ng-content. For the header and footer, we can use the "select" attribute to target the ng-container directives. And, for the "body" content, we can use a non-specific ng-content directive:
// Import the core angular services.
import { Component } from "@angular/core";
@Component({
moduleId: module.id,
selector: "my-layout",
styleUrls: [ "./layout.component.css" ],
template:
`
<div class="header-panel">
<!-- Notice the duplicate usage - just to explore how it works. -->
<ng-content select="ng-container[role=header]"></ng-content>
<ng-content select="ng-container[role=header]"></ng-content>
</div>
<div class="body-panel">
<!--
Here, we project / transclude any content that is not being transcluded
by more specific ng-content select methods. Notice that the wild-card
ng-content does not have to be the last ng-content in the template.
-->
<ng-content></ng-content>
</div>
<div class="footer-panel">
<!-- Notice the duplicate usage - just to explore how it works. -->
<ng-content select="ng-container[role=footer]"></ng-content>
<ng-content select="ng-container[role=footer]"></ng-content>
</div>
`
})
export class LayoutComponent {
// ...
}
Here, you can see that I'm using specific ng-content[select] and non-specific ng-content directives side-by-side, in the same component template. And, for the sake of experimentation, I'm even duplicating the specific ng-content directives just to see how they behave. When we run this Angular 2 application, we get the following output:
As you can see, everything "just worked!" The specific ng-content[select] directives transcluded the header and footer ng-containers; and, the non-specific ng-content directive transcluded all of the content that wasn't going to be transcluded by a more-specific ng-content directive. So cool!
You can't necessarily see this from the code, but the order of the content, as defined in the App Component, doesn't actually matter. Right now, the order of content in the App component and the transclusion of said content into the Layout component is the same. But, if you watch the video, you can see that the order doesn't matter - the App component content can be shifted around and the Layout component still renders exactly as we would hope.
And of course, we can see that the duplicate ng-content tags don't explode - they just fail to transclude any additional content.
The behavior and functionality of the ng-content directive has come a long way in Angular 2. With flexible selectors and the ability to have specific and non-specific ng-content directives working side-by-side, we can create easy-to-consume components that allow for more natural content definitions. Very cool stuff!
Want to use code from this post? Check out the license.
Reader Comments
FYI, this feature is not related to the <ng-container>, works with any element.
See:
http://plnkr.co/edit/zbjHOc0gBryAy8eNr0PA
@Mark,
Yes, correct. In my post, I linked to Jecelyn Yeen's article where she covers many way in which the [select] attribute can be used in ng-content. In my particular demo, I just wanted to look at what happens when you use [select] and non-[select] versions of ng-content in the same component. It was not something I had ever seen before.
@Ben, actually it was a helpful information (the [select] behavior) for me, too. Thx for sharing.
@Mark,
My pleasure!