# Angular - custom *ngIf directive

In this article, I'll show how to create a custom structural directive that works similarly to Angular's `ngIf`. The goal is to create a directive that checks if a current user has required permissions by passing them to the directive. The directive should also allow passing `else` template reference whenever the directive condition fails.

# Setup

If unsure how to set up Angular with Jest please refer to the article: https://barcioch.pro/angular-with-jest-setup

# Permissions

Define available permissions in the `Permission` enum.

```typescript
// permission.enum.ts

export enum Permission {
  login = 'login',
  dashboard = 'dashboard',
  orders = 'orders',
  users = 'users',
  admin = 'admin',
}
```

Create a simple service for permission validation. The service takes as a first argument in the constructor the current users' permissions.

```typescript
// permission.service.ts

import { Injectable } from '@angular/core';
import { Permission } from './permission.enum';

@Injectable()
export class PermissionService {
  constructor(private readonly permissions: Permission[]) {
  }

  hasPermission(permission: Permission): boolean {
    return this.permissions.includes(permission);
  }
}
```

# Directive

Start with creating an empty directive with required dependencies.

```typescript
// has-permission.directive.ts

import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
import { PermissionService } from './permission.service';

@Directive({
  selector: '[hasPermission]',
})
export class HasPermissionDirective {
  constructor(
    private readonly viewContainer: ViewContainerRef,
    private readonly templateRef: TemplateRef<unknown>,
    private readonly permissionService: PermissionService
  ) {
  }
}
```

There are 3 dependencies required for this directive to work:

* `ViewContainerRef` - a reference to the parent view
    
* `TemplateRef<unknown>` - a reference to the element's view that the directive is connected to
    
* `PermissionService` - service for checking users' permissions
    

The user needs to pass permission directly to the directive in the following manner:

```typescript
// component
...
readonly permissions = Permission;
...
```

```xml
<!-- view -->

<div *hasPermission="permissions.login"></div>
```

To handle directive inputs use `@Input()` decorator. The input methods should be prefixed with directive selector (`hasPermission` in this case). The default input should be named exactly as the directive selector.

```typescript
private hasCurrentPermission = false;

@Input() set hasPermission(permission: Permission) {
  this.hasCurrentPermission = this.permissionService.hasPermission(permission);
  this.displayTemplate();
}

private displayTemplate(): void {
  this.viewContainer.clear();

  if (this.hasCurrentPermission) {
    this.viewContainer.createEmbeddedView(this.templateRef);
  }
}
```

I created a setter `hasPermission`. It uses the `PermissionService` to check the permission and stores the result in the private property `hasCurrentPermission`. Next, the `displayTemplate` method is called. First, it clears the current view (removes the content) `this.viewContainer.clear()`. Then, it checks the `hasCurrentPermission` property and if it's `true` then it renders the element that the directive is attached to `this.viewContainer.createEmbeddedView(this.templateRef)`.

Next, let's support the `else` keyword. The `else` should point to the `ng-template` reference.

```xml
<div *hasPermission="permissions.login; else noPermission"></div>

<ng-template #noPermission>
  <span>Access denied</span>
</ng-template>
```

To handle the `else` functionality we need to add the new input called `hasPermissionElse`. It's argument's type is the `TemplateRef` because we will be passing the template reference. Note, that the name consists of two parts:

* prefix: directive selector `hasPermission`
    
* suffix: the name used in the view `else`
    

```typescript
private elseTemplateRef: TemplateRef<unknown>;

@Input() set hasPermissionElse(templateRef: TemplateRef<unknown>) {
  this.elseTemplateRef = templateRef;
  this.displayTemplate();
}

private displayTemplate(): void {
  this.viewContainer.clear();

  if (this.hasCurrentPermission) {
    this.viewContainer.createEmbeddedView(this.templateRef);
    return;
  }
  
  if (this.elseTemplateRef) {
    this.viewContainer.createEmbeddedView(this.elseTemplateRef);
  }
}
```

The `hasPermissionElse` setter takes and stores the `else` template reference and then it calls `displayTemplate()` method. The `displayTemplate` method has the following modifications:

* added `return` to `this.hasCurrentPermission` condition check
    
* added `this.elseTemplateRef` condition check. If the user has no permission and the `else` template reference was passed we render this template
    

The whole directive looks as follows:

