DEV Community

Jérôme Navez
Jérôme Navez

Posted on • Updated on

Angular Access Right And Feature Toggle By Extending NgIf!

Jump to final solution

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

It's used like this :

<button [feature]="'githubLogin'">Login via GitHub</button>
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Simple form with expanded syntax:

<ng-template [feature]="'githubLogin'">
    <button>GiHub login</button>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
daguttt profile image
Daniel Gutierrez Muñoz

I wouldn't have thought we can extend NgIf 🤯