I'm trying to create some functionality wrapping Angular2 decorators. I want to simplify the process of adding a CSS class to a host so I created the following:
WARNING: DOES NOT WORK WITH AOT COMPILATION
type Constructor = {new(...args: any[]): {}};
export function AddCssClassToHost<T extends Constructor>(cssClass: string) {
return function (constructor: T) {
class Decorated extends constructor {
@HostBinding("class") cssClass = cssClass;
}
// Can't return an inline class, so we name it.
return Decorated;
};
}
I also want to be able to create another decorator that adds a specific CSS class.
/**
* Decorator to be used for components that are top level routes. Automatically adds the content-container class that is
* required so that the main content scrollbar plays nice with the header and the header won't scroll away.
*/
export function TopLevelRoutedComponent<T extends Constructor>(constructor: T) {
// This causes an error when called as a decorator
return AddCssClassToHost("content-container");
// Code below works, I'd like to avoid the duplication
// class Decorated extends constructor {
// @HostBinding("class") cssClass = "content-container";
// }
// return Decorated;
}
// Called like
@TopLevelRoutedComponent
@Component({
selector: "vcd-administration-navigation",
template: `
<div class="content-area">
<router-outlet></router-outlet>
</div>
<vcd-side-nav [navMenu]="navItems"></vcd-side-nav>`
})
export class AdminNavigationComponent {
navItems: NavItem[] = [{nameKey: "Multisite", routerLink: "multisite"}];
}
The error message I get is
TS1238: Unable to resolve signature of class decorator when called as an expression. Type '(constructor: Constructor) => { new (...args: any[]): AddCssClassToHost<Constructor>.Decorated; p...' is not assignable to type 'typeof AdminNavigationComponent'. Type '(constructor: Constructor) => { new (...args: any[]): AddCssClassToHost<Constructor>.Decorated; p...' provides no match for the signature 'new (): AdminNavigationComponent'
I was able to work around it by creating a function that is called by both
function wrapWithHostBindingClass<T extends Constructor>(constructor: T, cssClass: string) {
class Decorated extends constructor {
@HostBinding("class") cssClass = cssClass;
}
return Decorated; // Can't return an inline decorated class, name it.
}
export function AddCssClassToHost(cssClass: string) {
return function(constructor) {
return wrapWithHostBindingClass(constructor, cssClass);
};
}
export function TopLevelRoutedComponent(constructor) {
return wrapWithHostBindingClass(constructor, "content-container");
}
Is there a way to make the first style work, without needing a helper function or duplicating the code? I realize my attempt is not great and it doesn't make sense, but I couldn't get my head around the error message.
Version that is (kind of) AOT compatible Because the following is a lot simpler, it doesn't cause the AOT compiler to crash. However, note that custom decorators are being stripped if using the webpack compiler, so it only works when compiled with ngc.
export function TopLevelRoutedComponent(constructor: Function) {
const propertyKey = "cssClass";
const className = "content-container";
const descriptor = {
enumerable: false,
configurable: false,
writable: false,
value: className
};
HostBinding("class")(constructor.prototype, propertyKey, descriptor);
Object.defineProperty(constructor.prototype, propertyKey, descriptor);
}
There are a few errors in your decorator's type signature that are preventing it from satisfying the typechecker.
In order to fix them, it is essential that you understand the distinction between decorators and decorator factories.
A decorator factory is, as you might suspect, nothing more than a function that returns a decorator.
Use a decorator factory when you want to customize the decorator that is applied by parameterizing it.
To use your code as an example, the following is a decorator factory
export type Constructor<T = object> = new (...args: any[]) => T;
export function AddCssClassToHost<C extends Constructor>(cssClass: string) {
return function (Class: C) {
// TypeScript does not allow decorators on class expressions so we create a local
class Decorated extends Class {
@HostBinding("class") cssClass = cssClass;
}
return Decorated;
};
}
The decorator factory takes a parameter that customizes the css that is used by the decorator it returns and replaces the target with (that was a mouthful).
But now we want to re-use the decorator factory to add some specific css. That means we need a plain old decorator, not a factory.
Since the decorator factory returns just what we need, we can just call it.
To use your second example,
export const TopLevelRoutedComponent = AddCssClassToHost("content-container");
Now we encounter the error on application
@TopLevelRoutedComponent
@Component({...})
export class AdminNavigationComponent {
navItems = [{nameKey: "Multisite", routerLink: "multisite"}];
}
and we must consider why.
Calling a factory function instantiates its type parameters.
Instead of a generic decorator factory that returns a non-generic decorator, we need a factory that creates generic decorators!
That is, TopLevelRoutedComponent
needs to be generic.
We can do that by simply rewriting our original decorator factory, moving the type parameter to decorator it returns
export function AddCssClassToHost(cssClass: string) {
return function <C extends Constructor>(Class: C) {
// TypeScript does not allow decorators on class expressions so we create a local
class Decorated extends Class {
@HostBinding("class") cssClass = cssClass;
}
return Decorated;
};
}
Here is a live example