Forked from ih2502mk/Angular - custom structural directives.md
Created
February 6, 2023 10:34
-
-
Save codec4/ae14952141915fd5745d001fa00faf39 to your computer and use it in GitHub Desktop.
Revisions
-
JanMalch revised this gist
Sep 20, 2018 . 1 changed file with 14 additions and 7 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -56,8 +56,6 @@ To create the default input (`10` in the example above), you add a `@Input()` an @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. ```typescript @@ -71,6 +69,15 @@ To set inputs in your HTML you use a `:`. The default input is first and doesn't <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 ```typescript 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`. @@ -156,10 +163,10 @@ To pass in the context we simply add it as an argument in `createEmbeddedView` ```typescript 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() }); ``` @@ -184,7 +191,7 @@ export class MathDirective implements OnInit, OnDestroy { 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. -
JanMalch renamed this gist
Sep 8, 2018 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
JanMalch revised this gist
Sep 8, 2018 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -244,6 +244,7 @@ images = [ ]; ``` #### app.component.html ```html <div *carousel="let source from images; let title = title; let ctrl = controller"> <button (click)="ctrl.previous()">Previous</button> -
JanMalch revised this gist
Sep 8, 2018 . 1 changed file with 5 additions and 6 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -122,11 +122,10 @@ The `$implicit` variable is sugared syntax as you can omit it when connecting to ```html <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> ``` @@ -242,11 +241,11 @@ images = [ source: "https://angular.io/generated/images/marketing/home/angular-connect.png" title: "Angular Connect" } ]; ``` ```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> @@ -258,4 +257,4 @@ The biggest advantage is it's entirely up to the developer how he wants so style ## Credits & Learn more This guide is heavily inspired by Alex Rickabaugh's talk **Advanced Angular Concepts** on [YouTube](https://www.youtube.com/watch?v=rKbY1t39dHU) and [Google Presentations](https://docs.google.com/presentation/d/1o1zbqxe-Fn4IdGRYeVGFNQb1PSiOpjXvIsNHRC7Bk1w/mobilepresent). >In the second part he shows how to implement the `*carousel` directive. -
JanMalch revised this gist
Sep 8, 2018 . 1 changed file with 14 additions and 13 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -20,20 +20,21 @@ The directive will take a number as input (here `10`, this can also be connected ## Table of contents - [Basics](#basics) - [Inputs](#inputs) - [Rendering](#rendering) - [Using variables in the template](#using-variables-in-the-template) - [`$implicit`](#implicit) - [excursus: microsyntax and `*ngFor`](#excursus-microsyntax-and-ngfor) - [Implementing logic](#implementing-logic) - [Add more functionality](#add-more-functionality) - [Practice time](#practice-time) - [Credits & Learn more](#credits--learn-more) ## Basics First create a new directive with `ng g d math`. You change the selector to `"math"` like this: ```typescript @Directive({ selector: '[math]' //tslint:disable-line:directive-selector @@ -57,14 +58,14 @@ To create the default input (`10` in the example above), you add a `@Input()` an >Inputs will be availabe in the `ngOnInit` lifecycle hook. 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. ```typescript @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. ```html <div *math="10; exponent: 3">Test</div> @@ -257,4 +258,4 @@ The biggest advantage is it's entirely up to the developer how he wants so style ## Credits & Learn more This guide is heavily inspired by Alex Rickabaugh's talk **Advanced Angular Concepts** on [YouTube](https://www.youtube.com/watch?v=rKbY1t39dHU) and [Google Presentations](https://docs.google.com/presentation/d/1o1zbqxe-Fn4IdGRYeVGFNQb1PSiOpjXvIsNHRC7Bk1w/mobilepresent). >In the second part he shows how to implement the `*carousel` directive. -
JanMalch created this gist
Sep 8, 2018 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,260 @@ # Writing your own structural directives with context variables > Complete code in [`math.directive.ts`](https://github.com/JanMalch/ngx-code-dump/blob/master/custom%20directives/math.directive.ts) After reading this you will be able to create a [structural directive](https://angular.io/guide/structural-directives) with inputs and context variables and use it like this: ```html <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](https://github.com/JanMalch) ## Table of contents * [Basics](#basics) * * [Inputs](#inputs) * * [Rendering](#rendering) * [Using variables in the template](#using-variables-in-the-template) * * [`$implicit`](#implicit) * * [excursus: microsyntax and `*ngFor`](#excursus-microsyntax-and-ngfor) * [Implementing logic](#implementing-logic) * [Add more functionality](#add-more-functionality) * [Practice time](#practice-time) * [Credits & Learn more](#credits--learn-more) ## Basics First create a new directive with `ng g d math`. You change the selector to `"math"` like this: ```typescript @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: ```typescript 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: ```typescript @Input() math: number; ``` >Inputs will be availabe in the `ngOnInit` lifecycle hook. 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. ```typescript @Input() mathExponent: number; // or: @Input("mathExponent") exponent: number; ``` To set inputs you use a `:`. The default input is first and doesn't need a label. ```html <div *math="10; exponent: 3">Test</div> ``` ### 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. ```typescript private createView() { this.vcr.clear(); this.vcr.createEmbeddedView(this.tmpl); } ``` and call it in `ngOnInit`. ```typescript 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 ```typescript 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: ```html <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: ```html <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> ``` ### excursus: microsyntax and `*ngFor` What we are writing here is called [microsyntax](https://angular.io/guide/structural-directives#microsyntax). While it's advantageous for readability, you could also omit the `:`, `;` or `=`. Everyone has used `*ngFor` in their applications like this: ```html <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: ```html <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` ```typescript this.vcr.createEmbeddedView(this.tmpl, { $implicit: this.math, // the value from our @Input() root: Math.pow(this.math, 1 / this.mathExponent), power: Math.pow(this.math, this.mathExponent), 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`. ```typescript constructor(private vcr: ViewContainerRef, private tmpl: TemplateRef<MathContext>) { } ``` Also make sure you clean up after yourself in `ngOnDestroy`. ```typescript export class MathDirective implements OnInit, OnDestroy { // ... ngOnDestroy() { this.vcr.clear(); } } ``` You now have a fully functioning `*math` directive! ## Add more 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. ```typescript 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. ```typescript this.vcr.createEmbeddedView(this.tmpl, { // ... controller: { increment: () => this.increment() } }); ``` Get the `controller` property, add a button and you are done: ```html <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 ```typescript 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" } ] ``` ```html <div *carousel="let source of 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](https://www.youtube.com/watch?v=rKbY1t39dHU) and [Google Presentations](https://docs.google.com/presentation/d/1o1zbqxe-Fn4IdGRYeVGFNQb1PSiOpjXvIsNHRC7Bk1w/mobilepresent). >In the second part he shows how to implement the `*carousel` directive.