TU Tran

Technologies should serve for business purpose.

NAVIGATION - SEARCH

Angular2 - Directives

What is directive?

Directive is a way we separate the re-usable function/ feature.

Can you explain more detail?

Back to the list of users, I want background color of first-name column of user who named "Tu" will be set to red. the UI is as below:

So I need to update template (html file) as below:

It is ok, if we only have 1 list of users.

If the list of users was used many places. this code was repeatedly . the code will be hard for maintenance.

In this case, we will define new item (called directive in angular). We will check and highlight the background of that column in side that directive.

So Html will be changed to:

By this way, anywhere needs to highlight the background base on first-name of user, just attach "hightlightBaseOnFirstName" as photo above.

In the future, we want to change the logic of this. just update in hightlightBaseOnFirstName directive. new change will affect to everywhere uses this directive.

It seems, we create many files just replace 1 single line of html. does it necessary?

For small application, just some pages and did not need to extend function in future. Yes, we may not need to create directive for all.

But for bigger application, where the function will be extend in future and a lot of features were re-used across the application. separated re-usable function/ feature into isolated component/ directive is important.

This avoid duplicated code and easy for maintenance the code. There are many enterprise application was not well organized, it is costly to append new feature.

How many types of directive?

We have 3 types of directive at this time. They are:

  • Attribute Directive
  • Structural Directive
  • Component (directives with a template)

How does attribute directive look like?

I assume my attribute directive named "hightlightBaseOnFirstName", we will use it as attribute of html element:

To declare this type of directive, we need to create hightlightBaseOnFirstName.ts file as below:

@Directive({
    selector: "[hightlightBaseOnFirstName]"
})
export class HightlightBaseOnFirstName{
    constructor(){
        console.log("this is HightlightBaseOnFirstName directive");
    }
}
Navigate to "<root url>/users", we can see "this is HightlightBaseOnFirstName directive" was printed to console of browser:

What is @Directive?

This is angular decorator. we use this decorator for defining an directive (the same meaning as @Component).

We need to aware on "selector" property. it was in "[name-of-directive]" pattern  ("[" and "]" were required).

Selector declare the name of directive that will be reference in the application.

In above sample, we can use our highlight directive by using "hightlightBaseOnFirstName", for example:

<div hightlightBaseOnFirstName ><div>

or

<p hightlightBaseOnFirstName></p>

I have been following your instruction in this article, but my directive does not work?

To this point, we just declare new directive named 'hightlightBaseOnFirstName" and use this directive in users.html template.

At compile time, Angular understands "hightlightBaseOnFirstName" as normal attribute of html tag. So we need to register this directive to angular. Thus "hightlightBaseOnFirstName" will be treated as name of directive instead of normal html attribute.

Go to userModules.ts file, add this code:

Let try to compile and refresh the browser, We will see "this is HightlightBaseOnFirstName directive" was printed to the console of browser:

How can I get reference to DOM object that my directive currently attached to?

Most case, we want to perform some actions over DOM element that our directive is current attached, such as: add event listener, change markup, ....

Let change constructor of our directive to:

constructor(ui: ElementRef){
	console.log(ui);
}

At the run-time, Angular injects the reference to object that our directive currently attached to.

Run the app, we receive from the console of browser as below:

We can also update background of this dom element by:

constructor(ui: ElementRef) {
	ui.nativeElement.style.backgroundColor = "red";
}

 

Ok, cool. The next question is: How can I pass parameter to my directive?

Our directive can be called from other component (such as: Users component, or in other directive). So our directive can receive the parameters (input parameters) from those and determines its reaction based on these input parameters.

Let review again our "hightlightBaseOnFirstName" directive. I will add "color" input parameter. this value will be used for setting the background of appropriated row/ user in the list.

In hightlightBaseOnFirstName.ts will be changed as below:

@Directive({
    selector: "[hightlightBaseOnFirstName]"
})
export class HightlightBaseOnFirstName implements AfterViewChecked {
    @Input() color: string = "white";
    ngAfterViewChecked(){
        console.log(this.color);
    }
}

In this class, we add new @Input parameter named "color". So other component/ directive use "hightlightBaseOnFirstName" directive can pass appropriated color using this property.

What is "ngAfterViewChecked"?

this is event of angular component/ directive, ... this let us can specify what we want our directives do when an event occurs.

Please just focus on directive for now, We will discuss more detail about those event (or Life-Cycle Hooks) in other article.

Agree, How can I pass the value for color input property of "hightlightBaseOnFirstName" directive?

We need to update in users.html as below:

<td hightlightBaseOnFirstName [color]="user.color">{{user.firstName}}</td>

