Contents

Angular Enterprise Dashboard - Phase 2.5: Role-Based Access Control with Custom Directives

We’ve secured our routes with Functional Guards, but what about the UI itself? An Admin should see the “Delete” button, but a standard User should not.

Authorization Beyond the Route

In the final post of our Phase 2 series, we’ll implement Role-Based Access Control (RBAC) directly in our templates using a custom Structural Directive.


We want to be able to hide or show parts of our UI based on the user’s role with a syntax as simple as this:

<button *appHasRole="'ADMIN'" (click)="deleteEverything()">Danger Zone</button>

A structural directive manages how a template is rendered. In our case, we want to render the template only if the user has the required role.

Because our AuthService uses Signals, we can use an effect inside our directive. This means the UI will automatically show or hide elements if the user’s role changes during their session (e.g., if their session expires or they log in as a different user).

@Directive({ selector: '[appHasRole]', standalone: true })
export class HasRoleDirective {
  private readonly templateRef = inject(TemplateRef<unknown>);
  private readonly viewContainer = inject(ViewContainerRef);
  private readonly authService = inject(AuthService);

  readonly roles = input.required<UserRole | UserRole[]>({
    alias: 'appHasRole',
  });

  constructor() {
    effect(() => {
      // 1. Reactive check
      const hasPermission = this.authService.hasRole(this.roles());

      // 2. DOM Management
      this.viewContainer.clear();
      if (hasPermission) {
        this.viewContainer.createEmbeddedView(this.templateRef);
      }
    });
  }
}

When you write:

<li *appHasRole="[UserRole.ADMIN, UserRole.MANAGER]">...</li>

Angular desugars the * prefix into an explicit <ng-template>:

<ng-template [appHasRole]="[UserRole.ADMIN, UserRole.MANAGER]">
  <li>
    <a routerLink="/projects" routerLinkActive="active">
      <span class="nav-icon">📁</span> Projects
    </a>
  </li>
</ng-template>

This is what allows Angular to provide the two key tokens to our directive via inject():

Injected TokenWhat It Is
TemplateRefA reference to the implicit <ng-template>. It holds the blueprint of the <li> and everything inside it — but it’s not rendered yet. Think of it as the what to render.
ViewContainerRefA reference to the insertion point in the DOM where the template was declared. It’s the “slot” where we can stamp out (or remove) the template. Think of it as the where to render.

Then the two lines in our effect() become crystal clear:

// Stamp the blueprint into the DOM slot → shows the <li>
this.viewContainer.createEmbeddedView(this.templateRef);

// Remove everything from the slot → hides the <li>
this.viewContainer.clear();

graph TD
    Trigger[State Change / Directive Init] --> Check[AuthService.hasRole]
    Check --> Result{Has Permission?}
    Result -- Yes --> Render[viewContainer.createEmbeddedView]
    Result -- No --> Clear[viewContainer.clear]

    Render --> UI[Template Shown]
    Clear --> UI2[Template Hidden]

Many beginners try to hide elements using CSS ([style.display]="...").

Why the Directive is better: Structural directives actually remove the element from the DOM.

  1. Security: The hidden HTML doesn’t exist in the browser’s DOM tree at all.
  2. Performance: Angular doesn’t need to run change detection on child components that aren’t rendered.

We have traveled a long way in Phase 2:

  1. Reactive State: Mastered Signals in our AuthService.
  2. Navigation Security: Implemented Functional Guards.
  3. App Architecture: Built a scalable App Shell.
  4. Visual Excellence: Applied Glassmorphism and Design Tokens.
  5. Granular Authorization: Created our HasRoleDirective.

The foundation is now rock-solid. We are ready for Phase 3, where we’ll bring our dashboard to life with real-time data visualization and the new Angular Resource API.


Thank you for following this series! You can find all the code discussed in these posts in the Angular Enterprise Dashboard repository.

Related Content