Content Projection with Angular

Photo by Jeremy Yap on Unsplash

I’m Jeffry Houser, a developer in the content engineering group at Disney Streaming Services. My team builds content portals that allow editors to create magic for our customers in the video services we power.

Today I’m going to write about content projection, sometimes called transclusion, in Angular. My team was building an application that needed a context sensitive header. The header had a lot of common elements, such as navigation and a user image, but also had parts that would change based on the screen currently displayed. Our solution was to build a component that used content projection. It allows us to encapsulate the shared elements into a component, while also letting us inject our own HTML into the component so we could customize as needed. I’ll show you how we did it.

Setup Your Angular Project

To follow these instructions, you’ll need to create an empty Angular project. If you don’t have it, I’d start by installing the Angular CLI using these instructions. The next step is to create the project with this command:

ng new

You’ll see something like this:

I created this project using Angular 7. It is named Transclusion02, which is part of my own internal naming structure. You can name the project whatever you like. I also enabled routing and used CSS for this sample.

Create Some Angular Routes

The next step is to create some Angular routes. Each route will be represented by a component. Use the Angular CLI to generate a view component:

ng generate component view1

You’ll see something like this:

Now generate a second view component:

ng generate component view2

You’ll see something like this:

Now we have to make a few app changes. Open the src/app/app-routing.module.ts. Find the routes array, which should be blank:

const routes: Routes = [];

Change this to add some default routes:

const routes: Routes = [
{ path: 'view1', component: View1Component },
{ path: 'view2', component: View2Component},
{ path: '**', redirectTo: '/view1' }
];

Now we have one route for each view, and a default route that points to view1. Open up app.component.html, delete all the content and replace it with this:

<a routerLink="/view1" >View 1</a> | 
<a routerLink="/view2" >View 2</a>
<router-outlet></router-outlet>

This will allow us to run the app and navigate between the routes:

ng serve

Open your browser to see the default view:

Click the View 2 link:

Run the app yourself and click around, I’ll be here when you’re ready to continue.

Create the Header

The next step is to create a header component. This is the structure we’re going to create

The header component will contain some navigation on the left, and a user profile image on the right. The center of the component will contain our context sensitive information. Create the component:

ng generate component header

You’ll see something like this:

Let’s get to work implementing the header component. We’re going to put our HTML in the header.component.html and CSS in header.component.css, both in the src/app/header directory.

Start with the HTML and add a div wrapper:

<div class="header">
</div>

The header wraps all the content in the component. It is primarily there to set up the FlexBox as you see with the CSS:

.header{
width: 100%;
display: flex;
}

The width property makes sure that the header expands to fill 100% of the available space. The display property sets the container to use Flexbox display containers, which allow containers to expand or collapse to fill the available space.

Inside the header wrapper, add the first column, a header-nav:

<div class="header-nav">
<a routerLink="/view1" >View 1</a> |
<a routerLink="/view2" >View 2</a>
</div>

This segment includes the navigation links that we created earlier that allow us to switch between the two views. Here is the CSS:

.header-nav{
order: 1;
width: 20%;
}

The width is set to 20% of the parent container’s available space. The order property says that this div is the first one to appear in the Flexbox alignment. While we’re at it be sure to remove the navigation links from src/app/app.component.html, so the file looks just like this:

<router-outlet></router-outlet>

Back to the header.component.html component, add the middle content:

<div class="header-context">
Context sensitive something here
</div>

This div is a placeholder for the dynamic content that will be set at runtime. We’ll loop back to this in the next section when I show you how to set up content projection. The CSS is for this block is simple:

.header-context{
order: 2;
width: 80%;
}

The order is set to 2. The width is set to 80%. Simple stuff.

Finally, add a header profile image. In this case, I’m just hard coding my initials and putting them in a circle:

<div class="header-profile-wrapper">
<div class="header-profile">
JH
</div>
</div>

A user profile link or menu will be more complex in a real app, but I just wanted to add a visual placeholder. The header-profile-wrapper CSS:

.header-profile-wrapper{
width: 20%;
order: 3;
}

The order is set to 3. The width is set to 80%. Finally, add the CSS for the header-profile:

.header-profile{
float: right;
width: 32px;
height: 32px;
border: 1px solid #000000;
border-radius: 50px;
text-align: center;
line-height: 2;
}

I used float to align the div to the right. A height and width to is set to 32 pixels. A solid black border is used with a border-radius of 50px. The border radius is what makes the final result look like a circle. The text is centered inside that circle with text-align, and I added a line-height to 2. The line-height value doubles the size of the text, relative to the current font.

Re-run the app and you should see something like this:

Try to navigate between views and everything should be working fine.

