We hope that the previous lessons helped you understand directives better. Hopefully, these lessons have created confidence when it comes to implementing custom directives that improve the codebase in future projects!

In this lesson, we’ll quickly go through all the fantastic features of directives that we’ve learned about. Consider this lesson a cheat sheet with all the key points about directives and how to use them.

Directives

Angular distinguishes these three types of directives:

  • Components are directives that have an HTML template.

  • Attribute directives can change the appearance or behavior of any element in Angular. For instance, this directive can highlight some words in the text. The directive NgStyle is an example of an attribute directive.

  • Structural directives can change the DOM layout during runtime by adding or removing DOM nodes. The NgIf directive, for example, is a structural directive.

It’s important to remember that a directive is just a simple class with a special decorator. Here’s an example of a boilerplate for a directive class:

@Directive({
  selector: '[myDirective]'
})
export class MyDirective {
}

We can use a directive by applying it to the element in the template, as illustrated in the following code block:

<div myDirective> Testing my directive </div>

The precise syntax of applying the directive depends on the directive’s selector. The selector can be defined in many ways. Here’s a list of available selectors:

  • The element-name selector is used to select by element name.

  • The .class selector is used to select by class name.

  • The [attribute] selector is used to select by attribute name.

  • The [attribute=value] selector is used to select by attribute name and value.

  • The :not(sub_selector) selector is used to select only if there’s no match with sub_selector.

  • The selector1, selector2 selector is used to select if there are selector1 and selector2 selectors that match.

Built-in attribute directives

In Angular, there are many built-in directives we can use. We have to remember that sometimes we may need to import a proper module. Some of the most popular built-in attribute directives are given below:

  • We use the NgClass directive to provide a dynamic set of CSS classes to an element. Here’s an example of setting classes to a button element:

    @Component({
      selector: 'app-button',
      template: `<button [ngClass]="buttonClass">Hello</button>`,
    })
    export class ButtonComponent {
      buttonClass = 'button button-primary';
    }
    
  • We use the NgStyle directive to provide a dynamic set of direct CSS styles to an element. Here’s an example of setting styles dynamically, depending on the value of the Input property:

    @Component({
      selector: 'app-text',
      template: `<p [ngStyle]="{'fontWeight': fontWeightStyle }">Hello there</p>`,
    })
    export class TextComponent {
      @Input() isBold: boolean = false;
    
    	get fontWeightStyle(): string {
    		if(this.isBold) {
    			return 'bold';
    		} else {
    			return 'normal';
    		}
    	}
    }
    
  • We use the NgModel directive to apply a nice two-way data binding model to HTML input elements. We may use double one-way data binding if that suits our needs better. Here’s an example of using two-way data binding on the input element:

    @Component({
      selector: 'app-form',
      template: `<input type="text" [(ngModel)]="name">`,
    })
    export class FormComponent {  
    	name: string = 'John';
    }
    

Built-in structural directives

Angular also provides us with a set of structural directives that we can use. The most commonly used ones are as follows:

  • We use the NgIf directive to take control over rendering elements. The elements with this directive applied are added to the DOM only when the provided condition is met. There’s also a possibility of supplying an optional alternative template to render if the condition fails. Here’s an example of the alternative template:

    @Component({
      selector: 'app-user',
      template: `
    			<p *ngIf="user; else noUser"> Hello {{ user.name }}! </p>
    
    			<ng-template #noUser> Hello unknown! </ng-template>
    	`,
    })
    export class UserComponent implements OnInit {  
    	user;
    
    	constructor(private authService: AuthService){}
    
    	ngOnInit() {
    		this.user = this.authService.getUser();
    	}
    }
    
  • We use the NgFor directive to create multiple elements by iterating over a provided array. The directive also gives access to extra features, such as information about the index of each element. It can also tell us whether it is in an odd or even position, or the first or the last. If we deal with big data, we may need to consider using the trackBy function to optimize the view. Here’s an example of this directive rendering a user’s lists:

    @Component({
      selector: 'app-users',
      template: `<p *ngFor="let user of users"> Hello {{ user }}! </p>`,
    })
    export class UsersComponent {
      
    	users: User[] = [
    		{ name: 'John' },
    		{ name: 'Jane' },
    		{ name: 'Ann' },
    	];
    }
    
  • We use the NgSwitch directive to conditionally render elements depending on the precise value of the provided condition. The NgSwitch is actually a set of three directives that are used together. Here’s an example of this directive displaying a correct element based on the exact value of the input property:

    @Component({
      selector: 'app-forecast',
      template: `
    		<section [ngSwitch]="forecast">
    			<p *ngSwitchCase="'sun'"> Sunny </p>
    			<p *ngSwitchCase="'rain'"> Rainy </p>
    			<p *ngSwitchCase="'cloud'"> Cloudy </p>
    			<p *ngSwitchCase="'snow'"> Snowy </p>
    			<p *ngSwitchDefault> Unknown </p>
    		</section>
    `,
    })
    export class ForecastComponent {
      
    	@Input() forecast: 'sun' | 'rain' | 'cloud' | 'snow'; 
    }
    

