Note: This post is part of an eleven-part series on Angular development. You can find the series overview here.

Overview

When creating display components, the initial inclination is to reach for the friendly Input on the component. This is an easy to implement and effective method to pass information to a component for display, however it starts to fall short as new requirements arise such as adding links, images, or other styling. This article aims to show the flexibility and power that comes from content projection opposed to (or often in conjunction with) component inputs. 

To follow along with the articles there a two repositories created: a starting point and final solution.

Display Components

In Angular, we often will build display components that can be used to control the presentation of data, inputs, or other elements to the user.  These types of low level components allow for a high level of reuse in the application to give a consistent and maintainable style.  By centralizing content into display components, we can make a change to the CSS for that component and completely update the look and feel of the application in a consistent way.  I have used this with great success in the past for formatting components, or displaying boolean values as icons.  If the format changes, or a new icon is desired, it’s a simple one line change to the application.

For this article, let’s start by building a simple display component: a card.  Cards are simply data displayed within a bordered box to help segregate the content.  Below is a screenshot of a simple usage of this from the Bootstrap documentation.  Let’s take this and make an Angular component!

We want to create a component that will apply the classes for us.  Let’s start with a simple component and populate the body of the card using an Input. 

@Component({
    selector: 'app-input-card',
    template: `
        <div class="card">
            <div class="card-body">
                {{bodyText}}
            </div>
        </div>`
})
export class InputCardComponent {
    @Input() bodyText: string;
}
We can use this component to create the content above like this:
<app-input-card bodyText="This is some text within a card body."></app-input-card>
But cards are often used with a heading.  Below is how we accomplish this with Bootstrap and the resulting display:
<div class="card">
    <div class="card-header">
        A sample card
    </div>
    <div class="card-body">
        This is some text within a card body.
    </div>
</div>


Let’s wrap up the initial design by adding an input for the heading text:

@Component({
    selector: 'app-input-title-card',
    template: `
        <div class="card">
            <h5 class="card-header">
                {{headerText}}
            </h5>
            <div class="card-body">
                {{bodyText}}
            </div>
        </div>`
})
export class InputTitleCardComponent {
    @Input() bodyText: string;
    @Input() headerText: string;
}

Adding Content Projection

This works great for what we currently need and we now have a consistent design element.  However, what happens when the body of the card needs a link, or an image?  How can we better format large sections of text?  With content projection, we can take advantage of content within our component element and place that wherever we need to within our component template.  Let’s first look at how the usage of the component will look once we add content projection before talking about how it is implemented:

<app-project-body-card headerText="A sample card">This is some text within a card body.</app-project-body-card>
As you can see, we no longer have an input for the body.  Instead we use the element as a wrapper around some content.  This let’s us add whatever markup we need to the body.  Below is an example usage and the resulting display.
<app-project-body-card headerText="A sample card">
    Now I <strong>can</strong> add <em>markup!</em >
    <span class="badge badge-info">And Other Elements</span>
</app-project-body-card>

To embed the content, we can use the ng-content element to tell Angular where to place the projected content.  Angular takes the DOM contents from within the component tags and moves them into the template into the ng-content tags.  All we need to do it replace the template interpolation {{bodyText}} with <ng-content></ng-content>.

@Component({
    selector: 'app-project-body-card',
    template: `
        <div class="card">
            <h5 class="card-header">
                {{headerText}}
            </h5>
            <div class="card-body">
                <ng-content></ng-content>
            </div>
        </div>`
})
export class ProjectBodyCardComponent {
    @Input() headerText: string;
}

Multiple Projections

This is great, but we still have the same problem for the heading.  How can we tell Angular to project different content to different places?  Let’s start with how we want this to work and see if we can implement it.  We want to add multiple elements into our element and have Angular put them where they should go.  We’ll need some way to identify where it should go, so let’s add some dummy attributes (header and body) to specify where it should go.  Here is the code and the resulting display:

<app-project-multiple-card>
    <div header>Projected Heading <button class="btn btn-primary">With buttons!</button></div>
    <div body>
        Now I <strong>can</strong> add <em>markup!</em >
        <span class="badge badge-info">And Other Elements</span>
    </div>
</app-project-multiple-card>

