Why Directives are the Go-To Choice for Select Component Options Reuse in Angular
Our application has various select components that are frequently used throughout the application, such as “select user,” “select wallet,” and “select blockchain” components.
One approach to managing these components would be to individually query the options
and pass them to the select
component each time they are needed; however, this approach isn’t DRY.
An alternative solution is to create a wrapper component for each select
component that encapsulates its logic. For example, we could create a UsersSelect
component that is reusable throughout the application:
@Component({
selector: 'app-users-select',
standalone: true,
imports: [SelectComponent, CommonModule],
template: `
<app-select
placeholder="Select user"
[options]="users$ | async"
[allowClearSelected]="allowClearSelected"
></app-select>
`
})
export class UsersSelectComponent implements ControlValueAccessor {
@Input() allowClearSelected: boolean;
users$ = inject(UsersService).getUsers().pipe(
map((users) => {
return users.map((user) => {
return {
id: user.id,
label: user.label,
};
});
})
)
// ... ControlValueAccessor...
}
Although that approach works, it’s not optimal. First, each component would require a new control value accessor. Moreover, to allow consumers to set inputs
and listen for outputs
, the select
component must proxy its API to the wrapped component.
To address these issues, we can utilize directives. Rather than creating a separate component for each select
options, we can create a reusable directive that can be used to add options to the select
component:
import { Directive, inject } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
@UntilDestroy()
@Directive({
selector: '[usersSelectOptions]',
standalone: true,
})
export class UsersSelectOptionsDirective {
private select = inject(SelectComponent);
private users$ = inject(UserService).getUsers();
ngOnInit() {
this.select.placeholder = 'Select user';
this.users$.pipe(untilDestroyed(this)).subscribe((users) => {
this.select.options = users.map((user) => {
return {
id: user.id,
label: user.name,
};
});
});
}
}
We obtain a reference to the SelectComponent
using DI. In the ngOnInit
lifecycle hook, the select.placeholder
property is set to “Select user”.
We fetch the users and sets the select options
. Now, we can use it in our select
component:
<app-select
usersSelectOptions <=====
formControlName="userId"
[allowClearSelected]="false"
></app-select>
Note that if you’re using Signals you can use the model input:
export class UsersSelectComponent implements ControlValueAccessor {
allowClearSelected = model<boolean>();
// ... ControlValueAccessor...
}
export class UsersSelectOptionsDirective {
private select = inject(SelectComponent);
ngOnInit() {
this.select.placeholder.set('Select user');
// ....
}
}
Follow me on Medium or Twitter to read more about Angular and JS!