In this html, we add new "[color]='user.color'", this will set the color of "hightlightBaseOnFirstName" directive by "color" property of each user.

Run the app again, we will see the color was printed to console as below:

I see, Can you complete "hightlightBaseOnFirstName" directive. Just receive color and set background appropriately?

The completed code for "hightlightBaseOnFirstName" directive is as below:

@Directive({
    selector: "[hightlightBaseOnFirstName]"
})
export class HightlightBaseOnFirstName implements AfterViewChecked {
    @Input() color: string = "white";
    private dom: any = null;
    constructor(ui: ElementRef) {
        this.dom = ui.nativeElement;
    }
    ngAfterViewChecked() {
        let firstName = this.dom.innerText;
        if (firstName.indexOf("Tu") < 0) { return; }
        this.dom.style.backgroundColor = this.color;
    }
}

Please pay attention on these points:

  • We get reference to DOM object which our directive was attached to and hold this reference in private property (named ui).
  • In ngAfterViewChecked, we check the logic and update appropriated value for backgroundColor property of current dom object.

The result is as below:

In this photo we see that, the fourth item (techcoaching) has default background color (white) as its first-name was not started with "Tu".

How can I pass more than 1 parameter into my directive?

Let update our directive that will receive another parameter named "text", so the code of HightlightBaseOnFirstName class will be changed as below:

export class HightlightBaseOnFirstName implements AfterViewChecked {
    @Input() color: string = "white";
    @Input() text: string = "Tu";
    private dom: any = null;
    constructor(ui: ElementRef) {
        this.dom = ui.nativeElement;
    }
    ngAfterViewChecked() {
        let firstName = this.dom.innerText;
        if (firstName.indexOf(this.text) < 0) { return; }
        this.dom.style.backgroundColor = this.color;
    }
}

In this code, we added new "text" parameter was declared as @Input also. users.html also needs to be updated:

<td  hightlightBaseOnFirstName [text]="'Tu'" [color]="user.color">{{user.firstName}}</td>

We add new "[text]=<value>" into td tag. Run the app again, the output on browser is the same:

Ok, Understand. There is another problem in above sample. In hightlightBaseOnFirstName directive, color and text both are text and marked as @Input string. in the markup for color is [color]="user.color", but for text is [text]="'TU'". While we have double quote outside single quote for text property?

This is a really nice question. In Angular, when binding a text value, we should place it inside '' (single quote). Otherwise, Angular understand the value between "" (double quote) is a variable. The application may not behave as expected:

First case, use single word in "":
<td  hightlightBaseOnFirstName [text]="Tu" [color]="user.color">{{user.firstName}}</td>

At run-time, the value mapped to text property of directive is "undefined", as "Tu" is unknown variable:

If we have the variable defined in Users class (users.ts):

export class Users {
    public Tu:string="Tran Thanh Tu";
    ....
}

Run the app again, the output was changed:

We have this case, because, "Tu" was understand as the variable in context of Users class (users.ts and users.html).

Second case, use multiple words in "":

Angular will raise error in console of browser as the variable name with white space is invalid:

For fixing this issues, just add this text inside a single quote. it will work well:

For binding a string value to text property. Can I use '"Tu Tran"' (single quote outside of double quote) instead of "'Tu Tran'" (double quote outside of single quote)?

Definitely yes, "(double quote) and ' (single quote) were interchangeable.

It is cumbersome to remember single quote and double quote. Is there other solution release developer from this?

Yes, we have. There is another nice solution for string property. instead of using:

<td  hightlightBaseOnFirstName [text]="'Tu Tran'"></td>

We can use:

<td  hightlightBaseOnFirstName text="Tu Tran"></td>

In this case, we change from [text]="'Tu Tran'" to text="Tu Tran". Angular will treat value as string event if it is variable in context. See the code below:

<td  hightlightBaseOnFirstName text="user.color">{{user.firstName}}</td>

Angular still understands user.color is a string not a binding variable:

We need to put user.color inside "{{" and "}}" (it means {{user.color}}) will fix the problem.

In above sample, we need to write much code (<td  hightlightBaseOnFirstName [color]="user.color"></td>) for setting the background color, can we make it shorter?

yes, there is an other concept in Angular, it is @Input alias. We will change HightlightBaseOnFirstName class to:

export class HightlightBaseOnFirstName implements AfterViewChecked {
    @Input("hightlightBaseOnFirstName") color: string = "white";
	/* the rest of code was not changed*/
}

And changed in users.html to:

<td  [hightlightBaseOnFirstName]="user.color"></td>

The output of the app was the same:

