Angular 2 Transclusion using ng-content

Implement Transclusion in Angular 2 using

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

Wait a minute... What is transclusion?

Understanding Transclusion

Don't get confused by the term Transclusion. It's best explained using an example.

Let's say we have a card component. It has a header, body, and footer.

  • The card layout (3 sections) and color (grey background for header and footer) are always fixed.
  • The card header and footer always allow text.
  • Any content is allowed in card body.

card component

Here are a few examples of how we can use it:

Example 1: Text, paragraph and buttons

Example 2: blockquote

Example 3: Image

How can we display the content in the card body?

We have a problem now. We want the header and footer content to be fixed, but we also want to allow a user to add dynamic content to the body section.

How can we do this? Transclution is the answer.

Transclusion is a way to let you define a fixed view template, and at the same time allow you to define a slot for dynamic content by using <ng-content> tag.

Interesting? Let's start to build our card component!

Here is the live example of our demo.

App structure

Here's our file structure:

|- app/
    |- app.component.html
    |- app.component.ts
    |- app.module.ts
    |- card.component.ts
    |- card.component.html
    |- main.ts
|- index.html
|- systemjs.config.js
|- tsconfig.json

Basic transclusion (Single Slot)

The most basic transclusion is just to define a single dynamic content area, or we call it single slot. Let's code our card component.

The Card Component Class

// card.component.ts

import { Component, Input, Output } from '@angular/core';
@Component({
  selector: 'card',
  templateUrl: 'card.component.html',
})
export class CardComponent {
    @Input() header: string = 'this is header';   
    @Input() footer: string = 'this is footer';
}

@Input is a decorator. It that allow us to pass data from the parent to child. In our case, both header and footer allow string input from its parent component.

The Card Component View

This is how our view look like:

<!-- card.component.html -->

<div class="card">
    <div class="card-header">
        {{ header }}
    </div>

    <!-- single slot transclusion here -->
    <ng-content></ng-content>

    <div class="card-footer">
        {{ footer }}
    </div>
</div>

Using our card component

We've completed our card component. Let's use it now. For example, if we want to use it in another component called AppComponent, here is how you can do it.

<!-- app.component.html -->

<h1>Single slot transclusion</h1>
<card header="my header" footer="my footer">
    <!-- put your dynamic content here -->
    <div class="card-block">
        <h4 class="card-title">You can put any content here</h4>
        <p class="card-text">For example this line of text and</p>
        <a href="#" class="btn btn-primary">This button</a>
      </div>
      <!-- end dynamic content -->
<card>

Hook it up in App Module

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }   from './app.component';
import { CardComponent } from './card.component'; // import card component

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, CardComponent ], // add in declaration
  bootstrap:    [ AppComponent ],
})

export class AppModule { }

Done! Save and run it. The transclusion slot <div class="card-block">...</div> will replace the <ng-content></ng-content> in our card component. This is how easy we can do transclusion.

Transclusion Slot Selector

<ng-content> accepts a select attribute, which allow us to sort of name our slot, to be more specific, it allow us to define the selector of our slot. Open our card component view, let's make some changes.

<!-- card.component.html -->

<div class="card">
    <div class="card-header">
        {{ header }}
    </div>

    <!-- add the select attribute to ng-content -->
    <ng-content select="[card-body]"></ng-content>

    <div class="card-footer">
        {{ footer }}
    </div>
</div>

Notice that we add select=[card-body]. The square bracket [] means attribute. It means "Replace me only if the element has card-body attribute".

Then, we change our app component view to include the card-body attribute.

<!-- app.component.html -->

<h1>Single slot transclusion</h1>
<card header="my header" footer="my footer">

    <div class="card-block" card-body><!--  We add the card-body attribute here -->
        <h4 class="card-title">You can put any content here</h4>
        <p class="card-text">For example this line of text and</p>
        <a href="#" class="btn btn-primary">This button</a>
      </div>

<card>

Save and run, everything is still working as previous.

Now, try to remove card-body from the app component view see what will happen -- Nothing will show up in the card body.

It is because we have defined <ng-content> in the card component that only element with card-body attribute can replace the slot.

Selector is Powerful

The select attribute in <ng-content> is very powerful. You can define different patterns of selection. We've just demo one just now. Here are some examples on how you can use that.

Using Attribute with Value

Replace only if the element with specific attribute and value.

<!-- card.component.html -->
...
<ng-content select="[card-type=body]"></ng-content>
...
<!-- app.component.html -->
...
<div class="card-block" card-type="body">...<div>
...

Using CSS Class Selector

Replace if the element has specific CSS class.

<!-- card.component.html -->
...
<ng-content select=".card-body"></ng-content>
...
<!-- app.component.html -->
...
<div class="card-block card-body">...</div>
...

Using Multiple Attributes or CSS Classes

You can define more than one attribute or CSS Classes:

  • Atttributes: [card][body]
  • Classes: .card.body

Here is the example of multiple attributes

<!-- card.component.html -->
...
<ng-content select="[card][body]"></ng-content>
...
<!-- app.component.html -->
...
<div class="card-block" body card>...</div>
...

Using an HTML Tag

You can use an HTML tag too.

<!-- card.component.html -->
...
<ng-content select="card-body"></ng-content>
...
<!-- app.component.html -->
...
<card-body class="card-block">...<card-body>
...

However, you will hit an error if you use the <card-body> tag now. Unhandled Promise rejection: Template parse errors: 'card-body' is not a known element

Angular 2 does not recognize the card-body tag. card-body is neither a directive nor a component. A quick way to get around this error is to add schema metadata property in your module, set value to NO_ERRORS_SCHEMA in your module file.

In our case, we do it in our app module.

// app.module.ts

import { NgModule, NO_ERRORS_SCHEMA }      from '@angular/core'; //
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }   from './app.component';
import { CardComponent } from './card.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, CardComponent ],
  bootstrap:    [ AppComponent ],
  schemas:      [ NO_ERRORS_SCHEMA ] // add this line
})

export class AppModule { }

Multi-Slot Transclusion

By using the select attribute, we can define multiple transclusion slots! Let's modify our card component, to allow transclusion slots in header and footer too.

<!-- card.component.html -->

<div class="card">
    <div class="card-header">
    <!-- header slot here -->
        <ng-content select="card-header"></ng-content>
    </div>
    <!-- body slot here -->
    <ng-content select="card-body"></ng-content>
    <div class="card-footer">
    <!-- footer -->
        <ng-content select="card-footer"></ng-content>
    </div>
</div>

Using our card component:

<!-- app.component.html -->

<h1>Multi slot transclusion</h1>
<card>
    <!-- header -->
    <card-header>
        New <strong>header</strong>
    </card-header>

    <!-- body -->
    <card-body>
        <div class="card-block">
            <h4 class="card-title">You can put any content here</h4>
            <p class="card-text">For example this line of text and</p>
            <a href="#" class="btn btn-primary">This button</a>
          </div>
    </card-body>

    <!-- footer -->
    <card-footer>
        New <strong>footer</strong>
    </card-footer>
<card>

Summary

Which type of selector should we use? Attribute, HTML tag, or CSS class or something else? It depends. My personal preference would be an attribute because it's readable. An HTML tag is also readable but you need to add schema in module metadata.

I would suggest to avoid CSS class selector if possible because it is not intuitive. It doesn't tell the user know it's a transclusion slot at first glance, until you read the card component sourcode. However, it's up to your decision.

That's it. Happy coding.

Jecelyn Yeen

Coder. Diver. Board Game Lover.

Speak English, Mandarin, JavaScript, Typescript, C# and more.

Problem solver at @iflix.