Generate a directive class

If we want to implement a custom directive, the first thing to do is to create a file. Angular CLI can provide us with a nice boilerplate for a directive along with its test file. To generate the file using Angular CLI, we use the following command:

ng generate directive app-avatar

Or, we may use its shortened version:

ng g d app-avatar

Change appearance

To manipulate the properties of the DOM’s element, we can inject ElementRef to get the instance of the host element. The host element is the element where the directive is applied. Here’s an example:

@Directive({
  selector: '[appAvatar]'
})
export class AvatarDirective implements OnInit {

  constructor(private elementRef: ElementRef) { }

	ngOnInit() {
		this.elementRef.nativeElement.style.border = '1px solid black';
	}
}

Pass data

Often, we need to pass data into the directive to make it more customizable and generic. To pass the data from a template where it’s applied into the directive, we can use Input properties. Below is an example that improves upon the previous snippet idea by making it far more customizable:

@Directive({
  selector: '[appAvatar]'
})
export class AvatarDirective implements OnInit {

	@Input() borderColor: string = 'black';

  constructor(private elementRef: ElementRef) { }

	ngOnInit() {
		const border = `1px solid ${this.borderColor}`;
		this.elementRef.nativeElement.style.border = border;
	}
}

We apply these properties as follows:

<img src="avatar.png" appAvatar [borderColor]="'#ff35aa'"> 

Single input syntax

If we want to use a single attribute to apply the directive to the element and bind its input in the same call, we name the property the same as we named the selector:

@Directive({
  selector: '[appAvatar]'
})
export class AvatarDirective {

	@Input() appAvatar: string = 'black';

  ...
}

This is how we can apply it:

<img src="avatar.png" [appAvatar]="'#ff35aa'"> 

We should remember that the following syntax for defining the Input property is also available:

@Input('appAvatar') color: string = 'black';

Listen for DOM events

If we want to react to DOM events, such as clicks, we can use the HostListener technique:

@Directive({
  selector: '[appAvatar]'
})
export class AvatarDirective {

	@HostListener('click', ['$event'])
	onClick(event: Event) {
		console.log('I was clicked!');
	}

}

Send custom events

When we want to pass information to other components (for example, the component that uses the directive), we can use the Output properties with EventEmitters, like this:

@Directive({
  selector: '[appAvatar]'
})
export class AvatarDirective {

	@Output() action = new EventEmitter();

	@HostListener('click', ['$event'])
	onClick(event: Event) {
		this.action.emit('I was clicked');
	}

}

and the component can handle the events as follows:

<img src="avatar.png" appAvatar (action)="onImageAction($event)" > 

Asterisk syntax *

To mark a directive as structural for Angular so that it uses the element as a template and doesn’t render it directly, we use the asterisk symbol as a prefix:

<div *yourDirective> </div>

Render templates

We inject TemplateRef and ViewContainerRef to get access so we can manipulate the view:

	constructor(
		private templateRef: TemplateRef<any>,
		private viewContainerRef: ViewContainerRef,
	){}

We use TemplateRef and ViewContainerRef to render the element based on its template in the view. The rendered element or view is placed in the view container.

this.viewContainerRef.createEmbeddedView(this.templateRef);

To clear the view container and remove all added nodes, we call the clear function on ViewContainerRef.

this.viewContainerRef.clear();

That’s all about directives in Angular! We hope you enjoyed learning about them and are ready to put them to use. We wish you many fantastic uses of directives in your projects!

Get hands-on with 1200+ tech skills courses.