Welcome to Angular challenges #6.
The goal of this Angular challenges series is to sharpen your abilities through practical, real-world scenarios. You can also submit your work as a PR for review by me or others, just like in an actual work environment or when contributing to Open Source Software.
Challenge number six centers on permission management inside an application. Most apps need to support varying permission levels for distinct user roles—admins, managers, and so on. Our task here involves showing or hiding specific pieces of information and limiting which routes users can access, all based on the permissions they hold. This challenge naturally breaks into two distinct segments:
- The first segment looks at different techniques for building a structural directive that manipulates our template to show or remove DOM elements.
- The second segment tackles protecting route navigation, which we handle through guards. (Details on that will come in a follow-up article)
If you haven't attempted the challenge yourself, I encourage you to try it before reading on. You can find it at Angular Challenges, then return here to compare your approach with mine. (And feel free to submit a PR for review)
We start with the provided application. It includes several buttons, each letting you log in as a user holding different permissions. There is also a set of sentences that must be toggled on or off depending on the currently logged-in user.
The User interface looks like this:
export type Role = 'MANAGER' | 'WRITER' | 'READER' | 'CLIENT';
export interface User {
name: string;
isAdmin: boolean;
roles: Role[];
}
The straightforward approach would be to slap an ngIf onto each row:
<div *ngIf="user.isAdmin">visible only for super admin</div>
<div *ngIf="!user.isAdmin && user.roles.includes('MANAGER')">
visible if manager
</div>
<div *ngIf="/*Get's complicated*/">visible if manager and/or reader</div>
//...
That approach forces us to string together a complex condition, and the logic can't be reused elsewhere. As the codebase expands, this pattern becomes increasingly tangled and difficult to maintain.
Understanding Structural Directives
Angular provides structural directives as a way to reshape the DOM layout based on runtime conditions. These directives give us the ability to insert or remove elements from the document flow. In the earlier approach, we relied on the built-in ngIf directive, which handles conditional rendering by adding or removing elements from the tree.
The most frequently used built-in structural directives include
NgFor,NgSwitch, andNgIf
For a more reusable and maintainable approach, we can design a custom structural directive named hasRole. This allows us to keep our templates clean and expressive, like so:
<div *hasRoleIsAdmin="true">visible only for super admin</div>
<div *hasRole="'MANAGER'">visible if manager</div>
<div *hasRole="['MANAGER', 'READER']">visible if manager and/or reader</div>
<div *hasRole="['MANAGER', 'WRITER']">visible if manager and/or writer</div>
<div *hasRole="'CLIENT'">visible if client</div>
<div>visible for everyone</div>
👉🏼 This approach brings several benefits: cleaner templates, higher reusability, easier maintenance, and improved readability. 👈🏼
The upcoming sections explore multiple implementations of this same directive.
First Implementation Approach
@Directive({
selector: '[hasRole], [hasRoleIsAdmin]',
standalone: true,
providers: [provideDestroyService()],
})
export class HasRoleDirective implements OnInit {
private destroy$ = injectDestroyService();
private templateRef = inject(TemplateRef<unknown>);
private viewContainer = inject(ViewContainerRef);
private store = inject(UserStore);
@Input('hasRole') role: Role | Role[] | undefined = undefined;
@Input('hasRoleIsAdmin') isAdmin = false;
ngOnInit(): void {
if (this.isAdmin) {
this.store.isAdmin$
.pipe(takeUntil(this.destroy$))
.subscribe((isAdmin) =>
isAdmin ? this.addTemplate() : this.clearTemplate()
);
} else if (this.role) {
this.store
.hasAnyRole(this.role)
.pipe(takeUntil(this.destroy$))
.subscribe((hasPermission) =>
hasPermission ? this.addTemplate() : this.clearTemplate()
);
} else {
this.addTemplate();
}
}
private addTemplate() {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef);
}
private clearTemplate() {
this.viewContainer.clear();
}
}
- To work with the DOM, we inject both
TemplateRefandViewContainerRef. The former gives us a handle to the embedded template, represented by<ng-template>, while the latter provides access to the view where the directive is applied.
When the compiler encounters
<div *ngIf="...">CONTENT</div>, it desugars it into<ng-template [ngIf]="..."><div>CONTENT</div><ng-template>
- Two inputs are defined,
hasRoleandhasRoleIsAdmin, to cover the various scenarios we need.
To combine inputs within our directive, each input should be prefixed with the directive selector. This enables syntax like
<div*hasRole="'...';isAdmin:true"></div>.
- All the decision-making takes place in the
ngOnInitlifecycle hook. Based on the condition's outcome, we either show or hide the embedded template. - For proper resource cleanup, we inject a
DestroyServiceand leverage thetakeUntiloperator. This ensures all subscriptions are terminated when the component is destroyed. The service is linked to the component by including it in the providers array viaprovideDestroyService().
Drawback: The ngOnInit hook contains duplicated logic. This could be streamlined to avoid redundancy.
Second Approach: Using BehaviorSubject
@Directive({
selector: '[hasRole], [hasRoleIsAdmin]',
standalone: true,
providers: [provideDestroyService()],
})
export class HasRoleDirective implements OnInit {
private destroy$ = injectDestroyService();
private templateRef = inject(TemplateRef<unknown>);
private viewContainer = inject(ViewContainerRef);
private store = inject(UserStore);
private show = new BehaviorSubject<Observable<boolean | undefined>>(
of(undefined)
);
@Input('hasRole') set role(role: Role | Role[] | undefined) {
if (role) {
this.show.next(this.store.hasAnyRole(role));
}
}
@Input('hasRoleIsAdmin') set isAdmin(isAdmin: boolean) {
if (isAdmin) {
this.show.next(this.store.isAdmin$);
}
}
ngOnInit(): void {
this.show
.pipe(
mergeMap((s) => s),
takeUntil(this.destroy$)
)
.subscribe((showTemplate) =>
showTemplate ? this.addTemplate() : this.clearTemplate()
);
}
private addTemplate() {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef);
}
private clearTemplate() {
this.viewContainer.clear();
}
}
- The overall logic stays consistent, but a
BehaviourSubjectis introduced to consolidate our inputs into a single stream. This lets us subscribe once and eliminates redundant code. - To update the
BehaviourSubjectwith incoming values, we use setters tied to our inputs.
Using setters is often clearer than implementing
ngOnChange. However,ngOnChangebecomes necessary when changes affect multiple inputs simultaneously.
Drawback: Manual subscription is still required. This can potentially introduce errors if not handled correctly.
Third Approach: Leveraging Ngrx/Component-store
@Directive({
selector: '[hasRole], [hasRoleIsAdmin]',
standalone: true,
providers: [ComponentStore],
})
export class HasRoleDirective {
private templateRef = inject(TemplateRef<unknown>);
private viewContainer = inject(ViewContainerRef);
private componentStore = inject(ComponentStore);
private store = inject(UserStore);
@Input('hasRole') set role(role: Role | Role[] | undefined) {
if (role) {
this.showTemplate(this.store.hasAnyRole(role));
}
}
@Input('hasRoleIsAdmin') set isAdmin(isAdmin: boolean) {
if (isAdmin) {
this.showTemplate(this.store.isAdmin$);
}
}
private readonly showTemplate = this.componentStore.effect<
boolean | undefined
>(
pipe(
tap((showTemplate) =>
showTemplate ? this.addTemplate() : this.clearTemplate()
)
)
);
private addTemplate() {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef);
}
private clearTemplate() {
this.viewContainer.clear();
}
}
By utilizing the Ngrx/component-store effect, we can cut down on complexity even more. One of the strengths of this design is its flexibility: the effect can consume either a plain value or an Observable, handling both the same way.
But the optimization doesn't have to stop there. With Angular v15 and later, we can take advantage of the hostDirective feature to reuse logic from other directives directly on our host element.
Fourth Approach: Implementing with hostDirectives
@Directive({
selector: '[hasRole], [hasRoleIsAdmin]',
standalone: true,
hostDirectives: [NgIf], // 👈🏼 the beauty of Angular v15 is located here
providers: [ComponentStore],
})
export class HasRoleDirective {
private store = inject(UserStore);
private componentStore = inject(ComponentStore);
private ngIf = inject(NgIf, { host: true });
@Input('hasRole') set role(role: Role | Role[] | undefined) {
if (role) {
this.showTemplate(this.store.hasAnyRole(role));
}
}
@Input('hasRoleIsAdmin') set isAdmin(isAdmin: boolean) {
if (isAdmin) {
this.showTemplate(this.store.isAdmin$);
}
}
private readonly showTemplate = this.componentStore.effect<boolean | undefined>(
pipe(
tap((showTemplate) => this.ngIf.ngIf = showTemplate) // 🥰
));
}
- This approach allows us to attach additional directives to our host element. In the example given, we combined our custom directive with
ngIfby listingngIfwithin thehostDirectivesconfiguration. By injectingngIfinto our directive, we gain direct access to its internal properties.
It's important to include the
hostmeta-property on the inject function to ensure we're grabbing thengIfinstance declared on our host and not one from an ancestor component.
- Within our CS effect
showTemplate, we assign a value to thengIfproperty of the directive. From there, the built-inNgIfdirective takes over all DOM manipulation. This method proves to be convenient, tidy, and highly DRY, which reduces the potential for errors.
Finally, it's worth mentioning the valuable RxAngular library. (If this is new to you, it's definitely worth checking out)
Fifth Approach: RxAngular
@Directive({
selector: '[hasRole], [hasRoleIsAdmin]',
standalone: true,
hostDirectives: [NgIf],
providers: [RxEffects],
})
export class HasRoleDirective {
private store = inject(UserStore);
private rxEffect = inject(RxEffects);
private ngIf = inject(NgIf, { host: true });
private show = new Subject<Observable<boolean | undefined>>();
private show$ = this.show.asObservable().pipe(mergeMap((b) => b));
@Input('hasRole') set role(role: Role | Role[] | undefined) {
if (role) {
this.show.next(this.store.hasAnyRole(role));
}
}
@Input('hasRoleIsAdmin') set isAdmin(isAdmin: boolean) {
if (isAdmin) {
this.show.next(this.store.isAdmin$);
}
}
constructor() {
this.rxEffect.register(this.show$, this.showTemplate);
}
private showTemplate = (showTemplate: boolean | undefined) =>
(this.ngIf.ngIf = showTemplate);
}
The final implementation is available as a Pull Request here. (To run it locally, clone the project, switch to the solution branch with git checkout solution-permissions, and execute nx serve permissions)
I hope this sixth challenge has been both enjoyable and informative.
For the companion piece on route protection with guards, follow this link.
Create a route Guard to manage permissions
thomas for Playful Programming Angular ・ Jan 24 '23
If this article was helpful, your appreciation through likes ❤️❤️ would help broaden its reach. Sharing it with colleagues who might benefit would also be wonderful. Your support means a lot.
👉 Explore more challenges at Angular challenges. Give them a try, and I'll be glad to provide feedback!
Connect with me on Twitter or Github to stay updated on future challenges. Feel free to reach out with any questions.