This is not exactly what we are looking for, everything is still being inserted into the single <ng-content> tag.  Let’s try adding another tage to see if that helps us split up the values:

@Component({
    selector: 'app-project-multiple-card',
    template: `
        <div class="card">
            <h5 class="card-header">
                <ng-content></ng-content>
            </h5>
            <div class="card-body">
                <ng-content></ng-content>
            </div>
        </div>`
})
export class ProjectMultipleCardComponent { }

Oddly enough, we get the exact same result.  What is happening here?  As I mentioned before, content projection takes DOM content within the element and moves it to the location of the ng-content.  However, when I add two ng-content tags, Angular won’t create DOM elements to duplicate the content – instead the content keeps getting moved around which leaves the first ng-content empty.  This is important to remember when working with projected content, especially if getting into situations where the content is hidden or displayed conditionally with ngIf or display attributes.  Furthermore, the content projection is evaluated before the parent ngIf.  Even if you wrap the ng-content with an ngIf, the content still gets placed before the ngIf executes, and then is removed when ngIf runs. More on dealing with that situation later.

What we need is a way to tell Angular how to pick which content that should go in which location.  To accomplish this, we can use the select directive on the ng-container element.  We can provide any valid CSS selector to use and Angular finds the matching content to project so we will use “[header]” and “[body]” to identify the content based on the dummy attributes that were added earlier.

@Component({
    selector: 'app-project-multiple-card',
    template: `
        <div class="card">
            <h5 class="card-header">
                <ng-content select="[header]"></ng-content>
            </h5>
            <div class="card-body">
                <ng-content select="[body]"></ng-content>
            </div>
        </div>`
})
export class ProjectMultipleCardComponent { }

Success!  We can now move the desired content where we need it to go and we have the ability to add markup to both locations.  This method works great for any predictable structure (for example a input wrapper component that expects a label and an input), but since we are creating arbitrary distinctions, we would be better off implementing a few components or directives.  We will create some components here to demonstrate some further benefits.
@Component({
    selector: 'app-card-heading',
    template: <div [ngClass]="headingClass()"><ng-content></ng-content></div>
})
export class CardHeadingComponent {
    @Input() textStyle: string;

    public headingClass() {
        return text-</span>${<span>this</span>.<span>textStyle</span>}<span>;
    }
}

@Component({
    selector: 'app-card-body',
    template: '<ng-content></ng-content>'
})
export class CardBodyComponent { }
A few items to note about these new components. First, the templates both utilize ng-content. We can nest ng-content throughout the elements as needed. The CardBodyComponent doesn’t do much other than give us a declarive way to wrap the content and move it where we need it to go. The CardHeadingComponent has some more going on with it, I’ve added an input that allows control over the styling of the text. This is one of the main reasons to use components to wrap your content – it makes extending functionality far easier and less of a breaking change. All we need to do on our card component is change the select directive to look for these element names. You can see the square brackets are gone because we are looking for an element and not an attribute.
@Component({
    selector: 'app-project-component-card',
    template: `
        <div class="card">
            <h5 class="card-header">
                <ng-content select="app-card-heading"></ng-content>
            </h5>
            <div class="card-body">
                <ng-content select="app-card-body"></ng-content>
            </div>
        </div>`
})
export class ProjectComponentCardComponent { }
And now we can implement this and see the result:
<app-project-component-card>
    <app-card-heading textStyle="primary">
        Projected Heading <button class="btn btn-primary">With buttons!</button>
    </app-card-heading>
    <app-card-body>
        Now I <strong>can</strong> add <em>markup!</em >
        <span class="badge badge-info">And Other Elements</span>
    </app-card-body>
</app-project-component-card>

We still have everything going where we need to go, but we also get the added control of styling our header with an input.

Conditional Projection

Now we can go back to the problem discussed earlier regarding conditional statements associated with the content.  What if we want the option to move the header content to be the footer instead?  This type of situation can arise when creating reactive layouts – we may want our styling to look different on mobile so we need to conditionally make changes to the structure of our content.  For this example I’ll control this through an input on the component, but it could just as easily be an injected service to the component.  In order to place the same content in multiple potential locations, you will need to enlist the help of some other Angular elements: ng-template and ng-container.  We can project the content into a template and use conditional statements to display this where we need it.  So what are ng-template and ng-container, and how are they different than ng-content?

