Structural Directives in Angular
Tags: angular
Published: 2023-08-01 12:38:55

## 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](https://github.com/angular/angular/discussions/50719)]. 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.

```ts
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`:

```ts
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:

```ts
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:
```ts
@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](https://gist.github.com/e-oz/f8b15341cfa6a863d1f9f8a60cd77677) (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](https://angular.io/guide/structural-directives). 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](https://dev.to/oz/why-i-like-the-built-in-control-flow-idea-2o69).

---
💙 _If you enjoy my articles, consider [following me on Twitter](https://twitter.com/eugeniyoz), and/or [subscribing](https://medium.com/@eugeniyoz) 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](https://www.upwork.com/services/consultation/development-it-evgeniy-1696285343741362176?ref=project_share)._