The code side of life

Custom View Templates For Components In Angular 2

Introduction

user

Shlomi Assaf


Custom View Templates For Components In Angular 2

Posted by Shlomi Assaf on .
Featured

Custom View Templates For Components In Angular 2

Posted by Shlomi Assaf on .

TL;DR

If you're into examples more then explanations, here is the StopWatch example in Plunker

Background

If you want to create reusable and customisable Angular 2 Components you will need to provide the user of the component a way to inject a custom template for the component.

In Angular 1 this was done by interacting with the $templateCache at runtime or using templateUrl in your directives/components.

In Angular 2 Templates are part of the component metadata.
Component metadata is defined by decorators in TypeScript using the @ prefix (@Component). The metadata get's processed before the component and you can think of it as static data stored on the class type (function).

Today, while trying to achieve this behaviour, I found a great blog post by Michael Bromley describing a nice approach to this problem.

If you're looking for more background please read Michael's post!

In his post he was kind enough to provide a follow up link to another blog post by Ben Nadal in which he describe a more simplified approach to tackle Custom View Templates.

I recommend reading his blog post for better understanding.

Jumping on the catch I started implementing it but I was missing the option to
provide a default template as part of the component so the user can either provide a template or let the default/fallback template be. Ben mentioned it will be a cool follow-up so why not?

How to?

The main idea for Custom Templates is to build a component that accept's external content (ng-content or transclustion in angular 1) and has some API logic in it.
The user will use local template variables to get a reference to the instance holding the logic for this component, from there everything is straight forward.

Consider a simple component:

@Component({
  selector: "hello-world",
  template: `<ng-content></ng-content>`
})
export class HelloWorld {  
    hello(): string {
        return 'world';
    }
}

Now, anywhere in our app, we can use this component and provide our UI:

@Component({
  selector: "my-app",
  template: 
   `<hello-world #helloWorld>
       <h1>{{ helloWorld.hello() }}</h1>
    </hello-world>`
})
export class HelloWorld {  
}

#helloWorld is a local variable template we defined on the template and the value it holds depends on the DOM element it is defined on, the name is not important, it can be anything with legal characters.

Since we define it on the DOM element hello-world and since hello-world is an Angular 2 Component, #helloWorld will reference the component's instance.
If the DOM element is not a component, #helloWorld will reference the DOM element instance. (remember this, we will use it later...)

Inside our <hello-world> element we provide the HTML content to inject into the component, this content will replace the <ng-content> element we defined in the template for the HelloWorld component.

The HTML output for my-app will be:

<my-app>  
    <hello-world>
        <h1>world</h1>
    </hello-world>
</my-app>  

Adding a Default template

Simple, but what if we want to provide a default behaviour?
We can, and its dead simple... how? local template variables again :)

@Component({
  selector: "hello-world",
  template: 
  `<div #contentWrap><ng-content></ng-content></div>
   <p *ngIf="contentWrap.childNodes.length === 0"> {{ hello() }} </p>`
})
export class HelloWorld {  
    hello(): string {
        return 'world';
    }
}

All of the changes were done in the template.

We wrapped <ng-content> with an element that has a #contentWrap local template variable (again, name is arbitrary).

What does #contentWrap reference, remember?

Since the DOM element "hosting" #contentWrap is not an Angular 2 Component we know it refers to the native DOM element instance of <div>, this means we have access to inspect it.

In the next line we use the #contentWrap local template variable.
While declaring a local template variable is done with a # prefix, accessing it or it's members is done without #.

The expression contentWrap.childNodes.length === 0 returns true if <div #contentWrap> has no content, i.e: no template provided by the user of our component!

We use an ngIf directive to switch our default template (in this case <p>) on/off based on the existence of a user template.

Note that <div #contentWrap><ng-content></ng-content></div> is one line, this is by design, creating new lines will create "text" nodes in the DOM thus changing the value of contentWrap.childNodes.length === 0

Quite easy and straight forward.

Since we're still in the early days of Angular 2 there might be better, more performant ways to tackle this issue, if you have one please comment.
user

Shlomi Assaf