14 — Host bindings и host directives на пальцах
Цель: понять, как директива/компонент управляет своим собственным тегом (host
binding / host listener) и как одна директива может «приклеить» к этому тегу другие
директивы без обёрток в шаблоне (host directives). Точные file:line — в notes/v21/.
Что такое «хост»
Хост (host) — это сам элемент, на котором сидит директива/компонент.
<button appHighlight>Жми</button>
<!-- ↑ этот <button> — ХОСТ для директивы appHighlight -->
<app-card></app-card>
<!-- ↑ этот <app-card> — ХОСТ для компонента Card -->
Шаблон компонента описывает то, что внутри. А host binding описывает сам
внешний тег. Это разные вещи: внутри компонента ты не видишь свой <app-card> —
для управления им и нужны host bindings.
Host binding — «привяжи к своему тегу»
host: {} в метаданных (или старые @HostBinding/@HostListener) позволяют
привязать к хост-элементу классы, атрибуты, стили и слушатели событий:
@Directive({
selector: '[appHighlight]',
host: {
'[class.active]': 'isOn', // класс по условию
'[style.color]': 'color', // стиль
'[attr.aria-pressed]': 'isOn', // атрибут
'(click)': 'toggle()', // слушатель события (host listener)
'role': 'button', // статический атрибут
},
})
export class HighlightDirective {
isOn = false;
color = 'red';
toggle() { this.isOn = !this.isOn; }
}
Аналогия: обычный биндинг в шаблоне (<div [class.x]="...">) направлен внутрь,
на чужие теги. Host binding направлен на самого себя — на тег, где висит директива.
Как host bindings работают под капотом
Помни про два режима из 01-kompilyaciya (create rf&1 / update rf&2). Host
bindings компилируются в отдельную функцию hostBindings, и Angular складывает их в
список операций — hostBindingOpCodes в TView (см. 06-view-lview):
TView.hostBindingOpCodes = [
~элемент, индекс директивы, её hostBindings-функция,
...
]
На каждом change detection Angular проходит этот список (processHostBindingOpCodes),
выбирает нужный хост-элемент и вызывает hostBindings(rf&2, директива) — та обновляет
класс/стиль/атрибут. То есть host bindings — часть того же update-прохода, просто
выполняются в контексте хост-элемента, а не внутренностей шаблона.
Host listener ((click)) регистрируется один раз на create — той же инструкцией
listener, что и обычные события в шаблоне, только цель — хост-элемент.
Host directives — «приклей чужую директиву к себе»
Вот более продвинутая и менее известная фича. Иногда хочется, чтобы компонент автоматически имел поведение другой директивы, не заставляя пользователя писать её в шаблоне.
Без host directives:
<!-- пользователь ВЫНУЖДЕН добавлять cdkDrag руками каждый раз -->
<app-card cdkDrag></app-card>
С host directives — компонент сам «приносит» директиву на свой хост:
@Component({
selector: 'app-card',
hostDirectives: [CdkDrag], // приклеили CdkDrag к хосту card
})
export class Card {}
// теперь просто <app-card> уже перетаскиваемый — cdkDrag писать не нужно
Это композиция поведения без обёрток в DOM: ни лишних тегов, ни директив в разметке пользователя. Поведение «вшито» в компонент.
Проброс inputs/outputs с алиасами
По умолчанию входы/выходы приклеенной директивы скрыты (инкапсуляция). Если хочешь открыть их наружу — перечисли явно, можно с переименованием:
@Component({
selector: 'app-menu',
hostDirectives: [{
directive: CdkMenuTrigger,
inputs: ['cdkMenuTriggerFor: menuFor'], // наружу как menuFor
outputs: ['cdkMenuOpened: opened'], // наружу как opened
}],
})
export class Menu {}
// использование: <app-menu [menuFor]="tpl" (opened)="...">
Слева — внутреннее имя в директиве, справа — публичное имя, которое увидит пользователь компонента.
Порядок применения (важный нюанс)
Host directives применяются раньше самой директивы/компонента-хозяина:
1. host directives (в порядке объявления) ← применяются ПЕРВЫМИ
2. сам компонент/директива
Зачем такой порядок: чтобы хозяин мог переопределить host bindings приклеенных директив (его значения «победят», т.к. применятся последними). Для DI это тоже значит, что host directives уже доступны к моменту создания хозяина.
Ограничения: приклеивать можно только standalone-директивы (не компоненты), и
состав hostDirectives фиксирован — подменять директивы по условию в рантайме нельзя.
Допускается лишь форма-функция hostDirectives: () => [Dir] (для forwardRef / циклов),
но и она задаёт фиксированный список, известный на компиляции.
Где это реально используется
- Angular CDK / Material:
CdkMenuTrigger,CdkDragи т.п. часто подмешивают через host directives, чтобы скрыть «кухню». - Свои «миксины поведения»: директива-аналитика, директива-доступность (a11y роли), директива-тема — приклеиваешь к компонентам, не засоряя их шаблоны.
Карта
HOST BINDING — управляю своим тегом
host: { '[class.x]':'expr', '(click)':'fn()' }
↓ компилируется в hostBindings-функцию
TView.hostBindingOpCodes → processHostBindingOpCodes (каждый update, rf&2)
↓
обновляются класс/стиль/атрибут хост-элемента
HOST DIRECTIVE — приклеиваю чужую директиву к своему тегу
hostDirectives: [Dir, { directive: Dir2, inputs:[...], outputs:[...] }]
↓ применяются ПЕРЕД хозяином (чтобы он мог переопределить)
поведение Dir работает на хосте без тегов/директив в разметке
Итог
- Host binding = биндинг на собственный тег директивы/компонента (
host: {}): классы, стили, атрибуты, события. Выполняется в общем update-проходе черезhostBindingOpCodes. - Host directive = «приклеить» standalone-директиву к своему хосту без обёрток в шаблоне; inputs/outputs можно пробросить наружу с алиасами.
- Host directives применяются до хозяина, поэтому хозяин может их переопределить.
Дальше → 15-zoneless-scheduler: что именно будит Angular и планирует один tick.