Customized Structural Directives

Let’s learn how to implement the custom structural directive.

The ability to create custom structural directives is usually handy in reducing code duplications. In many applications, we want to show or hide elements of the component’s template while adhering to some conditions. Often, these conditions are related to a business’s specific requirements, and they apply to many elements across the application.

A use case for premium accounts

As an example, we’ll focus on a particular use case in this lesson. Let’s consider an application that supports two kinds of user accounts—standard and premium. Here are the differences between these two accounts:

  • Premium users have access to everything in the application.
  • Standard users have limited access to some features.

Some features can be guarded for premium users at the route level, but some rules apply to particular elements on the page. For instance, there may be a button that performs an action that is only available to premium users. In that case, we want to hide that button from users with a standard account. First, we define the account interface, as shown below:

export interface Account {
	username: string;
  premium: boolean;
	fullName: string;
}

Then, we need a service to persist some information about the currently signed-in account. Let’s call this service AccountService, like this:

@Injectable()
export class AccountService {
	currentAccount: Account;
}

We shouldn’t worry about how the data is populated. Let’s assume that there is some other service that sets the current account so that it always has the newest data.

In the component, we can use AccountService as follows:

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
	account: Account;

	constructor(private accountService: AccountService){ }

	ngOnInit() {
        // get account from the service 
		this.account = this.accountService.currentAccount;
	}
	
}

So far, everything looks standard and straightforward. Now, we just need a dashboard template with a button for premium users only:

<button (click)="doAction()"> 
	I'm only for premium 
</button>

Now, let’s think about how to hide the button from non-premium users. Well, we can use NgIf to do this.

<button (click)="doAction()" *ngIf="account.premium"> 
	I'm only for premium 
</button>

When account.premium evaluates to true, the button renders, and, if it evaluates to false, the button won’t render in the DOM. This is exactly what we want!

However, let’s imagine that there are several elements across the application that need this exact type of guard. This results in two kinds of duplications:

  • Duplications in the template occur because each time we need to hide something, we need to copy the same code.

    *ngIf="account.premium"
    
  • Duplications in the component’s class occur because each time we need to inject the service, we need to create a property that stores account and supplies that property with the current value from the service.

    account: Account;
    
    constructor(private accountService: AccountService){ }
    
    ngOnInit() {
    	this.account = this.accountService.currentAccount;
    }
    

This may not sound like a big deal, but there will be tons of duplicated code after a while. Each time a new feature is added, we need to copy and paste the same chunk of code in two places. That seems inconvenient. So, what can we do about it?

The custom NgIf directive

We can create a custom directive that meets our needs precisely by preventing excessive code duplication. This custom directive is similar in a way to NgIf. However, this directive wraps the common logic and exposes only the necessary properties to use it.

Let’s create this directive class file. Just like before, we can get help from the Angular CLI and create a directive using the command below:

ng generate directive premium-only

Or, we can create a file by ourselves.

Whichever path we choose, we should end up with the class below:

import { Directive } from '@angular/core';

@Directive({ selector: '[appPremiumOnly]'})
export class PremiumOnlyDirective {
}

Now, we just need to implement the logic inside the directive class. Let’s remember that we have access to the current account data and its type through AccountService. So, right now, we need to figure out how to either implement the rendering element in the DOM or remove it from the directive.

TemplateRef and ViewContainerRef classes

Finally, we use the TemplateRef and the ViewContainerRef classes. Let’s start by injecting the instances of these two classes into our directive:

@Directive({ selector: '[appPremiumOnly]'})
export class PremiumOnlyDirective {

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

}

When we apply appPremiumOnly to any element, Angular wraps it with its content in the ng-template. By using templateRef in our directive, we get access to the ng-template content. We can use this to create the actual element and insert it into the DOM through the viewContainerRef.

How can we create a view and insert it into the DOM? This is the syntax for instantiating a TemplateRef class:

this.viewContainerRef.createEmbeddedView(this.templateRef);

On the other hand, if we want to clear the view and remove the element entirely, we can simply clear the view container of any views that are inside. We do this by calling the following:

this.viewContainerRef.clear();

Now, we can combine all these and implement our directive!

@Directive({ selector: '[appPremiumOnly]'})
export class PremiumOnlyDirective implements OnInit{

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

	ngOnInit() {
        // get account from the service
		const account = this.accountService.currentAccount;

		if(account.premium){
		// render the template
this.viewContainerRef.createEmbeddedView(this.templateRef);
		}else{
		// clear the view
	this.viewContainerRef.clear();
		}
	}
}

In our case, there’s not much sense in clearing the view because it’s empty anyway. However, if we plan on improving the code to react to account changes later, it’s crucial.

The code is not very complex, though it may look confusing initially. Once we understand the reason behind templates and views, it all starts to make sense.

Now, let’s watch our directive in action. We prepared an application with the following syntax mentioned in this lesson:

  • We can change AccountService to switch account types. We need to restart the application if we want to see the changes.

  • The PremiumOnlyDirective is the directive we just wrote.

  • The AppComponent has a few elements that may or may not the directive applied to them.

Remember, this is a structural directive. To apply it, we use * syntax:

<button (click)="doAction()" *appPremiumOnly> 
	I'm only for premium 
</button>

Get hands-on with 1200+ tech skills courses.