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
- Basics
- Using variables in the template
- Implementing logic
- Add more functionality
- Practice time
- Credits & Learn more
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>) { }
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
}
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();
}
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>
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>
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 -->
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!
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.
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:
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"
}
];
<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.
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.