```typescript
// has-permission.directive.ts

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { PermissionService } from './permission.service';
import { Permission } from './permission.enum';

@Directive({
  selector: '[hasPermission]',
})
export class HasPermissionDirective {
  private elseTemplateRef: TemplateRef<unknown>;
  private hasCurrentPermission = false;

  constructor(
    private readonly viewContainer: ViewContainerRef,
    private readonly templateRef: TemplateRef<unknown>,
    private readonly permissionService: PermissionService
  ) {
  }

  @Input() set hasPermissionElse(templateRef: TemplateRef<unknown>) {
    this.elseTemplateRef = templateRef;
    this.displayTemplate();
  }

  @Input() set hasPermission(permission: Permission) {
    this.hasCurrentPermission = this.permissionService.hasPermission(permission);
    this.displayTemplate();
  }

  private displayTemplate(): void {
    this.viewContainer.clear();

    if (this.hasCurrentPermission) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      return;
    }

    if (this.elseTemplateRef) {
      this.viewContainer.createEmbeddedView(this.elseTemplateRef);
    }
  }
}
```

# Testing

A simple test that checks all possible combinations of directive inputs.

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { Permission } from './permission.enum';
import { CommonModule } from '@angular/common';
import { HasPermissionModule } from './has-permission.module';
import { PermissionService } from './permission.service';
import { By } from '@angular/platform-browser';

describe('HasPermissionDirective', () => {
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        CommonModule,
        HasPermissionModule
      ],
      declarations: [TestComponent],
      providers: [
        {
          provide: PermissionService,
          useValue: new PermissionService([Permission.dashboard, Permission.users])
        }
      ]
    });

    fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges()
  });

  describe('when the test component is initialized', () => {
    describe('and user has set "dashboard" and "users" permissions', () => {
      it('should not display "orders" element', () => {
        const element = fixture.debugElement.query(By.css('#orders'));
        expect(element).toBeFalsy();
      });

      it('should display "dashboard" element', () => {
        const element = fixture.debugElement.query(By.css('#dashboard'));
        expect(element).toBeTruthy();
      });

      it('should not display "admin" element', () => {
        const element = fixture.debugElement.query(By.css('#admin'));
        expect(element).toBeFalsy();
      });

      it('should display "no-access-admin" element', () => {
        const element = fixture.debugElement.query(By.css('#no-access-admin'));
        expect(element).toBeTruthy();
      });

      it('should display "no-access-dashboard" element', () => {
        const element = fixture.debugElement.query(By.css('#admin'));
        expect(element).toBeFalsy();
      });


      it('should display "users" element', () => {
        const element = fixture.debugElement.query(By.css('#users'));
        expect(element).toBeTruthy();
      });

      it('should display "no-access-users" element', () => {
        const element = fixture.debugElement.query(By.css('#no-access-users'));
        expect(element).toBeFalsy();
      });

    });
  });
});


@Component({
  selector: 'app-test',
  template: `
    <div id="orders" *hasPermission="permissions.orders"></div>

    <div id="dashboard" *hasPermission="permissions.dashboard"></div>

    <div id="admin" *hasPermission="permissions.admin; else noAccessAdmin"></div>

    <ng-template #noAccessAdmin>
      <div id="no-access-admin"></div>
    </ng-template>

    <div id="users" *hasPermission="permissions.users; else noAccessUsers"></div>

    <ng-template #noAccessUsers>
      <div id="no-access-users"></div>
    </ng-template>
  `,
})
export class TestComponent {
  readonly permissions = Permission;
}
```

Run jest

```bash
npx jest
```

and you should see all tests passed

```text
PASS  src/app/permissions/has-permission.spec.ts
  HasPermissionDirective
    when the test component is initialized
      and user has set "dashboard" and "users" permissions
        ✓ should not display "orders" element (52 ms)
        ✓ should display "dashboard" element (8 ms)
        ✓ should not display "admin" element (6 ms)
        ✓ should display "no-access-admin" element (6 ms)
        ✓ should display "no-access-dashboard" element (5 ms)
        ✓ should display "users" element (5 ms)
        ✓ should display "no-access-users" element (4 ms)

Test Suites: 1 passed, 1 total
Tests:       7 passed, 7 total
Snapshots:   0 total
Time:        0.836 s, estimated 2 s
```

# Source code

https://gitlab.com/barcioch-blog-examples/008-angular-custom-ngif
