During your Angular journey, you could be invested into a development that implies hiding elements to some kind of users depending access right or roles. You could even need to hide complete features from you Angular client waiting for the backend to be implemented or waiting for the feature to be officially released to the public.
I can already hear it from you, when it comes to display or hide components or other elements from a template, the first thing that comes in mind is to create a directive. It's reusable and we love reusable things.
Let's continue our story with our second example. We need to implement a directive that hide the element based on a given feature. Let us suppose that we have a feature called: githubLogin. Basically, this feature active the authentication via GitHub.
Creating the directive (the naive way)
And Abracadabra, we have our nice FeatureToggleDirective
.
@Directive({ selector: '[feature]' })
export class FeatureToggleDirective implements OnInit {
constructor(
private viewContainer: ViewContainerRef,
private featureService: FeatureService) {
}
@Input()
feature: string;
ngOnInit() {
if (this.featureService.isDisabled(this.feature)) {
this.hide();
}
}
private hide(): void {
this.viewContainer.clear();
}
}
It's used like this :
<button [feature]="'githubLogin'">Login via GitHub</button>
Meaning that the button Login via GitHub is displayed if the feature is enabled.
So here we are, we created our directive, and you noticed that I used ViewContainerRef
to hide the content. It's a choice, I could also inject ElementRef
and use
this.elementRef.nativeElement.style.display = 'none';
to hide the content. Both solutions are valid, and we will go further in the comparison since it's not the goal of this article.
Can we do better?
Of course, we can do better! What if we want to change the behavior of this directive to be more dynamic and flexible.
For example, being able to use it when the feature name is given asynchronously:
<button [feature]="featureName$ | async">Login via GitHub</button>
We would need to use a setter or implement OnChange.
We could also decide to display a message explaining why this feature is disabled. By providing a fallback template to the directive:
<button [feature]="'githubLogin'" [featureFallbackTemplate]="fallback">Login via GitHub</button>
<ng-template #fallback>Sorry, the GitHub feature is not yet available</ng-template>
There is so much work to do to make our directive completely robust. At some point, it begins to be as dynamic and robust as another directive that we all know...
Extending NgIf
The subtitle says it all, why not reusing the code of NgIf
. It has been proved that it works. It's the most used directive in the Angular world ("ngIf" gives 4M file results in Github!). So what do we do ? Do we copy the NgIf solution? What about extending NgIf? Yes, sounds good!
Let's take a look at the NgIf code. We notice 4 methods: 3 setters and one private method where all the magic occurs.
Since our directive has its own selector name feature
, we need to rename the setters with feature
, featureThen
, and featureElse
. Each of those methods will act as a proxy and call the corresponding setter in NgIf
class.
We also need to add the FeatureService
logic in the feature setter. Our condition returns a boolean
, so we set the type of the NgIf
to boolean
.
@Directive({ selector: '[feature]' })
export class FeatureToggleDirective extends NgIf<boolean> {
@Input()
set feature(feature: string) {
this.ngIf = this.featureService.isEnabled(feature);
}
@Input()
set featureThen(templateRef: TemplateRef<NgIfContext<boolean>>|null) {
this.ngIfThen = templateRef;
}
@Input()
set featureElse(templateRef: TemplateRef<NgIfContext<boolean>>|null) {
this.ngIfElse = templateRef
}
constructor(private featureService: FeatureService ,_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext<boolean>>) {
super(_viewContainer, templateRef);
}
}
And that's it, you can now use all the power of NgIf
with your feature toggle directive! Amazing, isn't it?
Some usage examples
Some examples of how we can use our directive:
Simple form with shorthand syntax:
<button *feature="'githubLogin'">GiHub login</button>
Simple form with expanded syntax:
<ng-template [feature]="'githubLogin'">
<button>GiHub login</button>
</ng-template>
Shorthand form with an else
block:
<button *feature="'githubLogin'; else fallback">GiHub login</button>
<ng-template #fallback>
<i>Sorry, the feature is not yet available</i>
</ng-template>
Shorthand form with then
and else
blocks:
<ng-container *feature="'githubLogin'; then github; else fallback">GiHub login</ng-container>
<ng-template #github>
<button>GiHub login</button>
</ng-template>
<ng-template #fallback>
<i>Sorry, the feature is not yet available</i>
</ng-template>
Working example
Take a look at the working online example here: StackBlitz. Play with it and fork it!
Conclusion
We created a highly reusable directive based on a simple feature toggle example. There are plenty of other features that could reuse NgIf
. I hope this article will give you more ideas and uses cases to build even more directives of this kind.
Happy coding!
Top comments (1)
I wouldn't have thought we can extend
NgIf
🤯