Structural Directives
Every Angular developer is familiar with ngIf
and ngFor
- they are commonly used in templates. They are built-in structural directives: ngIf
adds or removes a part of the template from the DOM, while ngFor
does the same but using an iterable input such as an array or set.
You can create your own structural directives, this feature will remain even when ngIf, ngFor and ngSwitch are replaced with the built-in control flow and deprecated [RFC]. The mechanism of structural directives itself will not be deprecated, so there is no need to worry. In this article, I will demonstrate how to quickly create a simple case of structural directives - conditional ones, similar to ngIf.
ifHasPermission
Let's proceed with building the ifHasPermission
directive. It's a popular example in articles about structural directives for a reason - it's quite useful.
First of all, here is the base class that we will extend. I'm not a fan of inheritance in programming, and you can modify this code to make it work as a delegate instead. However, in this case, inheritance simplifies things.
import { inject, TemplateRef, ViewContainerRef } from "@angular/core";
export abstract class StructuralConditionalDirective {
protected readonly templateRef = inject(TemplateRef<any>);
protected readonly viewContainer = inject(ViewContainerRef);
protected elseTemplateRef: TemplateRef<any> | undefined = undefined;
protected hasView: boolean = false;
protected hasElseView: boolean = false;
protected condition: boolean = false;
public setCondition(condition: boolean) {
this.condition = condition;
this.updateContainer();
}
public setElseTemplate(template: TemplateRef<any>) {
this.elseTemplateRef = template;
this.updateContainer();
}
private clearContainer() {
this.viewContainer.clear();
this.hasView = false;
this.hasElseView = false;
}
private updateContainer() {
if (this.condition) {
if (!this.hasView) {
this.clearContainer();
if (this.templateRef) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
}
}
} else {
if (!this.hasElseView) {
this.clearContainer();
if (this.elseTemplateRef) {
this.viewContainer.createEmbeddedView(this.elseTemplateRef);
this.hasElseView = true;
}
}
}
}
}
Here, we insert the given template into the ViewContainer, if the condition is true. Otherwise, we check if we have a template for the else case and insert that template into the ViewContainer. Additional checks are in place to avoid re-rendering the ViewContainer if the condition has not changed since the last call.
Now, ifHasPermission
:
import { Directive, inject, Input, TemplateRef } from '@angular/core';
import { Permission, PermissionsService } from '../permissions.service';
import { StructuralConditionalDirective } from "../structural-conditional.directive";
@Directive({
selector: '[ifHasPermission]',
standalone: true,
})
export class IfHasPermissionDirective extends StructuralConditionalDirective {
private readonly permissionsService = inject(PermissionsService);
@Input('ifHasPermission') set permission(permission: Permission) {
this.setCondition(this.permissionsService.hasPermission(permission));
}
@Input('ifHasPermissionElse') set ifHasPermissionElse(template: TemplateRef<any>) {
this.setElseTemplate(template);
}
}
Quite small, just 2 inputs, just a few lines - all the dirty work is delegated to StructuralConditionalDirective class.
Here you have one external dependency: PermissionsService
. It's up to you how you implement it and how it will check the permissions - it can be asynchronous (with Promises or Observables), or synchronous, like in the example.
Asynchronous code could look like this:
this.permissionsService.hasPermission(permission).then((result) => {
this.setCondition(result);
});
To create a conditional directive, you only need to implement two inputs: the first takes the condition, and the second, which is optional, takes the template to render when the condition is false.
Names of the inputs should follow the convention:
- Input, named the same as the directive selector, should handle the input and modify the container;
- Input, named as a selector with the suffix
Else
, will receive the template reference, mentioned after the else keyword in the template.
Usage Example
Let's write a test for our directive, and in that test, we'll have a usage example.
Test component:
@Component({
selector: 'test-if-has-permission',
standalone: true,
imports: [IfHasPermissionDirective],
template: `
<ng-template #falseTemplate>
<p>Does not have permission</p>
</ng-template>
<div *ifHasPermission="'READ'; else falseTemplate">
<p>Content that requires read permission</p>
</div>
<div *ifHasPermission="'CREATE'; else falseTemplate">
<p>Content that requires create permission</p>
</div>
<div *ifHasPermission="'DELETE'">
<p>Content that requires delete permission</p>
</div>
`,
})
class TestComponent {
}
In this template, we have *
in front of our directive selector - similar to ngIf
, and that's why Angular applies the structural directives logic for our directive.
The complete test file you can find here (GitHub Gist).
Your structural directives can be much more sophisticated. Here, I have demonstrated how you can reuse the StructuralConditionalDirective
class to create conditional structural directives with just a few lines of code.
However, structural directives are much more powerful. You can explore their full potential by reading the comprehensive guide on Angular.io. There, you can learn how to use shorthand syntax (also known as "microsyntax") and improve type checking.
Another purpose of this article is to alleviate any concerns caused by the new built-in control flow syntax. It is a significant improvement for Angular, and we should embrace it without hesitation. You can find more information about it in this article.
💙 If you enjoy my articles, consider following me on Twitter, and/or subscribing to receive my new articles by email.
🎩️ If you or your company is looking for an Angular consultant, you can purchase my consultations on Upwork.
Top comments (0)