Creating a custom basic Attribute Directive in Angular

What is Hoisting in JavaScript?
In my previous post, we gone through Directive in Angular and their types. I admit that it was not a deep article about directive but we can explore it in this article.
Now will try creating our own basic attribute directive and will also learn how could we create it better.
For that, we will start with a basic project setup. So, Let's do it.
Changing DOM using Structural Directive
So we have just one main component which is App Component and will be add some html elements in the app.component.html in order to render something on the view page.
we will start with creating our very first custom attribute directive
Creating a new folder with name basic-highlight and add a new file with name basic-highlight.directive.ts
This file is not going to be any component, instead just a directive.
Changing DOM using Structural Directive
Now in this file we will export a class BasicHighlightDirective, you are free to choose your own naming convention but I will be going with the above mentioned one as it is very descriptive about what the class is. Then we will declare @Directive annotation where Directive needs to be imported from ‘@angular/core’ package. we also need to pass an object to @Directive() to configure our directive. The passed object will contain a selector tag which expects a unique name through which we will attach that with any DOM element to pass the instructions written inside our directive. You can remember it from our component directive, where we also pass a css selector to uniquely identify our component. So, our selector should have unique name.
import { Directive} from '@angular/core';
@Directive({
selector: '[appBasicHighlight]'
})
export class BasicHighlightDirective {
}
Now wrapping the value of our selector with square bracket [] will mark it as an attribute and will be recognized whenever we place this attribute on any DOM Element without using square bracket.
The next thing is we need to provide some functionality to this directive. Lets say the basic use case would be changing the element background color wherever it is being attached. For this, we need to get access to the element where it is placed or our custom directive sits on.
Angular gives us this access, we can inject the element where our directive is placed, into this directive. The injection is something like an easy way to get access to some of the classes without having them to instantiate on our own.
In this case, we just need reference to the element the directive was placed on, and this is what injection in Angular means, Angular will try to create the thing we need and provide it to us.
So, we need to import ElementRef from the same package ‘@angular/core’ and then we will list couple of arguments we want to get whenever an instance of this class is created. Here Angular is responsible for creating these instance.
export class BasicHighlightDirective implements OnInit {
constructor(private elementRef: ElementRef) {  }
we would not write anything in the constructor body, we would just accept value of ElementRef, We can access directly the element by using ElementRef.
we have placed private keyword in-front of elementRef identifier to make it as property of class and in same time assigning value to it by the instance ElementRef.
we got access to the native element, and will do something with that in the constructor.
constructor(private elementRef: ElementRef) {
// accessing the element
}
But we have a better place for it than constructor.
OnInit lifecycle hook. It also has OnDestory hook.
Other than those two above, we don’t have any other lifecycle hook since there is no view or template attached with this Directive.
Import OnInit from ‘@angular/core’ package and implement it with the class and then we can access our element as shown below.
import { Directive, ElementRef, OnInit } from '@angular/core';
@Directive({
selector: '[appBasicHighlight]'
})
export class BasicHighlightDirective implements OnInit {
constructor(private elementRef: ElementRef) {  }
ngOnInit() {
this.elementRef.nativeElement.style.backgroundColor = 'green'
}}
ElementRef is wrapper around nativeElement which is underlying elements of DOM.
Now our custom directive is ready but before using it, we need to inform angular that we have a custom directive which we want to use it inside our template. For that import this directive class in our app.module.ts and add in the declarations: [] array.
import { BasicHighlightDirective } from './basic-highlight/basic.highlight.directive';
@NgModule({
imports:[ BrowserModule, FormsModule ],
declarations: [ AppComponent, BasicHighlightDirective],
bootstrap:    [ AppComponent ],
providers: []
})
export class AppModule { }
Now let it use in our app.component.html file.
<h2 appBasicHighlight > This is Basic Highlight Directive </h2>
You will find that the background color have changed to green for this h2 element.
appBasicHighlight directive is working good as expected but we could have made it better. I am saying so because, we have used ElementRef for accessing our DOM element which is not appropriate and safe since it can lead to some security risk and other kind of vulnerability and cross site scripting. Its all because of tightly binding with elements.
One other reason could be, Angular could render our template without DOM and their these properties will not be available directly. Changing the environment may affect our application working.
Now the question is how could access our elements in different way without accessing it directly?
and the Answer is....
Renderer2
So what is Renderer2, it is nothing but a class which provide a abstraction by Angular in the form of service to deal with DOM manipulation without even touching that specific DOM. By default, Angular renders a template into DOM. But using Renderer2, we can also perform DOM manipulation in a Non-DOM environment. That means, we dont have any platform dependency and due to its abstraction, it is safe and secure in communicating with our elements.
Angular is not limited to running in the browser. It also run with service workers and these are environment where we might not have access to the DOM. So, directly accessing native elements like we did earlier will throw error in those situations.
So what can we do with Renderer2?
We can add and remove CSS classes, styles, HTML attributes , also set DOM property with a value at run time, can create or remove element, append child in-front of parent, can listen to event bound using Renderer2.
Lets create and another directive and use Renderer2
To build new directive, we use angular cli with below commands
ng generate directive better-highlight
or
ng g d better-highlight
And lets do the same with this directive but with the better approach of accessing DOM.
we still need ElementRef as a target, to provide reference of element to Renderer,
import { Directive, Renderer2, OnInit, ElementRef} from '@angular/core';
@Directive({
selector: '[appBetterHighlight]'
})
export class BetterHighlightDirective implements OnInit {
constructor(private elemRef: ElementRef, private renderer: Renderer2) { }
ngOnInit() {
 this.renderer.setStyle(this.elemRef.nativeElement, 'background-color', 'blue');
   }
 }
Of course, you can do more than simply change the styling of an element via setStyle() . we also have a listen() method over Renderer to listen the events made by users to make it interactive .
listen(target: 'window'|'document'|'body'|any, eventName: string, callback: (event: any) => boolean | void): () => void
Take a look on below example
export class BetterHighlightDirective implements OnInit, AfterViewInit {
    constructor(private elemRef: ElementRef, private renderer: Renderer2) {}
    ngOnInit() {
        this.renderer.setStyle(this.elemRef.nativeElement, 'background-color', 'blue');
    }
    ngAfterViewInit() {
        this.renderer.listen(this.elemRef.nativeElement, 'click', () => {
                //do something here
                this.renderer.setStyle(this.elemRef.nativeElement, 'color', 'red');
            }

        });
}
}
we have used AfterViewInit lifecycle hook, if you are aware with this Angular hook, you would be knowing that this lifecycle hook is only called when our comonent view gets initialized or after the view is initially rendered. This is why @ViewChild() depends on it. You can’t access view members before they are rendered.
Using HostListener decorator to Listen to Host Events
we have another way to respond to output events that occur on the host element the directive is attached to and can also manipulate the host elements by binding to its input properties. Didn’t get me? Lets us see.
Suppose, we want to react on some events occurring on the element that directive sits on, if we mouse over the element the background color should change to some green color and when I move the mouse back from the same element it should be turn to transparent as its background color.
Lets do it…. using @HostListener decorator
we have to import HostListener from ‘@angular/core’ package and then we will define a method followed by the HostListener decorator, method name could be anything, here it is mouseover and then pass an occurred event type between the parenthesis of HostListener(‘’) decorator. Event type can be anything like mouseover,
import {
    Directive,
    Renderer2,
    OnInit,
    ElementRef,
    HostListener
} from '@angular/core';
@Directive({
    selector: '[appBetterHighlight]'
})
export class BetterHighlightDirective implements OnInit {
    constructor(private elemRef: ElementRef, private renderer: Renderer2) {}
    ngOnInit() {}
    @HostListener('mouseenter') mouseover(eventData: Event) {
        console.log("Mouse over function called", eventData);
        this.renderer.setStyle(this.elemRef.nativeElement, 'background-color', 'blue');
    }
    @HostListener('mouseleave') mouseleave(eventData: Event) {
        console.log("Mouse leave function called", eventData);
        this.renderer.setStyle(this.elemRef.nativeElement, 'background-color', 'transparent');
    }
}
we can also bind with expression like we did in ngStyle. When true is assgined to the class, the class will be active on the marked element. In the case of false, it will not be active.
<h2>  [ngClass]="{classA: true}">Some Text </h2>
<h2>  [ngClass]="{classA: 2/2 == 0? true : false}">Some Text </h2>
using HostBinding() decorator to bind to Host Properties
So, we saw how using Renderer can make your task done for accessing the host element and changing its properties like background color, font-size.
We have yet another method for simply changing the background color if that is what we want to do in the directive.
Although, using Renderer was not a bad option but we should also know how HostBinding decorator can help us in this case. Event this decorator needs to be imported from ‘@angular/core’ package.
Now, with the @HostingBinding() we need to bind some property which value will become important, it could be a backgroundColor property, a new property of type string. Then in the host binding parenthesis we need to pass or define which property of the hosting element we want to bind. we could bind any property, for instance it could be “style” and there background color sub-property.
@HostingBinding(‘style.backgroundColor’) backgroundColor: string;
we basically telling Angular, on the element this directive sits, please access the style property which every element has and then its sub-properties background -color. And now we can simply change the background color using the property we have bound with the actual one.
import {
    Directive,
    HostBinding,
    HostListener,
    OnInit
} from '@angular/core';
@Directive({
    selector: '[appGoodHighlight]'
})
export class GoodHighlightDirective implements OnInit {
    @HostBinding('style.backgroundColor') backgroundColor: string;
    constructor() {}
    ngOnInit() {}
    @HostListener('mouseenter') mouseenter(eventData: Event) {
        this.backgroundColor = 'yellow';
    }
    @HostListener('mouseleave') mouseleave(eventData: Event) {
        this.backgroundColor = 'transparent';
    }
}
Binding to Directive Properties using Custom property binding
We indeed built a good directive, what we can find in our directive is that the value to our properties is hard coded and cannot be changed from outside. Lets say we want to export this directive as a third party package so the developer using this directive could change its background color dynamically by passing the value from outside.
Well! we could achieve this using custom property binding.
Lets add two properties which we will bind using @Input() decorator which needs to be imported from ‘@angular/core’
@Input() defaultColor: string = 'transparent';
@Input() highlightColor: string = 'blue';
So, now those above two values to properties could be overwritten from outside. check out the below codes….
import {
    Directive,
    OnInit,
    HostBinding,
    HostListener,
    Input
} from '@angular/core';
@Directive({
    selector: '[appBestHighlight]'
})
export class BestHighlightDirective implements OnInit {
    @Input() defaultColor: string = 'transparent';
    @Input() highlightColor: string = 'cyan';
    @HostBinding('style.backgroundColor') backgroundColor: string;
    constructor() {}
    ngOnInit() {
        this.backgroundColor = this.defaultColor;
    }
    @HostListener('mouseenter') mouseenter(eventData: Event) {
        this.backgroundColor = this.highlightColor
        this.backgroundColor = this.finalColor;
    }
    @HostListener('mouseleave') mouseleave(eventData: Event) {
        this.backgroundColor = this.defaultColor;
        this.backgroundColor = this.defaultColor;
    }
}
And in the app.component.html we can just pass the new color value
<h3 appBestHighlight defaultColor="pink" highlightColor="darkcyan">
    I am dynamic directive
