Content Projection in Angular
Written by Preston Lamb
Posted on Aug 12, 2019
Angular content projection allows you to pass content (HTML, other Angular components) into another component, making it more reusable. It allows you to make more reusable components that focus more on functionality than on UI. Then you can use them in different applications with entirely different UIs. The
<ng-content> tag in an Angular component is what enables us to project content into our components. You can also select certain parts of the projected content by class, attribute, id, or tag and place it in a certain location inside the component if needed. The more you use content projection, the more you will come to see how powerful and useful it is.
Content projection is the ability to put HTML elements (or other Angular components) between the opening and closing tag of a component selector, and have that content displayed. It allows for components to be even more reusable, as the user can determine what content to project. The user could use the same component five different times with five different sets of content, all while having some default styles or functionality applied as well.
When should we use content projection? There are many ways, and I'm not going to be able to name or cover them all, but here are a couple ideas:
Let's say you have a card component that applies some general styles to the card, but the content is and the format of the content doesn't really matter. Maybe sometimes you want a single h3 tag as the body. Other times you might want an h3 tag and a paragraph tag. Maybe sometimes you just want an image. Maybe some cards have a header and a footer with the body content, and sometimes the card doesn't have the header or footer. With content projection, your card component can allow you to project the card content that you want, while providing the default styles for you.
Another example could be an accordion component. Typically, accordions have a header that you click on and the body shows/hides on click. Many accordion components out there come with predetermined styles or other decisions that are hard /impossible to overwrite. Using content projection, you could add the accordion functionality to whatever content is projected.
My general rule is if I'm repeating the same thing over and over, with just minor content differences, I'll create a component and implement content projection. That way each component gets the reused parts automatically, but I can still customize the components where needed.
Let's look at how to use content projection now! The simplest way to get started is to just add the
<ng-content> tag to your component's template. Let's look at an example:
That's all your component's template needs for content projection to work. To project our content, you just have to do this:
<my-content-projector> <p>This content will be output in the component. where the ng-content tag is.</p> </my-content-projector>
Now obviously only containing an
<ng-content> tag might not make a whole lot of sense. In reality, it might actually look more like this (like our card example above):
<div class="card"> <div class="card-body"> <ng-content></ng-content> </div> </div>
This makes it so that we don't have to repeat the
.card div setup each time we want a new card. We can write that once (and change it once if needed). In some cases there may be some default styles added to
.card as well.
There might be times where we want to select a certain part of the projected content and place it in a particular location in the component. Let's look at a different variation of our card component. In this case, we want to specify part of the projected content as the card header, and the rest as the card body.
<div class="card"> <ng-content select=".card-header"></ng-content> <ng-content></ng-content> </div>
Let's look at this template really quick. All that's been added is a
select attribute on the
<ng-content> tag. The values that can be used in the select attribute are normal CSS selectors, like an attribute (
"[my-attr]"), a class (
".my-class"), an id (
"#my-id"), or an HTML tag, including a different Angular component selector (
"p"). Any of those can be used in the selector attribute. If no matching content is provided, nothing is output. No error is thrown, which makes it nice so that the content can be optional if you want it to be.
You can have as many
<ng-content> tags with selectors as you want, but if you have multiple
<ng-content> tags without selectors, it's only going to output non-selected content once. You can have one "catch-all", but as many defined selectors as you'd like. Interestingly, though, if you have two
<ng-content> tags with the same
select attribute value, the matching projected content will be output that same number of times.
Now let's look at the accordion component example. We don't really care what the user passes in for the component's header and body content, but they do need to provide those. We'll also wrap the projected content in some HTML elements that we can use for click event handlers and animations. Our template looks like this:
<div class="accordion-header-container" (click)="toggle()"> <ng-content select="[accordion-header]"></ng-content> </div> <div class="accordion-body-container" [@animation]="..."> <ng-content select="[accordion-body]"></ng-content> </div>
This is just an example; there are many ways to implement this.
We put a click event on the
.accordion-header-container element and the animation attribute on the
.accordion-body-container element. This way, the user gets the functionality they want, which is the body showing and hiding based on the clicks, but they can pass in whatever they want for the accordion header and the body. It's so much more flexible than if we told them to give inputs for the header text and the body text. In that case, we would probably have a whole bunch of default styles and elements placed in the component that would be either hard or impossible to overwrite.
Lately as I've written components, I've worked hard to use content projection so that I can reuse the component in every project. I first thought of this idea a few years ago when I heard about Kent C. Dodds's React library, Downshift. The idea behind it was providing the functionality for everyone who used it while they got to choose how the component actually looked. I knew there had to have been a way to do it in Angular, but it took me a bit to figure out how to do it. I've now been working on several components and directives implementing content projection. You can see some of them in my ngx-plug-n-play library.
At first, content projection seems scary because it's new, and that can be a little overwhelming, but with a little practice it will become easier and easier. You'll see how the flexibility can benefit you while building new components and you'll reach for it more and more. If you'd like to know how to test components using content projection, you can see an example in my Angular unit testing blog post. The demo test for components in that article tests a component using content projection.