DEV Community

Cover image for Directives as a Service
Michael Muscat
Michael Muscat

Posted on • Updated on

Directives as a Service

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
}
Enter fullscreen mode Exit fullscreen mode
Can your redux store do this?

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"
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
@Component({
  standalone: true,
  imports: [UITheme],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UIApp {
  blueTheme = {
    color: "blue"
  }
  greenTheme = {
    color: "green"
  }
}
Enter fullscreen mode Exit fullscreen mode

And consumed from a descendant component.

<div [style.color]="theme.color">
  <ng-content></ng-content>
</div>
Enter fullscreen mode Exit fullscreen mode
@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UIButton {
  theme = select(UITheme)
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
Good enough for now

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)