</h3>
we can also bind to properties of our own directive by simply placing them on the same element.
<h3 appBestHighlight defaultColor="pink" highlightColor="darkcyan">
    I am dynamic directive
</h3>
you may be thinking , which property would apply if h3 already has css background-color properties set to some color value, and our directive is bound with some other value. In case, Angular would first figure out the applied directive properties and then it would reach to the native element properties.
<h3 style="background-color:red" appBestHighlight [defaultColor]="'#ccc'" [highlightColor]="'darkcyan'">
    I am dynamic directive
</h3>
If you have seen NgClass, directive which is itself closed in a square brackets. And that’s a typical use case especially if you have only one property to bind or at least one main property then we can provide an alias by setting value directly to the directive selector.
@Directive({
    selector: '[appBestHighlight]'
})
export class BestHighlightDirective implements OnInit {
    @Input() defaultColor: string = 'transparent';
    @Input('appBestHighlight') highlightColor: string = 'cyan';
    @HostBinding('style.backgroundColor') backgroundColor: string;
and now in the app.component.html we can directly bind the value to our directive selector.
<h3 [appBestHighlight]="'darkcyan'" [defaultColor]="'#ccc'">
    I am dynamic directive
</h3>
//or by omitting the square barcket
<h3 appBestHighlight="darkcyan" [defaultColor]="'#ccc'">
    I am dynamic directive
</h3>
Thanks for reading this article. Pleas click on the below link to continue this article

People Reaction : 0

Rohit Sharma
Name : Email : Website :
© 2020 WriteSomeCode. All Right Reserved. A Rohit Sharma Blog. Creative Commons License licensed under a Creative Commons Attribution 4.0 International License