Original cover photo by Ivan Bandura on Unsplash.

Welcome to the sixth entry in our series on Angular directives and their seemingly magical capabilities. Today, we shift focus to a less common yet highly frequent real-world scenario: embedding business logic directly into directives.

Introduction

Before diving in, let's quickly recap what we've accomplished so far and understand how today's use cases stand apart.

Previously, we built directives that performed specific, reusable, and practical tasks.

For instance, we created a directive that

  • assessed password strength
  • triggered custom events when the host element entered the viewport
  • moved a template view into a different component
  • rendered a loading indicator
  • applied default input values to existing components

These examples share a common thread: each directive is broadly reusable and can be dropped into any project without modification, as none of them contain any business logic. However, in many real-world applications, the logic we want to reuse and share is precisely the business logic. So, what options do we have?

Using directives to handle permissions

Consider an app that uses permissions to control feature availability. For example, users might edit their profile only if granted the appropriate permission. We have a PermissionService exposing a hasPermission method that accepts a permission name and returns a boolean. In a component, we might use it like this:

@Component({
  selector: 'app-profile',
  template: `
    <div *ngIf="hasPermission('edit-profile')">
      <button (click)="editProfile()">Edit profile</button>
    </div>
  `
})
export class ProfileComponent {
  constructor(private permissionService: PermissionService) {}

  hasPermission(permissionName: string) {
    return this.permissionService.hasPermission(permissionName);
  }

  editProfile() {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

However, every time we need to verify a permission, we'd have to inject the service, potentially write a wrapper, call it from the template, or use another somewhat tedious approach. Furthermore, if the permission mechanism changes (like a new API for the service), we might face a significant refactor. And if we use an Observable-based state management solution like NgRx, the code gets even more complicated.

How can we simplify this?

Wouldn't it be convenient to write something like this instead:

@Component({
  selector: 'app-profile',
  template: `
    <div *hasPermission="'edit-profile'">
      <button (click)="editProfile()">Edit profile</button>
    </div>
  `
})
export class ProfileComponent {
  editProfile() {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

This way, we simply provide the permission name as an input to the directive, and it manages everything else. Let's see how to build this.

  • First, create a directive that injects the PermissionService and has an input property for the permission name.
  • Next, add ngIf as a HostDirective to handle template visibility.
  • Finally, inject the NgIf directive reference and pass the resulting boolean to it.

Let's implement it:

@Directive({
  selector: '[hasPermission]',
  standalone: true,
  hostDirectives: [NgIf],
})
export class HasPermissionDirective {
  private readonly permissionService = inject(PermissionService);
  private readonly ngIfRef = inject(NgIf);

  @Input() 
  set hasPermission(permissionName: string) {
    // we can use any other approach here
    this.ngIfRef.ngIf = this.permissionService.hasPermission(
      permissionName,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Our directive now works almost flawlessly. What's missing? We need to support the scenario where the user lacks permission. We can achieve this by adding an else template to our directive:

@Directive({
  selector: '[hasPermission]',
  standalone: true,
  hostDirectives: [NgIf],
})
export class HasPermissionDirective {
  private readonly permissionService = inject(PermissionService);
  private readonly ngIfRef = inject(NgIf);

  @Input() 
  set hasPermission(permissionName: string) {
    this.ngIfRef.ngIf = this.permissionService.hasPermission(
      permissionName,
    );
  }

  // the improtant part
  @Input() 
  set hasPermissionElse(template: TemplateRef<any>) {
    this.ngIfRef.ngIfElse = template;
  }
} 
Enter fullscreen mode Exit fullscreen mode

At this point, we're essentially executing the business logic and forwarding the outcome to the NgIf directive. Here's a live example:

Using directives to handle shared data in components

Suppose we're using a UI library that provides dropdown components:

@Component({
  selector: 'app-profile',
  template: `
    <div>
      <third-party-dropdown
        [items]="dropdownItems"></third-party-dropdown>
    </div>
  `
})
export class ProfileComponent {
  dropdownItems = [
    {
        label: 'Item 1',
        value: 1
    },
    {
        label: 'Item 2',
        value: 2
    }
  ];

}
Enter fullscreen mode Exit fullscreen mode

Often, we need to supply the same set of options to several dropdowns. For instance, the list of permissions we discussed earlier might be needed in many places:

@Component({
  selector: 'app-profile',
  template: `
    <div>
      <third-party-dropdown
        [items]="permissions"></third-party-dropdown>
    </div>
    <div>
  `
})
export class SomeComponent {
    permissionService = inject(PermissionService);
    permissions = this.permissionService.getPermissions();
}
Enter fullscreen mode Exit fullscreen mode

We'd likely end up duplicating this code across the app. How do we make this more reusable? One approach is to create a wrapper component that accepts the item list as an input:

<app-wrapper-around-third-party-dropdown
  [items]="permissions"></app-wrapper-around-third-party-dropdown>
Enter fullscreen mode Exit fullscreen mode

However, that approach introduces its own set of issues:

  • CSS encapsulation challenges
  • Needing to forward all of the third-party dropdown's inputs and outputs
  • Increased template complexity

What's the alternative? We can create a directive that injects the list of items into the third-party dropdown:

@Directive({
  selector: 'third-party-dropdown[permissionList]',
  standalone: true,
})
export class PermissionsDropdownDirective implements OnInit {
  thirdPartyDropdown = inject(ThirdPartyDropdown);
  permissionService = inject(PermissionService);

  ngOnInit() {
    this.thirdPartyDropdown.items = 
      this.permissionService.getPermissions();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we can use it like this:

<third-party-dropdown permissionList></third-party-dropdown>
Enter fullscreen mode Exit fullscreen mode

Done! Our template stays clean, the third-party-dropdown remains unchanged, and we've added reusable functionality to it.

Here's the example with a live preview:

Conclusion

We've covered a vast range of what directives can do for us. Even though there's more to explore, we've covered enough for projects of varying sizes, so this series is winding down. In the upcoming final article, we'll dive into directive selectors in detail—what options we have, how to combine them, and common pitfalls or restrictions. See you there!