<ng-template>

The Angular ng-template element is based off of the HTML <template> element.  From MDN:

The HTML Content Template (<template>) element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.

Think of a template as a content fragment that is being stored for subsequent use in the document. While the parser does process the contents of the <template> element while loading the page, it does so only to ensure that those contents are valid; the element’s contents are not rendered, however.

For Angular’s ng-template, this goes a step further.  If we place content into ng-template, it won’t even be included in the DOM, it gets replaced by a comment – that is until we include a structural directive to tell Angular what to do with the content.

<div *ngIf="!showTemplate else myTemplate">
    I appear when showTemplate is false
</div>

<ng-template #myTemplate>
    I appear when showTemplate is true
</ng-template>
This is an example of a common use case for a template, the else statement of ngIf.  NgIf will render the element it’s on when true, otherwise we can tell Angular to use another template.  This can be an effective way to use a loading animation in place of content while it’s loading, you can use the same template (e.g. a spinner) in place of different sections while the content loads.  The contents can be replicated as needed, so you don’t have any restriction to show one template at a time.

<ng-container>

The Angular ng-container element is another element that doesn’t represent anything in the DOM, but it’s contents are always rendered.  It’s a useful container to use as a wrapper around elements and you have some reason to not use a DOM element such as a div to hold it.  An example usage of this is to address a limitation where you are not allowed to have multiple structural directives on the same element.  Let’s say you want to loop through some elements but only display them if a display property is true.

<!-- This will throw an error and is not valid in Angular -->
<div *ngFor="let item of items" *ngIf="item.display">{{item.text}}</div>

<!-- This is legal and does what we want but does not create any nested elements -->
<div *ngFor="let item of items" >
    <ng-container *ngIf="item.display">{{item.text}}</ng-container>
</div>
We can use the ng-template and ng-container together using the *ngTemplateOutlet structural directive. This allows us to use the content in a template anywhere we want on the page:
<ng-container *ngTemplateOutlet="template"></ng-container>

<ng-template #template>
    Hello!
</ng-template>

Bringing All Three Together

We can use these tools together to place the heading content into a template, then conditionally display the template in one of two locations determined by the value of the isHeader property. 

@Component({
    selector: 'app-multiple-locations-card',
    template: `
        <ng-template #content>
            <ng-content select="app-card-heading"></ng-content>
        </ng-template>
        <div class="card">
            <h5 class="card-header">
                <ng-container *ngTemplateOutlet="isHeader ? content : null"></ng-container>
            </h5>
            <div class="card-body">
                <ng-content select="app-card-body"></ng-content>
            </div>
            <div class="card-footer">
                <ng-container *ngTemplateOutlet="!isHeader ? content : null"></ng-container>
            </div>
        </div>`
})
export class MultipleLocationsCardComponent {
    @Input() isHeader: boolean;
}
As a side note, this doesn’t still doesn’t give us the opportunity to duplicate the projected content. If there are two instances of ng-container that display our template, only one will get the content. This is by Angular design that content projection is never duplicated.
We can now dynamically change the location of where the content goes. To emulate this, we’ll add a button to toggle a boolean value that we can pass into the input.
<button class="btn btn-info" (click)="contentLocation = !contentLocation">Toggle</button>
<div class="m-3"></div>
<app-multiple-locations-card [isHeader]="contentLocation">
    <app-card-heading>My Title</app-card-heading>
    <app-card-body>Some content</app-card-body>
</app-multiple-locations-card>

Conclusion

Content Projection is a valuable tool to add to your Angular arsenal.  Not only does it provide useful tools in adding unknown content into a component – it provides the users of that component the most flexibility possible to format the content and include additional elements and functionality to meet their needs.  Once you grasp the associated concepts, you will find a whole new world of possibilites in your code to create more modular and flexible components that can give a significant boost to code maintainability and flexibility.

About Intertech

Founded in 1991, Intertech delivers software development consulting to Fortune 500, Government, and Leading Technology institutions, along with real-world based corporate education services. Whether you are a company looking to partner with a team of technology leaders who provide solutions, mentor staff and add true business value, or a developer interested in working for a company that invests in its employees, we’d like to meet you. Learn more about us.