Директивы Angular — удобный способ инкапсулировать переиспользуемое поведение: директивы могут применять атрибуты, CSS-классы и слушатели событий к элементу.
API композиции директив позволяет применять директивы к host-элементу компонента изнутри TypeScript-класса компонента.
Добавление директив к компоненту
Директивы применяются к компоненту через свойство hostDirectives в декораторе
компонента. Такие директивы называют host-директивами.
В этом примере директива MenuBehavior применяется к host-элементу AdminMenu. Это
работает аналогично применению MenuBehavior к элементу <admin-menu> в шаблоне.
@Component({
selector: 'admin-menu',
templateUrl: './admin-menu.html',
hostDirectives: [MenuBehavior],
})
export class AdminMenu {}
Когда фреймворк рендерит компонент, Angular также создаёт экземпляр каждой host-директивы. Host-привязки директив применяются к host-элементу компонента. По умолчанию inputs и outputs host-директив не входят в публичный API компонента. См. Включение inputs и outputs ниже.
Angular применяет host-директивы статически на этапе компиляции. Нельзя динамически добавлять директивы в runtime.
Директивы в hostDirectives не должны указывать standalone: false.
Angular игнорирует selector директив, применённых в свойстве hostDirectives.
Включение inputs и outputs
Когда вы применяете hostDirectives к компоненту, inputs и outputs host-директив
по умолчанию не включаются в API компонента. Их можно явно включить в API компонента,
расширив запись в hostDirectives:
@Component({
selector: 'admin-menu',
templateUrl: './admin-menu.html',
hostDirectives: [
{
directive: MenuBehavior,
inputs: ['menuId'],
outputs: ['menuClosed'],
},
],
})
export class AdminMenu {}
Явно указав inputs и outputs, потребители компонента с hostDirective могут
привязывать их в шаблоне:
<admin-menu menuId="top-menu" (menuClosed)="logMenuClosed()"></admin-menu>
Кроме того, inputs и outputs из hostDirective можно алиасить, чтобы настроить API
компонента:
@Component({
selector: 'admin-menu',
templateUrl: './admin-menu.html',
hostDirectives: [
{
directive: MenuBehavior,
inputs: ['menuId: id'],
outputs: ['menuClosed: closed'],
},
],
})
export class AdminMenu {}
<admin-menu id="top-menu" (closed)="logMenuClosed()"></admin-menu>
Добавление директив к другой директиве
hostDirectives можно добавлять и к другим директивам, не только к компонентам. Это позволяет
транзитивно агрегировать несколько поведений.
В следующем примере определены две директивы — Menu и Tooltip. Затем поведение
этих двух директив композируется в MenuWithTooltip. Наконец, MenuWithTooltip
применяется к SpecializedMenuWithTooltip.
Когда SpecializedMenuWithTooltip используется в шаблоне, создаются экземпляры всех
Menu, Tooltip и MenuWithTooltip. Host-привязки каждой из этих директив применяются к host-
элементу SpecializedMenuWithTooltip.
@Directive({
/* ... */
})
export class Menu {}
@Directive({
/* ... */
})
export class Tooltip {}
// MenuWithTooltip can compose behaviors from multiple other directives
@Directive({
hostDirectives: [Tooltip, Menu],
})
export class MenuWithTooltip {}
// CustomWidget can apply the already-composed behaviors from MenuWithTooltip
@Directive({
hostDirectives: [MenuWithTooltip],
})
export class SpecializedMenuWithTooltip {}
Семантика host-директив
Порядок выполнения директив
Host-директивы проходят тот же жизненный цикл, что и компоненты и директивы, используемые напрямую в шаблоне. Однако host-директивы всегда выполняют конструктор, хуки жизненного цикла и привязки до компонента или директивы, к которым они применены.
Следующий пример показывает минимальное использование host-директивы:
@Component({
selector: 'admin-menu',
templateUrl: './admin-menu.html',
hostDirectives: [MenuBehavior],
})
export class AdminMenu {}
Порядок выполнения здесь:
- Создаётся экземпляр
MenuBehavior - Создаётся экземпляр
AdminMenu MenuBehaviorполучает inputs (ngOnInit)AdminMenuполучает inputs (ngOnInit)MenuBehaviorприменяет host-привязкиAdminMenuприменяет host-привязки
Такой порядок операций означает, что компоненты с hostDirectives могут переопределять любые host-привязки,
заданные host-директивой.
Этот порядок операций распространяется и на вложенные цепочки host-директив, как в следующем примере.
@Directive({...})
export class Tooltip { }
@Directive({
hostDirectives: [Tooltip],
})
export class CustomTooltip { }
@Directive({
hostDirectives: [CustomTooltip],
})
export class EvenMoreCustomTooltip { }
В примере выше порядок выполнения:
- Создаётся экземпляр
Tooltip - Создаётся экземпляр
CustomTooltip - Создаётся экземпляр
EvenMoreCustomTooltip Tooltipполучает inputs (ngOnInit)CustomTooltipполучает inputs (ngOnInit)EvenMoreCustomTooltipполучает inputs (ngOnInit)Tooltipприменяет host-привязкиCustomTooltipприменяет host-привязкиEvenMoreCustomTooltipприменяет host-привязки
Внедрение зависимостей
Компонент или директива, указывающие hostDirectives, могут внедрять экземпляры этих host-
директив и наоборот.
При применении host-директив к компоненту и компонент, и host-директивы могут определять провайдеры.
Если компонент или директива с hostDirectives и эти host-директивы предоставляют один и тот же
injection token, провайдеры, определённые классом с hostDirectives, имеют приоритет над провайдерами,
определёнными host-директивами.
Дедупликация host-директив
Когда одна и та же директива появляется более одного раза в разрешённом дереве host-директив, она автоматически дедуплицируется, а не вызывает ошибку. Для выбора выжившего совпадения используются два детерминированных правила.
Совпадение по шаблону имеет приоритет
Если директива совпадает с элементом один раз через селектор шаблона и также появляется как host-директива, Angular сохраняет только совпадение по шаблону и отбрасывает все совпадения host-директив.
Ментальная модель: совпадение host-директивы представляет Partial<YourDirective> — частичное
применение, где открыты только inputs и outputs, явно перечисленные в hostDirectives,
тогда как совпадение по шаблону представляет полную директиву с полным публичным API.
@Directive({selector: '[hoverable]'})
export class Hoverable {}
@Component({
selector: 'app-button',
hostDirectives: [Hoverable],
})
export class Button {}
<!-- Hoverable is matched by selector AND as a host directive of Button. -->
<!-- Angular keeps only the selector match, which has the full public API. -->
<app-button hoverable></app-button>
Несколько совпадений host-директив объединяются
Если одна и та же директива появляется более одного раза как host-директива — например, когда две
директивы объявляют общую зависимость в своих hostDirectives — Angular объединяет все
экземпляры в один экземпляр директивы. Отображения inputs и outputs из всех экземпляров
комбинируются.
Это решает классическую проблему ромба в композиции host-директив:
// A shared behavior that both triggers need
@Directive({
host: {
'[attr.data-trigger-id]': 'triggerId',
},
})
export class TriggerRef {
readonly triggerId = `trigger-${crypto.randomUUID()}`;
}
// Two separate triggers, each declaring TriggerRef as a host directive
@Directive({
selector: '[popoverTrigger]',
hostDirectives: [TriggerRef],
})
export class PopoverTrigger {
readonly triggerRef = inject(TriggerRef);
}
@Directive({
selector: '[dropdownTrigger]',
hostDirectives: [TriggerRef],
})
export class DropdownTrigger {
readonly triggerRef = inject(TriggerRef);
}
<!-- Angular keeps one TriggerRef instance, shared by both triggers. -->
<button popoverTrigger dropdownTrigger>Actions</button>
ПОЛЕЗНО: Поскольку Angular создаёт только один экземпляр общей директивы, и PopoverTrigger,
и DropdownTrigger получают один и тот же экземпляр TriggerRef при внедрении.
Конфликтующие алиасы
Когда Angular объединяет дублирующиеся совпадения host-директив, он также объединяет их отображения inputs и outputs. Если два экземпляра одной host-директивы открывают один и тот же input или output под разными алиасами, Angular выбрасывает ошибку на этапе компиляции (NG8024)
@Directive({
selector: '[popoverTrigger]',
hostDirectives: [{directive: TriggerRef, inputs: ['triggerId: popoverTriggerId']}],
})
export class PopoverTrigger {}
@Directive({
selector: '[dropdownTrigger]',
hostDirectives: [
{directive: TriggerRef, inputs: ['triggerId: dropdownTriggerId']}, // different alias!
],
})
export class DropdownTrigger {}
<!-- Error: triggerId is exposed as both "popoverTriggerId" and "dropdownTriggerId". -->
<button popoverTrigger dropdownTrigger></button>
Чтобы разрешить это, убедитесь, что оба пути открывают общий input или output под одним алиасом, либо не открывайте его вовсе.