Set Up Content Projection

The last step is to set up the header component to support content projection. Open up the header.component.html file. Find our header-context div:

<div class="header-context">
Context sensitive something here
</div>

Remove the text inside the div and replace it with the ng-content tag, like this:

<div class="header-context">
<ng-content></ng-content>
</div>

That’s it, now any of the content that exists inside the app-header tag will show up here. Let me demonstrate by modifying the app-header in src/app/view1/view1.component.html:

<app-header>
<div style="text-align: center; width:100%">
View 1
</div>
</app-header>

This adds a div inside the app-header tag. Content is centered using text-align and the width is set to 100%:

We can set up something different for view 2 inside the src/app/view1/view2.component.html tag:

<app-header>
Welcome to View 2
</app-header>

You’ll see something like this:

Using content projection like this is super easy to set up, but very powerful.

What are some use cases for Content Projection?

I showed you some basics on how to set up Content Projection, but now what would you use it for? Anytime you want to inject any kind of HTML into a component, you can do so with content projection.

Here are a few possible use cases of things you may want to inject:

  • Custom Text: You might use it to add a custom or message on each screen, as I demonstrated in the previous section with view 2.
  • Custom HTML: You can use content projection to send HTML to your component, just as I did with view 1, injecting a div that centered the text inside it.
  • Form Inputs: You could use it to add a search filter on top of a grid screen, like this:
<app-header>
<input type="text" placeholder="Filter Text Here"
(keyup)="filterSomeGrid($event)">
</app-header>

And you’ll see something like this:

This snippet shows off a great use case for this approach because the main component interacts with the projected content, with little control over it’s placement on the page, while the app-header component knows how to place the input on the page but does not interact with it.

  • Custom Components: Just like you can project HTML elements into a component with content projection, you can also project custom Angular components. Let’s create a new component and I’ll show you how:
ng generate component projected-text

You’ll see something like this:

For the purposes of this quick sample, I’m not going to modify the new projected-text component. You can add it like this:

<app-header>
<app-projected-text></app-projected-text>
</app-header>

You’ll see results like this:

· Wrap Inputs: The ng material library’s form field component uses content projection to wrap a traditional form field. The mat-form-field lets you apply common text field styles such as underlines, floating labels, hint, or error messages to your input. Here is a sample that adds a hint label:

<mat-form-field hintLabel=”Max 10 characters”>
<input matInput maxlength=”10" placeholder=”Enter some input”>
</mat-form-field>

You’ll see something like this:

This approach is used by multiple components throughout the ng material library.

Multiple Slot projection

The examples I showed to date tell you how to inject a single snippet of HTML into an Angular component, but Angular has the ability to inject multiple snippets. This section will create an expanded header to create a title and content.

First, create a new view:

ng generate component view-expanded

You should see this:

Then, create an expanded header:

ng generate component header-expanded

You’ll see something like this:

First, open the src/app/header/header.component.html and add a link to the new route as part of the header-nav section:

<a routerLink="/viewExpanded" >Expanded View</a> <br/>

Open the src/app/app-routing.module.ts to add the route to the routes array:

{path: 'viewExpanded', component: ViewExpandedComponent},

Now, copy the src/app/header/header.component.css to the src/app/header-expanded/header-expanded.component.css, and the src/app/header/header.component.html to the src/app/header-expanded/header-expanded.component.html.

At the beginning of the header-expanded.component.html add a header-title section:

<div class="header-title">
<ng-content select=".title"></ng-content>
</div>

The rest of the HTML can stay the same.

Notice that the ng-content tag has a select property with the value .title. This is a CSS selector and it will be used to distinguish the title projected content from the other projected content.

Put some CSS For this in the header-expanded.component.css:

.header-title{
width: 100%;
text-align: center;
}

Now, open src/app/view-expanded/view-expanded.component.html. Add the app-header-expanded to the top of the component:

<app-header-expanded>
<h1 class="title">This is the title</h1>
<p>Other Injected Content</p>
</app-header-expanded>

First, there is an h1 with the class named title. This is not a CSS class I defined in the application anywhere, this is used by the HeaderExpandedComponent to select this element and place it at the ng-content area with the .title selector. The paragraph with no specified CSS will be injected at the ng-content spot where no select property is specified.

Run the app and you’ll see this:

Final Thoughts

I don’t use content projection much in normal application development, but it can be a very powerful tool when you are building components intended for reuse. It was the perfect choice to meet our requirements, which mixed some static, consistent header elements with some ever changing elements dependent upon which screen it would be a part of. As I showed with the multiple projection sample it can get rather complex if you need it, and I see it used extensively in frameworks such as ng material.