MindMap Gallery Directives Mindmap
A mind map about Directives Mindmap.
Edited at 2022-09-16 09:13:34A mind map about Directives Mindmap.
In Angular, DOM manipulations are typically handled through the framework's templating and data binding features, as well as through direct access to the DOM using ElementRef and Renderer2. When it comes to directly manipulating the DOM in a web application, there are several important considerations and best practices to keep in mind. Here's an overview of DOM manipulationsin Angular.
Angular forms in depth: Template driven, Reactive Forms & difference b/w TDF & RF
A mind map about Directives Mindmap.
In Angular, DOM manipulations are typically handled through the framework's templating and data binding features, as well as through direct access to the DOM using ElementRef and Renderer2. When it comes to directly manipulating the DOM in a web application, there are several important considerations and best practices to keep in mind. Here's an overview of DOM manipulationsin Angular.
Angular forms in depth: Template driven, Reactive Forms & difference b/w TDF & RF
Directives
What are Directives?
Directives are instructions in the DOM, they help in manipulating DOM
Types of Directives
Component Directive
Directives With the View
Attribute Directive
Change the appearance or behavior of an element, component, or another directive.
Structural Directive
Change the DOM layout by adding and removing DOM elements
Built-in Directives
Attribute Directives
ngClass
Adds and removes a set of CSS classes.
Implementation
<div [ngClass]="{redColor:yes,fontSize36:yes}">HELLO</div>
yes:boolean=true;
.redColor{ color:red;}.fontSize36{ font-size: 36px;}
ngStyle
Adds and removes a set of HTML styles.
Implementation
<div [ngStyle]="{color:'green',fontSize:'20px'}">HELLO</div>
CamelCase
ngModel
Adds two-way data binding to an HTML form element.
Structural Directive
*ngIf
When NgIf is false, Angular removes an element and its descendants from the DOM. Angular then disposes of their components, which frees up memory and resources.
<app-item-detail *ngIf="isActive" [item]="item"></app-item-detail>
When the isActive expression returns a truthy value, NgIf adds the ItemDetailComponent to the DOM. When the expression is falsy, NgIf removes the ItemDetailComponent from the DOM and disposes of the component and all of its sub-components.
If Else
<div *ngIf="!yes;else elseBlock">HELLO</div>
<ng-template #elseBlock> <div> OYE HOYE! </div></ng-template>
IfThenElse
<div *ngIf="yes;then ifBlock else elseBlock"></div>
<ng-template #ifBlock> <div> UMM HMMM! </div></ng-template>
<ng-template #elseBlock> <div> OYE HOYE! </div></ng-template>
*ngSwitch
<ul [ngSwitch]="user"> <li *ngSwitchCase="'Sanjay0'">Sanjay</li> <li *ngSwitchCase="'Sanjay1'">Sanjay1</li> <li *ngSwitchCase="'Sanjay2'">Sanjay2</li> <li *ngSwitchDefault>USER</li></ul>
If matches multiple elements ,then renders multiple elements
If no one matches, shows the default one!
*ngFor
Angular uses strict equality operator to look for changes, thus forces us to use immutables.
How does ngFor work when we add or remove elements from the list?
As the input list gets modified, ngFor will try to avoid to constantly create and destroy the DOM elements of the list, as this is an expensive operation
when we pass ngFor a new list, this does not mean that the whole list will be re-built, (meaning all the DOM re-created).
Many of the existing DOM elements will be reused and only some values inside them will be overwritten, and the decision is taken for each element in the list separately.
In order to take that decision Angular needs to identify each list element in a unique way, because for example if we pass in a new list with a different order, Angular will try to identify the elements and re-order the DOM elements of the list without deleting them and recreating them.
How are list items tracked by default?
ngFor by default tracks list items using object identity.
This means that if you build a list of new objects from scratch with the exact same values as the previous list and pass this newly built list to ngFor, Angular will not be able to tell that a given list item is already present or not.
From a point of view of object identity, the new list contains a whole new set of items, completely different from the previous set. This is the case if for example we query the data again from the backend.
Tracking by object identity is a good default strategy because Angular has no information about the object so it cannot tell which property it should use for tracking.
Why can this be important for performance?
ngFor already does a lot of optimizations out-of-the-box to try to reuse existing DOM elements as much as possible, but it's doing so based on object identity.
In the case of templates with large lists, or lists that occupy a large part of the screen, we might even with those optimizations still run into performance issues and notice that the UI is slow due to the large amount of DOM elements being created and destroyed.
If that happens to be the case, we can configure ngFor to do the tracking by something else other than object identity.
How to use trackBy?
We can provide our own mechanism for tracking items in a list by using trackBy. We need to pass a function to trackBy, and the function takes a couple of arguments, which are an index and the current item:
trackById(index:number,user:any) { return user.id; }
<ul [ngClass]="{black:blackTheme}"> <li *ngFor="let user of users;trackBy:trackById"> {{user.name}} </li></ul>
One Structural Directive per element?
Attribute Directives
Defintion
Change the appearance or behavior of DOM elements and Angular components
Examples: ngStyle, ngClass
Creating Custom Attribute Directive
1. To be able to change the appearance/behavior of DOM element or Component we need to get a reference to that DOM Element/Component.
2. import ElementRef from @angular/core. ElementRef grants direct access to the host DOM element through its nativeElement property.
3. Add ElementRef in the directive's constructor() to inject a reference to the host DOM element, the element to which you apply attribute directive.
import { Directive, ElementRef } from '@angular/core';@Directive({ selector: '[appHighlight]'})export class HighlightDirective { constructor(private el: ElementRef) { this.el.nativeElement.style.backgroundColor = 'yellow'; }}
4. Instead of setting the properties directly on the native element it is preferred to use Renderer to change native element properties
5.Applying an attribute directive
<p appHighlight>Highlight me!</p>
6. Handling User Events
@Directive({ selector: '[appHighlight]'})export class HighlightDirective { constructor(private el: ElementRef) { } @HostListener('mouseenter') onMouseEnter() { this.highlight('yellow'); } @HostListener('mouseleave') onMouseLeave() { this.highlight(''); } private highlight(color: string) { this.el.nativeElement.style.backgroundColor = color; }}
7. Passing Input values to Attribute directive
@Input() appHighlight = '';
<p [appHighlight]="color">Highlight me!</p>
Structural Directives
1. Definiton
Structural Directives helps us manipulate DOM.
They can be used to dynamically render content , iterate over lists etc.
Structural directives uses ng-template under the hood.
<p *ngIf="condition> Content </p>
It Is Syntactic Sugar For :
<ng-template [ngIf]="condition> <p>Content</p> </ng-template>
So Basically, Structural directives are same as Attribute Directives
Angular wraps the host element on which the structural directive is applied inside ng-template and then consumes the ng-template in finished DOM with Diagnostic Comments
<ng-template> are lazy and their content isn't rendered by default. So we have to tell Angular explicitly if we want to render it.
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd"> ({{i}}) {{hero.name}}</div><ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById"> <div [class.odd]="odd">({{i}}) {{hero.name}}</div></ng-template>
With <ng-template>, you can render the content manually with full control over how the content displays.If there is no structural directive and you wrap some elements in an <ng-template>, those elements disappear.
Our structural directives are also nothing more than attribute directives. Behind the scenes they are also converted in to attribute directives and are applied to an <ng template> not on host element(Important thing to remember). They then instantiate that template for us.
2. Creating Custom Structural Directive
1. To Create Custom Structural Directive we need 2 things
Reference to Container where we want to render the template( Host/Container Element). => ViewContainerRef
1.Reference to TemplateRef which we want to inject. 2.TemplateRef basically refers whats inside <ng-template>. <ng-template> elements are represented as instances of the TemplateRef class.
Both these things can be provided by Angular Dependency Injection
constructor( private viewContainerRef:ViewContainerRef, private templateRef:TemplateRef<any> ) { }
2. Taking Input in Structural Directive
1.We have to use the same name for Input() as that of our selector because
It looks like that directive is applied on the <p> DOM element, but it is actually applied on <ng-template> which wraps our DOM element. So if we use Input name other than selector name Angular will think that property is part of our DOM element instead of our Structural Directive
<p *ngIf="condition> Content </p>
is syntatic sugar for:
<ng-template [ngIf]="condition> <p>Content</p> </ng-template>
@Directive({ selector: '[hideAfterDelay]'})export class HideAfterDirective implements OnInit {@Input() hideAfterDelay:number =0;}
2. Extending the Input to something like *ngIf="condition; else placeholder"
we can use else in our *ngIf structural directives to provide other TemplateRef which we can show in case our condition is falsy. How can we do that if we want to do it in our custom structural directive or how it works internally
Challange is how to take that template as Input in our directive
1. <section class="banner" *hideAfterDelay="5000 ;then permanent;>
2. @Input('hideAfterDelayThen') placeholder:TemplateRef<any>|null=null;
Basically our directive selector name and then add prefix (Then or Else ) depending on what are we using in our template. It Should be in lowerCamelCase pattern
Implementation
private _delay:number=0; //keeping it generic as we might use async pipe and async pipe can also return null @Input('hideAfterDelay') set delay(delay:number|null) { this._delay=delay ?? 0; } //This is for accessing the then block similar to how ngIf renders else block @Input('hideAfterDelayThen') placeholder:TemplateRef<any>|null=null;
constructor( private viewContainerRef:ViewContainerRef, private templateRef:TemplateRef<any> ) { } ngOnInit(): void { this.viewContainerRef.createEmbeddedView(this.templateRef); setTimeout(()=>{ this.viewContainerRef.clear(); if(this.placeholder!=null) { this.viewContainerRef.createEmbeddedView(this.placeholder); } },this._delay) }
3. Adding Context to our Structural Directive
We can use as operator in *ngIf and *ngFor to store value in operator like *ngIf="(movies$ | async) as movies". or let i=index in our *ngFor directive Benefit is we can use movies variable in our template.
1. For this to work , we have to introduce context for our template (basically context for ng-template).
2. To Provide Context, we have to use: this.viewContainerRef.createEmbeddedView(this.templateRef,this.context);
Context - it could be any object , we can define our interface or our class
class HideAfterContext{ //TO use as operator the property name should match Directive input where we want to use as keyword For First Truthy: public hideAfterDelay:number=0;}. For Falsy Then Case public hideAfterDelayThen
private _delay:number=0; //keeping it generic as we might use async pipe and async pipe can also return null @Input('hideAfterDelay') set delay(delay:number|null) { this._delay=delay ?? 0; this.context.hideAfterDelay=this._delay; }
private context = new HideAfterContext(); this.viewContainerRef.createEmbeddedView(this.templateRef,this.context);
We can also create other context properties if we want like we have let i =index; in *ngFor directive
main> <h1>Structural Directives</h1> <section class="banner" *hideAfterDelay="5000 as time ;then permanent;let counter=counter "> <h2>Temporary Content </h2> <p>Loading your content in {{counter}} seconds</p> </section></main>
<ng-template #permanent let-hiddenAfter="hideAfterDelay"> <section class="banner"> <h2>Permanent Content is visible after {{hiddenAfter/1000}} seconds</h2> <p>Hi Sanjay</p> </section> </ng-template>
In above Code Snippet we let counter=counter; and this is a property from context which we can reuse in our template
class HideAfterContext{ //TO use as operator this property name should match Directive input where we want to use public hideAfterDelay:number=0; public counter:number=0;}
@Input('hideAfterDelay') set delay(delay:number|null) { this._delay=delay ?? 0; this.context.hideAfterDelay=this._delay; this.context.counter=this._delay/1000; }
private context = new HideAfterContext();
this.viewContainerRef.createEmbeddedView(this.templateRef,this.context);
Default values for our template by using context
use $implicit in our context to provide default values
class HideContentContext{ public get $implicit():number { return this.appHideContentAfter; } appHideContentAfter:number=0; counter:number =0;}
<h1 *appHideContentAfter="5000 as timer;later skill;let counter=counter; let val">
let val will get value from $imlicit
4. Strict Type For Directive
1. Make our TemplateRef type specific
From TemplateRef<any> to
TemplateRef<HideAfterContext>
Context Type Guard
Helps the Angular template type checker find mistakes in the template at compile time, which can avoid runtime errors
Suppose if we made typo when accessing context variable in template then Angular Compiler should warn us , it doesn't warn us by default.
static ngTemplateContextGuard(dir:HideAfterDirective,context:unknown) : context is HideAfterContext { return true ; }
Basically if above function returns true then context is of type HideAfterContext else not. This can be used also when we have inputs of Union Types. Check TypeScript Narrowing => TypePredicate
5.Full Implementation
class HideAfterContext{ //TO use as operator this property name should match Directive input where we want to use public hideAfterDelay:number=0; public counter:number=0;}
@Directive({ selector: '[hideAfterDelay]'})export class HideAfterDirective implements OnInit { private _delay:number=0; //keeping it generic as we might use async pipe and async pipe can also return null @Input('hideAfterDelay') set delay(delay:number|null) { this._delay=delay ?? 0; this.context.hideAfterDelay=this._delay; this.context.counter=this._delay/1000; } //This is for accessing the then block similar to how ngIf renders else block @Input('hideAfterDelayThen') placeholder:TemplateRef<HideAfterContext>|null=null; private context = new HideAfterContext();
constructor( private viewContainerRef:ViewContainerRef, private templateRef:TemplateRef<HideAfterContext> ) { } static ngTemplateContextGuard(dir:HideAfterDirective,context:unknown) : context is HideAfterContext { return true ; }
ngOnInit(): void { this.viewContainerRef.createEmbeddedView(this.templateRef,this.context); const intervalId= setInterval(()=>{ this.context.counter -=1; },1000) setTimeout(()=>{ this.viewContainerRef.clear(); clearInterval(intervalId); if(this.placeholder!=null) { this.viewContainerRef.createEmbeddedView(this.placeholder,this.context); } },this._delay) }
Why Structural Directives use *?
* in the structural directives lets angular know that its a structural directive (& not an attribute directive) and use of ng-template is required.
One Structural Directive per Elements?
Practice
Create One Structural Directive which shows your data after some delay(configurable) when condition is truthy. Also we want it to accept Else like *ngIf.
Make it Strict by adding ContextTypeGuard
Check in Frontend-Concepts git repo