Photo by Nick Fewings on Unsplash
How I created a directive in Angular that renders components only if user is authorized?
Table of contents
No headings in the article.
Recently, I was working on an Angular project where certain components were public meaning that everyone should see them and others should be rendered only if the user is authenticated or have a specific role, so I was doing something like this:
<!-- file name: offers.component.html -->
<div>
<app-nav-bar></app-nav-bar>
<app-profile-image *ngIf="user.isAuthenticatd"
></app-profile-image>
<div>
<app-search></app-search>
<app-offers-list></app-offers-list>
</div>
<div>
<app-review-offer *ngIf="user?.roles.include('AGENT')"
></app-review-offer>
</div>
<div>
This doesn't sounds clean to me, especially the last one: *ngIf="user?.roles.include('AGENT')"
Thankfully Angular gives us the ability to create our custom directives. so let's create one!
First, we need to create a class and annotate it with @Directive
annotation (Angular CLI made it easy ๐)
ng generate directive authorized
# or use the shourhand: ng g d authorized
The above command will generate a class as follows:
// file name: authorized.directive.ts
import { Directive } from '@angular/core';
@Directive({
selector: '[appAuthorized]'
})
export class AuthorizedDirective {
constructor() { }
}
Remember we need to pass/bind the role as an input to use it in our directive something like this:
<div>
<app-review-offer *appAuthorized="'AGENT'"></app-review-offer>
</div>
Like in component classes, we will create a property
and annotate it with the @Input()
decorator.
// file name: authorized.directive.ts
import { Directive, Input } from '@angular/core';
@Directive({
selector: '[appAuthorized]'
})
export class AuthorizedDirective {
@Input appAuthorized
constructor() { }
}
Now we want to perform our login whenever the appAuthorized
input is changed, for this, we will use a setter:
// file name: authorized.directive.ts
import { Directive, Input } from '@angular/core';
@Directive({
selector: '[appAuthorized]'
})
export class AuthorizedDirective {
@Input set appAuthorized(role: string){
console.log(role) // this will print: AGENT
}
constructor() { }
}
// file name: offers.component.html
//...
<div>
<app-review-offer *appAuthorized="'AGENT'"></app-review-offer>
</div>
//...
//...
The above code will simply print AGENT
in the console
** Note that appAuthorized
is still a property learn more about component-interaction
To proceed we need to inject some objects:
TemplateRef : to access our template
Note that:
<app-review-offer *appAuthorized="'AGENT'"></app-review-offer>
Is a shourthand of:
<ng-template>
<app-review-offer [appAuthorized]="'AGENT'"></app-review-offer>
</ng-template>
ViewContainerRef : to create a new embedded view.
AuthService: This one could be different from one project to another, for me, I have a service called
authService
where I had stored the currently logged-in user with all its information such as username, roles...
import {Directive, Input, OnInit, TemplateRef, ViewContainerRef} from '@angular/core';
import {AuthService} from "../services/auth/auth.service";
import {Principal} from "../models/principal";
@Directive({
selector: '[appAuthorized]'
})
export class AuthorizedDirective {
@Input set appAuthorized(role: string){
console.log(role) // this will print: AGENT
}
constructor(
private auth: AuthService,
private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef
) { }
}
And finally, add the logic that handles the authorization:
import {Directive, Input, OnInit, TemplateRef, ViewContainerRef} from '@angular/core';
import {AuthService} from "../services/auth/auth.service";
import {Principal} from "../models/principal";
@Directive({
selector: '[appAuthorized]'
})
export class AuthorizedDirective {
@Input() set appAuthorized(role: string){
this.auth.principal$.subscribe({
next:(value)=>{
// Check if user is authenticated
if(!(<Principal>value).authenticated){
this.viewContainerRef.clear();
return;
}
// Check if the user have the given role
if (role !== '*' && !(<Principal>value).roles.includes(`ROLE_${role}`)){
this.viewContainerRef.clear();
return;
}
this.viewContainerRef.createEmbeddedView(this.templateRef)
}
})
}
constructor(
private auth: AuthService,
private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef
) { }
}
As you noticed in the above code we used the clear method this.viewContainerRef.clear()
to destroy the view if the user is not authenticated or doesn't have the desired role.
We also used this.viewContainerRef.createEmbeddedView(this.templateRef)
to instantiate an embedded view and inserts it into our container.
Now we can use our directive anywhere in our application:
<!-- file name: offers.component.html -->
<div>
<app-nav-bar></app-nav-bar>
<app-profile-image *appAuthorized="'*'"
></app-profile-image>
<div>
<app-search></app-search>
<app-offers-list></app-offers-list>
</div>
<div>
<app-review-offer *appAuthorized="'AGENT'"
></app-review-offer>
</div>
<div>
That was it, I hope you found it helpful. If you have any feedback or suggestions for improvements, I would like to hear from you.