DEV Community

Michael Muscat
Michael Muscat

Posted on • Updated on

Experimental class composition with Angular 14

Do you sometimes wish that Angular had functional composition like other frameworks?

Angular webdev

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

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

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

GitHub logo 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!")
   }
}
Enter fullscreen mode Exit fullscreen mode
@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(
Enter fullscreen mode Exit fullscreen mode

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)