Original cover photo by Ivan Bandura on Unsplash.
It’s time for the 6th installment of this article series about Angular directives – and their almost magical superpowers.
Today we are going to explore a less popular, but very, very common and valid use case scenario – bringing business logic into the directives.
Introduction
First of all, let’s reflect on what we have already done, and understand how today’s cases are fundamentally different.
In previous articles, we created directives that did some specific, reusable, and very useful things.
For example, we built a directive that
- checked for password strength
- emitted custom events when the host element scrolled into view
- transported a template view into another component
- displayed a loader
- added some default inputs onto existing components
As you can see, there is a common theme for all these cases – all those directives are super-reusable in the sense that they can be dropped in any project and just work; non of them really contain any business logic. But in a lot of cases, with real-life projects, it is the business logic that we want to be reusable and easily shareable. So what scenarios are there?
Using directives to handle permissions
Imagine that our app has permissions to enable or disable certain features. For example, we might have a feature that allows users to edit their profile, but only if they have the right permissions. We have a few PermissionService
that has a hasPermission
method that takes a permission name and returns a boolean. We could use this service in our component like this:
@Component({
selector: 'app-profile',
template: `
`
})
export class ProfileComponent {
constructor(private permissionService: PermissionService) {}
hasPermission(permissionName: string) {
return this.permissionService.hasPermission(permissionName);
}
editProfile() {
// ...
}
}
But now every time we want to check for this or that permission, we need to inject the service, maybe write a wrapper method, call it from the template, or some other mildly cumbersome way. Also, if the way how we access those permissions changes (for example, the service now has a different API interface), we might face a pretty large refactoring. Also, if were have some sort of state management solution that works with Observables (like NgRx, for instance), our code will become even more complex.
So how do we deal with this?
Well, wouldn’t it be nice if we could just do this:
@Component({
selector: 'app-profile',
template: `
`
})
export class ProfileComponent {
editProfile() {
// ...
}
}
So now we can pass the permission name as an input to the directive, and the directive will handle the rest. Let’s see how we can do this.
- First, we need to create a directive that injects the
PermissionService
and has an input property that will accept the permission name - next, we will add
ngIf
as a HostDirective so it can handle showing/hiding the template - and finally, we will inject the reference to the
NgIf
directive, and pass the resulting boolean to it
Let’s do this:
@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,
);
}
}
Now, our directive works almost perfectly. What do we need to add? Of course, a possibility to handle the case when the user does not have the permission. We can do 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;
}
}
Now, we essentially just do the business logic and pass the result to the NgIf
directive. Here is a working example with a preview:
Using directives to handle shared data in components
Imagine we use some UI library that allows us to show dropdowns:
@Component({
selector: 'app-profile',
template: `
`
})
export class ProfileComponent {
dropdownItems = [
{
label: 'Item 1',
value: 1
},
{
label: 'Item 2',
value: 2
}
];
}
And in some cases, we need to pass the same list of options to multiple dropdowns. For example, the same list of permissions we encountered earlier in this article could be really useful in lots of places:
@Component({
selector: 'app-profile',
template: `
`
})
export class SomeComponent {
permissionService = inject(PermissionService);
permissions = this.permissionService.getPermissions();
}
And then we could really see this same code in multiple places throughout our app. So how can we make this more reusable? Well, we could create a component that will wrap the dropdown and accept the list of items as an input:
[items]="permissions">
But that would introduce a set of problems:
- CSS encapsulation
- Having to pass eventually all of the third-party dropdown’s inputs and outputs up and down
- More complexity in our templates
So what can we do instead? Well, we can create a directive that will inject 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();
}
}
And now we can use it like this:
permissionList>
And that’s it! Now our template is simple, the third-party-dropdown
is still as it used to be, and we got additional reusable functionality for it.
Here is the example with a preview:
Conclusion
We have explored a huge amount of possibilities that directives give us. While there are plenty more, this much is fairly enough for projects of different sizes, so this article series is nearing its end. In the next and last one, we will talk in detail about directive selectors, what we can use, how can we combine different selectors, and what are some pitfalls or where we cannot use a certain selector. Stay tuned!