How can my directive publish change to outside?

  • We need this case for notify an expected event to the caller which uses our directive. We need to follow the order as below:
  • Declare that property with @Output and type of EventEmitter. For example @Output output: EventEmitter<bool> = new EventEmitter()).
  • Publish change by calling emit method of EventEmitter instance. For example: outout.emit(true);
  • Using that output property by (<name of output variable>)=callback($event), this callback function will be called when directive calls to emit function of that output property.
  • Your logic will be implemented in above callback function.

It is hard to understand your answer above.  Could you please create a concrete sample?

Ok, let update our sample.

In HightlightBaseOnFirstName class, we add new output property:

export class HightlightBaseOnFirstName implements AfterViewChecked {
    @Output() isCustombackground: EventEmitter<boolean> = new EventEmitter();
	/*The remain code is the same */
}

and also update ngAfterViewChecked function as below:

ngAfterViewChecked() {
	let firstName = this.dom.innerText;
	if (firstName.indexOf(this.text) < 0) { return; }
	this.dom.style.backgroundColor = this.color;
	this.isCustombackground.emit(true);
}

In our directive, if the first-name of attached DOM start with "TU", isCustombackground will be fire with true.

In users.html, map output property to callback function:

<td hightlightBaseOnFirstName (isCustombackground)="onBackgroundChanged(user, $event)">{{user.firstName}}</td>

We see that isCustombackground output property was called in "(<name of property>)" pattern. It is difference to @Input property.

In this case, onBackgroundChanged function will be called with 2 parameters (first is the current user, second is the $event in current context). $event is value passed in emit function inside directive.

Go to Users class and add logic of onBackgroundChanged function as below:

public onBackgroundChanged(user: any, isCustom: boolean) {
	user.isCustomBackground=isCustom;
}

The logic is rather simple, just set isCustomBackground property of current user to appropriated value passed in emit function.

Let compile and run the app.

Click on #1 ("Tu Tran" user), we see below result:

Click on #4 ("Tech Coaching" user), the result is as below:

Got it, We write many code just for update 'isCustomBackground' property of user. Is there any other better solution?

Of course, There is convention for output property. new value will be update directly into mapped property.

let update the sample code above for isCustombackground output property. Let change HightlightBaseOnFirstName class to:

export class HightlightBaseOnFirstName implements AfterViewChecked {
    @Input() color: string = "white";
    @Input() text: string = "Tu";
    @Input() isCustombackground: boolean;
    @Output() isCustombackgroundChange: EventEmitter<boolean> = new EventEmitter();
    private dom: any = null;
    constructor(ui: ElementRef) {
        this.dom = ui.nativeElement;
    }
    ngAfterViewChecked() {
        let firstName = this.dom.innerText;
        if (firstName.indexOf(this.text) < 0) { return; }
        this.dom.style.backgroundColor = this.color;
        this.isCustombackgroundChange.emit(true);
    }
}

In this code:

  • we add new @Input parameter named isCustombackground type of boolean.
  • We also add new @Output parameter named isCustombackgroundChange type if EventEmitter<boolean>

There is convention for this: @Output parameter = @Input parameter + "Change".

We also need to update users.html:

<td hightlightBaseOnFirstName [(isCustombackground)]="user.isCustomBackground">{{user.firstName}}</td>

We use isCustombackground property in difference syntax: [(<name of input property>)]

In this case, we did not need any callback function in Users class as before. Just remove onBackgroundChanged function in Users class in previous sample.

Let compile and clear the cache of you browser. Just make sure your html files, js files were not out-of-date. The result is the same as sample above:

I see that we have 2 solution for doing the same thing (update isCustomBackground property of appropriate user). Do I need to know both of them?

Yes, you should know.

For the first one, we will use in the case of firing an event to outside of our directive (such as: click of button). So the parent (component/ directive use our directive) will decide how to react to specified event.

For the second, let imagine, user object have 10 properties need to updated from HightlightBaseOnFirstName directive as similar as isCustomBackground property. Follow the first, we need to define 1 callback function, just do the simple thing as "user.isCustomBackground = isCustom;". this will make the code complex unnecessary. So, the second is the right solution for this case.

For learning Angular2, I think you should follow the list of articles as below (click on link to see detail page):

  • Overview: Introduce about Angular2
  • Routing: Understand how Angular2 navigate between pages/ components
  • Component: Learn about components/ pages in Angular2.
  • Binding: Learn how Angular2 show data on UI and receive input data from user.
  • Directive: Learn how to create re-usable component/ control that can be re-used across the application.
  • Integrate together: using what we learn about Angular for building the sample demo. 
  • Append "angular 2" into current "Angular 1" application.

Thank for reading.

Note: Please rate and share to your friends if you think this is usefull article, I really appreciate

Add comment