So to oversimplify things you can think of statements (and declarations for that matter) as somewhat pre-defined, static parts of the application code. They are static in the sense that you have to explicitly write them down as they are and they have to be known at compile time so to speak. For example, a function declaration or an if statement inside of it is written by you and is hardcoded. These are templates that will be used (executed) at runtime. Whereas expressions are the bread and butter of a program, the dynamic parts. You may have not realized it before, but anytime you’re calling a function that’s an expression, a call expression. Sticking to this example, it is easy to understand then that expressions allow the program to have dynamic behaviours.
Why’s all this prep talk about the building blocks of programming languages? Because now that we’ve understood what expressions are we can appreciate what it means for us that classes can be used as expressions. You can basically do anything you want with them, and place them almost anywhere you want them. That’s how we get back to the fact that we can write functions in the return classes.
Getting Started With Mixins in Angular
Let’s tackle some things about mixins before we go any further. Mixin functions should always take one optional parameter and that’s a potential base class constructor function. That’s how we can chain them together. This allows one mixin function to take another mixin function’s returned value (another class expression) as its input and use it as its own class expressions’ base class.
export function someMixin<T>(base?: Constructor<T> = class {} as T) {
return class extends base {}
}
What now? Did I just hint that even the extends keyword can be used with dynamic values? Yep, I did. Just take a sec to think about it. How else would we apply our first mixin function to a normal “child” class if we couldn’t use dynamic values with the extends keyword? In fact, let me get this out of the way quickly. The extends keyword requires a class constructor function reference or any expression that returns one. So this means that syntaxes like these are completely valid.
export class MyChildClass extends someMixin() {}
Another thing about mixins and their usage that’s important to mention is that TS only allows the args: any[]
parameter definition for mixin constructors. This makes sense as from the context of one mixin you wouldn't know how it's going to be used and what parameters it might receive. Since mixins have to pass their constructor params to each other. So the current mixin that you're working on could state what parameters it requires, it would have no way of knowing what other mixins used together with it would.
export function someMixin<T>(base?: Constructor<T> = class {} as T) {
return class extends base {
constructor(args: any[]) {
super(...args);
}
}
}
This means that if your mixin needs some params like a configuration you have to get creative and supply that param in a manner that will allow you to find it in the args array. One such solution is to use class instances for configuration in tandem with the instanceof
operator.
export class MixinConfig {
// whatever your mixin needs
}
export function someMixin<T>(base?: Constructor<T> = class {} as T) {
return class extends base {
constructor(args: any[]) {
super(...args);
const config = args.find(arg => arg instanceof MixinConfig);
if (!config) {
// handle the case where the mixin didn't receive it's config.
}
Object.assign(this, config);
}
}
}
Understanding Mixins in Angular
There are some caveats to using mixins in an Angular application. Let’s start with the most obvious one. Angular features that require the usage of Angular decorators like @Input and @output will not work in mixins. The child class extending the mixins can define them with no problem. But the Angular compiler will not execute or walk your code in order to figure out what your component class extends if the base class is not a statically known symbol. In fact, TS won’t even let you decorate your mixin class expression. This may change from TS version to another however the general takeaway is that Angular features don’t work in mixins.
You can pass constructor DI parameters onward to your mixins from your child class. You can also use the relatively new inject()
function to inject the mixins dependencies right inside the mixin class itself.
Lifecycle hooks can work in mixins but there are some gotchas here too. Prior to Angular v10 (not like anyone’s still maintaining legacy code bases with those Angular versions right?) lifecycle hooks were handled by Angular with component features. These features had to be discovered by the compiler at compile time. So even though you could define an ngOnInit
method in your mixin it wouldn't have been called because Angular didn't know about it. After v10 however, the compiler resolves the lifecycle hooks in a different manner. to be honest I'm not entirely sure how, so if you are interested go and dig deeper for yourself. The result of this is that from v10 lifecycle hooks defined in mixins will be called by Angular unless...
Property class on the prototype chain
I’m assuming that you have a basic understanding of how inheritance and classes work in JS. To be more specific the prototype chain. What happens then if multiple classes extend each other and multiple classes define the same method? Yes, you’ve guessed right: property name clash. 😉
In situations like these, the property lookup algorithm used by JS engines will walk upward the prototype chain checking each prototype object along the way for that certain property name. And if it is found the algorithm returns the found property’s value. So a more appropriate name for such situations is property name shadowing. You have basically no way of accessing the other properties with the same name.
Working with Lifecycle Hooks in Angular Mixins
By now we know that lifecycles can be used in mixins and that multiple lifecycle hooks of the same kind will shadow each other. Okay but then how are we going to use ngOnInit and the other hooks in our mixins? I mean, you can always call them by hand right? Sounds like a simple yet tedious solution. The thing is though, mixins don’t know about each other so-called super. ngOnInit in your mixin will yield you a TS error. Not to mention that even if this approach worked it would be very error-prone.
Get Started With The AutoHooks Decorator
We’re finally here! Let’s talk about how to solve the outlined problem with a TS decorator. What else did you expect? :D
AutHooks is a class decorator that has to be placed onto the child class that extends mixins. The decorator will walk the prototype chain of the decorated class and look for any and all Angular lifecycle hooks (and any other method configured in the decorator). It will then override each method it found with one that calls the next lifecycle hook method in line. The method chaining logic ensures and execution order of the lifecycle hooks is the same as the execution of class constructors. Meaning that the last lifecycle hook is called first and the execution proceeds to the first hook. The first is the hook of the child class in this case.
@Component({ ... })
@AutoHooks()
export class SomeComponent extends someMixins(AnotherMixin()){
// class implementation
}
The decorator can only be used once per prototype chain. This only really means that if you happen to use multiple regular classes in a prototype chain then only the bottom-most child class can use the decorator. However, the decorator does let you know if you’re using it incorrectly.
We did have to use some Angular private APIs to make the decorator work in ng versions less than 10. As I’ve mentioned before those versions work differently than the latter ones. But the usage of these private APIs should be fine since old versions won’t change in the future.
Wrap-up
It’s been a long journey up until here, but we managed to discover what mixins are and how can we use them with Angular. We saw what’s the problem with lifecycle hooks defined in mixins and also how to solve this problem.
Now, after all this, you might be thinking to yourself: Alright, pretty neat. But how could I use this solution myself?
Lucky for you, we’ve already done the heavy lifting for you. I and my team over at @Adroit Group have been working on a utility library for Angular full of useful stuff. Including, of course, the AutoHooks decorator I've introduced to you today.
So if this directive or the library, which we’ll certainly write more about soon, has sparked your interest, you can head on over to NPM or Github to take a look or even download and use it. 📚👀
I’d like to thank you for your time and attention in reading this article.
I was your tour guide Jonatán Ferenczfi, Frontend tech lead at @Adroit Group, Angular bro and high-functioning coffee addict. ☕
Until next time 👋
Originally published at adroitgroup.io and written by Jonatan Ferenczfi
Top comments (0)