Create a custom Structural Directive to manage permissions
Tags: angular
Published: 2023-01-02 09:25:00

## Welcome to Angular challenges #6.
> _The aim of this series of Angular challenges is to increase your skills by practicing on real life exemples. Moreover you can submit your work though a PR which I or other can review; as you will do on real work project or if you want to contribute to Open Source Software._

The sixth challenge is about managing permissions within an application. Many applications have to handle permissions for different types of users, such as admins, managers…  In this challenge, we will need to display or hide certain information and restrict certain route navigation based on a user's permissions. This challenge can be divided into two parts:
- The first part will introduce us to various ways to write a structural directive that modify our HTL to show or hide DOM elements. 
- The second part will focus on protecting route navigation: guard. _(which will be cover in a following article)_

If you haven't done the challenge yet, I invite you to try it first by going to [Angular Challenges](https://github.com/tomalaforge/angular-challenges/blob/main/apps/permissions/README.md) and coming back afterward to compare your solution with mine. _(You can also submit a PR that I'll review)_

---

We begin the challenge with the following application. We have a list of buttons to log in with different users who have different permissions. We also have a list of sentences that we need to display or hide based  on the user we logged in with.

![startup screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4s1p4g6tpgfxnvm7200z.png)


`User` interface is defined as follow:

```typescript
export type Role = 'MANAGER' | 'WRITER' | 'READER' | 'CLIENT';

export interface User {
  name: string;
  isAdmin: boolean;
  roles: Role[];
}
```
The naive way to solve this challenge would be to use `ngIf` on each row:

```html
<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>
//...
```
As we can see, we have to create a complicated condition and this solution is not reusable. It can only get messier and less maintainable as the project grows.

## Structural Directives
Angular offers a solution called **structural directives** to modify the structure of the DOM based on specific conditions. Structural directives allow us to add or remove elements from the DOM. In the previous solution, we used the built-in structural directive `ngIf`, which allows us to add or remove elements based on a condition.

> _Other commonly used built-in structural directives are `NgFor` , `NgSwitch` , `NgIf`_

To make our solution more reusable and maintainable, we can create a custom structural directive called `hasRole`. This will allow us to simplify our template like this:

```html
<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>
```
👉🏼 Cleaner, Reusable, Maintainable, More readable. 👈🏼

---

In the following sections, we will see several implementations of the same directive.

### Solution #1:
 

```typescript
@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();
  }
}
```
- `TemplateRef` and `ViewContainerRef` need to be injected in order to access our DOM elements and manipulate them. `TemplateRef` allows us to access the embedded template: `<ng-template>` while `ViewContainerRef` accesses the view in which our directive has been called. 

> _When we write `<div *ngIf="...">TOTO</div>` , the compiler translates it to `<ng-template [ngIf]="..."><div>TOTO</div><ng-template> `_

- We define two inputs `hasRole` and `hasRoleIsAdmin` for all our use cases.

> _To concatenate inputs inside our directive, we need to prefix our input with our directive selector, this way we will be able to write : `<div*hasRole="'...';isAdmin:true"></div>`. _

- All of the logic is created inside our `ngOnInit` hook and we display or hide the embedded template depending on the result of our condition. 
- To avoid memory leaks, we inject a `DestroyService` and use the `takeUntil` operator to unsubscribe to all of our subscriptions when component is destroyed. To connect our service with the component, we include it in the component provider array with `provideDestroyService()`

**Issue:** In our `ngOnInit` hook, we have repeated the same logic twice. We can do improve this by avoiding the repetition.

### Solution #2: BehaviorSubject

```typescript
@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 logic remains the same, but to combine both subscriptions into one, we use a `BehaviourSubject` to merge our input into a single stream. Now, we only need to subscribe to one observable and we avoid repeat ing ourselves. 
- To feed our `BehaviourSubject` we use setters on our inputs to work on the incoming values. 

> _Setters are more readable than using `ngOnChange`. However you must use `ngOnChange` if your changes involve two or more inputs._

**Issue:** We still need to manually subscribe to our stream. Manual subscription can be error-prone.
 

### Solution #3: Ngrx/Component-store

```typescript
@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();
  }
}
```
Using the **Ngrx/component-store effect** allows us to further reduce the complexity. The nice design of the CS effect is that it can take either a strict value or an Observable as input and handle it in the same way.

But we can further simplify our directive. Since Angular v15, we can take advantage of the hostDirective and reuse code from other directives.

### Solution #4: hostDirectives

```typescript
@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) // 🥰
  ));
}
```
- We can now add directives to our host element. In this example, we tied the implementation of `ngIf` with our custom directive by declaring `ngIf` inside `hostDirectives` . We inject `ngIf` inside our directive to access the internal class properties of `ngIf`. 

> _Don't forget to add the `host` meta-property on the inject function to make sure you get the `ngIf` directive declared on your host element and not one located higher on your component tree._

- In our CS effect `showTemplate`, we set the `ngIf` class property of the `ngIf` directive and the built-in `NgIf` directive will handle all of the DOM logic for us. This is convenient, handy, and DRY which is less error-prone. 

Last but not least, don't forget about a really nice library called **RxAngular**. _(If you don't know it, you should take a look)_

### Solution #5: RxAngular

```typescript
@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);
}
```

---

You can find the final code in the form of a Pull Request [here](https://github.com/tomalaforge/angular-challenges/pull/33). _(If you want to get it up and running, you can clone the project, check out the solution branch git checkout solution-permissions and run nx serve permissions)_

---

I hope you enjoyed this sixth challenge and learned from it.

If you want to read the second part about protecting your routes with guards, follow [this link](https://dev.to/this-is-angular/create-a-route-guard-to-manage-permissions-4c09).

{% embed https://dev.to/this-is-angular/create-a-route-guard-to-manage-permissions-4c09 %}

If you found this article useful, please consider supporting my work by giving it some likes ❤️❤️ to help it reach a wider audience. Don't forget to share it with your teammates who might also find it useful. Your support would be greatly appreciated.

👉 Other challenges are waiting for you at [Angular challenges](https://github.com/tomalaforge/angular-challenges). Come and try them. I'll be happy to review you!

Follow me on [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge) to read more about upcoming Challenges! Don't hesitate to ping me if you have more questions.