Do you sometimes wish that Angular had functional composition like other frameworks?
Well too bad, Angular is wedded to classes. And despite many attempts to fit square peg functions into round hole classes it just doesn't work well in practice. Angular does what it does for a reason and we shouldn't try to make it something it's not.
But what if you want it anyway and stumble across a way to make it happen with just one line of code?
<p>Count: {{ counter.value }}</p>
@Auto()
@Component()
class MyComponent {
// nothing unusual?
counter = new Counter()
ngOnInit() {
console.log("so far so good")
}
}
@Auto()
class Counter {
value = 0
interval
increment(ctx) {
ctx.value++
}
ngOnInit() {
console.log("wait what?")
this.interval = setInterval(this.increment, 1000, this)
}
ngOnDestroy() {
console.log("this shouldn't be possible")
clearInterval(this.interval)
}
}
One of they key features of functional composition is the ability to extract and co-locate lifecycle hooks into a single unit of logic that can be reused across many components. In Angular this unit of logic is normally represented by services decorated with Injectable
.
Services however, have some downsides compared to functional composition:
- Inability to pass parameters to a service from a component when it is created
- Leaky services. Some code further down the tree could inject and use it in unintended ways.
- Extra ceremony of having to add to providers array.
- Unless provided in a component, inability to update the view
- Accidentally injecting a parent instance because it wasn't provided correctly, or omitting
@Self
. - No access to the lifecycle of a directive.
Angular only supports lifecycle hooks on decorated classes, but in the example above we have an arbitrary Counter
object with lifecycle hooks. How does that work? Why now?
Angular 14
In my recent article Angular 14 dependency injection unlocked I explain how inject
became a public API for all Angular decorated classes including components. This liberates us from constructors as the only means to instantiate our dependencies, making the following possible without any hacks at all:
@Component()
class MyComponent {
resource = new Resource()
}
class Resource() {
http = inject(HttpClient) // now works in Angular 14!
}
The ability to inject dependencies is another key piece of the composition puzzle. We just need some way to hook into the component lifecycle.
Automatic Composition
antischematic / angular-auto
Auto decorators for Angular
Auto decorators for Angular
@Auto()
@Component({
template: `{{ count }}`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
@Input()
count = 0;
object = new Resource()
@Subscribe()
autoIncrement = interval(1000).pipe(
tap((value) => this.count = value + 1)
);
@Unsubscribe()
subscription = new Subscription();
ngOnInit() {
console.log("I am called!")
}
}
@Auto()
export class Resource {
private http = inject(HttpClient)
@Check()
value
ngOnInit() {
console.log("I am also called!")
}
fetch(params) {
this.http.get(endpoint, params)
.subscribe(
…With one line of code, just add Auto
to your component, directive, service, etc. and it instantly composes with other Auto
decorated objects.
Behind the scenes this decorator will cascade lifecycle hooks to any Auto
object created inside a class field initializer or constructor. These are guarded so that component life cycles don't leak to services and vice versa.
Try it out!
But There's a Catch
For now this is only possible by mutating some private Angular APIs. So it's definitely not something you should try in production 🙇
Angular Friendly?
If you flinch when seeing useXXX
in other frameworks, rest assured that I am not advocating for this to become a thing in Angular.
In Angular we use new XXX
.
Happy Coding!
Top comments (0)