One of the unique features of Angular State Library is that every store is a @Directive
.
Disclaimer: This project is an experiment, it is not production ready.
Unlike services, directives have access to inputs, outputs, host bindings/listeners, lifecycle hooks, templates, and queries. Directives are also instantiated eagerly, while services remain dormant until something else injects it. Directives have full control over the DOM.
Using directives as a service isn't a new concept. It's common practice to inject directives and components from other components in the injector tree. But we don't always think of directives as services, or that we might want services to be like a directive.
@Store()
@Component()
export class UIButton {
@Input() disabled = false
@Action()
@HostListener("click")
animateClick() {
const { nativeElement } = inject(ElementRef)
if (!this.disabled) {
return dispatch(animateRipple(nativeElement), {
complete() {
console.log("animation done")
}
})
}
}
}
function animateRipple(element) {
// use your imagination
}
In Angular State Library, a @Store
is both a directive and a service. By using directives we can eliminate most of the boilerplate that comes with state management.
Template Providers
Since directives have inputs, we can make stores configurable. The store can then be re-used in different ways without sub-classing. This can be used to create a context API similar to React.
@Directive({
standalone: true,
selector: "ui-theme",
})
export class UITheme extends TemplateProvider {
color = "red"
}
The theme provider can then be configured from a parent component.
<ui-theme [value]="blueTheme">
<ui-button>Blue button</ui-button>
</ui-theme>
<ui-theme [value]="greenTheme">
<ui-button>Green button</ui-button>
</ui-theme>
@Component({
standalone: true,
imports: [UITheme],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UIApp {
blueTheme = {
color: "blue"
}
greenTheme = {
color: "green"
}
}
And consumed from a descendant component.
<div [style.color]="theme.color">
<ng-content></ng-content>
</div>
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UIButton {
theme = select(UITheme)
}
We now have a configurable and reactive store that can be composed anywhere in our application.
Host Directives
Host Directives are the perfect use case for directives as a service. In Angular 15 (pending) we will be able to add directives to a host component without adding them to the template. This means we can attach root stores directly to a root @Component
or Angular Element, which currently isn't possible.
@Component({
standalone: true,
selector: "ui-root",
hostDirectives: [
AuthStore,
ProfileStore,
NotificationStore
]
})
export class UIRoot {}
We will have to wait and see what the final solution looks like once it's released, but for now we can work around this by attaching the host directives to the component template instead.
<ng-container authStore profileStore notificationStore>
<router-outlet></router-outlet>
</ng-container>
When to Use a Directive Instead of a Service
This is just an opinion, but you might want to use a directive as a service if:
The service is stateful
A directive has the advantage of lifecycle hooks to properly manage long-lived subscriptions, react to input changes, and run change detection. It's also much easier to compose in templates.
The service depends on DOM elements or DOM events
While services can access the host element if it is provided in a directive, a directive can also use @Content
and @View
queries. It also has access to @HostListener
and @HostBinding
to make things easier.
The service needs to be initialized before it is injected
Directives are like ENVIRONMENT_INITIALIZER
, except it works at component scope instead of application scope. Directives are always eager.
Angular State Library is a yes on all fronts. You can also start using your own directives as a service today.
Help Wanted
This project is currently a proof of concept. It's no where near production ready. If you are interested in contributing or dogfooding feel free to open a line on Github discussions, or leave a comment with your thoughts below.
Thanks for reading!
Top comments (0)