Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save dryqin/d9adca300c5bb3cb9d641742a1c16e08 to your computer and use it in GitHub Desktop.
Save dryqin/d9adca300c5bb3cb9d641742a1c16e08 to your computer and use it in GitHub Desktop.
Writing your own structural directives with context variables

Writing your own structural directives with context variables

Complete code in math.directive.ts

After reading this you will be able to create a structural directive with inputs and context variables and use it like this:

<div *math="10; exponent: 3; let input; 
            let exponent = exponent; let r = root;
            let p = power; let ctrl = controller">
    input: {{ input }}, exponent = {{ exponent }}<br/>
    root = {{ input }}<sup>1/{{ exponent }}</sup> = {{ r }}<br/>
    power = {{ input }}<sup>{{ exponent }}</sup> = {{ p }}<br/><br/>
    <button (click)="ctrl.increment()">increment input</button>
</div>

The directive will take a number as input (here 10, this can also be connected to a variable in your component of course) and an exponent (here 3). It will give you the calculated power, root and your inputs. You will also be able to increment the input manually and have all values updated.

Written by JanMalch

Table of contents

Basics

First create a new directive with ng g d math. You change the selector to "math" like this:

@Directive({
  selector: '[math]' //tslint:disable-line:directive-selector
})

To get the HTML template you defined (the div container) and a view container to render this template in, we have to change the constructor to this:

constructor(private vcr: ViewContainerRef,
            private tmpl: TemplateRef<any>) { }

Inputs

To create the default input (10 in the example above), you add a @Input() and give it the same name as the directive selector:

@Input() math: number;

To add the exponent input you add another @Input(). The name has to start with the directive selector and then the actual variable name, but with the first letter capitalized.

@Input() mathExponent: number;
// or: @Input("mathExponent") exponent: number;

To set inputs in your HTML you use a :. The default input is first and doesn't need a label.

<div *math="10; exponent: 3">Test</div>

As @Input() values are available in ngOnInit we can use them from now on in our directive. With the HTML above we get the following result

ngOnInit() {
  console.log("base value =", this.math);       // base value = 10
  console.log("exponent =", this.mathExponent); // exponent = 3
}

Rendering

The div won't be rendered at this point. To do this we have to render the TemplateRef in the ViewContainerRef.

To do this we create a new private function.

private createView() {
  this.vcr.clear();
  this.vcr.createEmbeddedView(this.tmpl);
}

and call it in ngOnInit.

ngOnInit() {
  this.createView();
}

Using variables in the template

You cannot use the math or mathExponent variables in your HTML just yet. To do this you have to provide a context object. A context object can be any plain object literal.

First define an interface for our directive

export interface MathContext {
  $implicit: number;
  root: number;
  power: number;
  exponent: number;
}

These variables will be availabe in your directive / HTML. To get these values you have to use the let x = ... syntax. Where x can be any variable name you want. To connect x with the value of root you would write let x = root. Then you can use your x variable in the template like this:

<div *math="10; exponent: 3; let x = root;">
    root = {{ x }}
</div>

$implicit

The $implicit variable is sugared syntax as you can omit it when connecting to a variable. So let input = $implicit; is the same as let input. With this we can already get all our variables in the template:

<div *math="10; exponent: 3; let input; 
            let exponent = exponent; let r = root;
            let p = power">
    input: {{ input }}, exponent = {{ exponent }}<br/>
    root = {{ input }}<sup>1/{{ exponent }}</sup> = {{ r }}<br/>
    power = {{ input }}<sup>{{ exponent }}</sup> = {{ p }}
</div>

excursus: microsyntax and *ngFor

What we are writing here is called microsyntax. While it's advantageous for readability, you could also omit the :, ; or =.

Everyone has used *ngFor in their applications like this:

<div *ngFor="let val of values"></div>

You may have thought, well, that's just a JavaScript for-loop, but it's actually microsyntax with some sugar. You can reduce the sugar step by step:

<div *ngFor="let val of values"></div> <!-- normally -->
<div *ngFor="let val; of: values"></div> <!-- with ; and : -->
<div *ngFor="let val = $implicit; of values"></div> <!-- using $implicit -->

Implementing logic

You are almost done. The only thing missing is filling our context variables like power and root with data.

To pass in the context we simply add it as an argument in createEmbeddedView

this.vcr.createEmbeddedView(this.tmpl, {
    $implicit: this.math,                             // the value from our @Input()
    power: Math.pow(this.math, this.mathExponent),    // scary math
    root: Math.pow(this.math, 1 / this.mathExponent), // even scarier math
    exponent: this.mathExponent                       // the value from our @Input()
});

To ensure correct typing you set the context interface MathContext as the generic type of your TemplateRef.

constructor(private vcr: ViewContainerRef,
            private tmpl: TemplateRef<MathContext>) {
}

Also make sure you clean up after yourself in ngOnDestroy.

export class MathDirective implements OnInit, OnDestroy {
    // ...
    ngOnDestroy() {
        this.vcr.clear();
    }
}

You now have a fully functioning *math directive!

Advanced functionality

The last thing missing is the ability to increment the input value and update the output. First we create a private function called increment, which increases our math variable and renders the template again.

private increment() {
    this.math++;
    this.createView();
}

To use this method we add a controller to our context, which exposes a increment() function. This function simply calls our private increment() function.

this.vcr.createEmbeddedView(this.tmpl, {
    // ...
    controller: {
        increment: () => this.increment()
    }
});

Get the controller property, add a button and you are done:

<div *math="10; exponent: 3; let input; 
            let exponent = exponent; let r = root;
            let p = power; let ctrl = controller">
    <!-- ... -->
    <button (click)="ctrl.increment()">increment input</button>
</div>

You now have a fully functional and dynamic structural directive.

Practice time

As practice you can now implement an image carousel directive, that takes an array of objects and exposes a controller, that allows you to move to the next or previous image.

Here is some code that might get you on the right track:

app.component.ts

images = [
    {
        source: "https://angular.io/assets/images/logos/angular/[email protected]"
        title: "Angular logo"
    },
    {
        source: "https://angular.io/generated/images/marketing/home/code-icon.svg"
        title: "Angular code icon"
    },
    {
        source: "https://angular.io/generated/images/marketing/home/angular-connect.png"
        title: "Angular Connect"
    }
];

app.component.html

<div *carousel="let source from images; let title = title; let ctrl = controller">
    <button (click)="ctrl.previous()">Previous</button>
    <img [src]="source" [title]="title">
    <button (click)="ctrl.next()">Next</button>
</div>

The biggest advantage is it's entirely up to the developer how he wants so style his carousel, but you provide him a nice and simple API with all the functionality he needs.

Credits & Learn more

This guide is heavily inspired by Alex Rickabaugh's talk Advanced Angular Concepts on YouTube and Google Presentations.

In the second part he shows how to implement the *carousel directive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment