Как повторно использовать декораторы изнутри декораторов в TypeScript
Я пытаюсь создать некоторые функциональные возможности упаковки Angular2 декораторов. Я хочу упростить процесс добавления класса CSS к хосту, поэтому я создал следующее:
ВНИМАНИЕ: НЕ РАБОТАЕТ С AOT-КОМПИЛЯЦИЕЙ
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;
};
}
Я также хочу иметь возможность создать другой декоратор, который добавляет определенный класс CSS.
/**
* 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"}];
}
Я получаю сообщение об ошибке:
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'
Я смог обойти это, создав функцию, которая вызывается обоими
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");
}
Есть ли способ заставить работать первый стиль без необходимости использования вспомогательной функции или дублирования кода? Я понимаю, что моя попытка не велика и не имеет смысла, но я не смог разобраться с сообщением об ошибке.
Версия, которая (вроде бы) совместима с AOT Поскольку следующее намного проще, это не приводит к сбою компилятора AOT. Однако обратите внимание, что пользовательские декораторы удаляются при использовании компилятора webpack, поэтому он работает только при компиляции с помощью 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);
}
1 ответ
В сигнатуре типа вашего декоратора есть несколько ошибок, которые мешают ему пройти проверку типов.
Чтобы их исправить, важно понимать разницу между декораторами и фабриками декораторов.
Как вы можете подозревать, фабрика декораторов - это не что иное, как функция, которая возвращает декоратор.
Используйте фабрику декораторов, когда вы хотите настроить декоратор, который применяется путем его параметризации.
Чтобы использовать ваш код в качестве примера, ниже приведена фабрика декораторов
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;
};
}
Фабрика декораторов принимает параметр, который настраивает CSS, который используется декоратором, который он возвращает, и заменяет цель (которая была полной).
Но теперь мы хотим повторно использовать фабрику декораторов, чтобы добавить некоторые специфические CSS. Это означает, что нам нужен старый добрый декоратор, а не фабрика.
Поскольку фабрика декораторов возвращает именно то, что нам нужно, мы можем просто назвать это.
Чтобы использовать ваш второй пример,
export const TopLevelRoutedComponent = AddCssClassToHost("content-container");
Теперь мы сталкиваемся с ошибкой в приложении
@TopLevelRoutedComponent
@Component({...})
export class AdminNavigationComponent {
navItems = [{nameKey: "Multisite", routerLink: "multisite"}];
}
и мы должны рассмотреть почему.
Вызов фабричной функции создает экземпляры параметров ее типа.
Вместо универсальной фабрики декораторов, которая возвращает неуниверсальный декоратор, нам нужна фабрика, которая создает универсальные декораторы!
То есть, TopLevelRoutedComponent
должен быть общим.
Мы можем сделать это, просто переписав нашу оригинальную фабрику декораторов, переместив параметр типа в декоратор, который он возвращает
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;
};
}
Вот живой пример