Директивная композиция Angular
Давайте взглянем на одну из самых ожидаемых функций Angular. Angular 15 уже на горизонте, а вместе с ним и множество замечательных функций. Одна из них - директивная композиция.
Композиция директивы была одной из наиболее обсуждаемых проблем Angular на GitHub. Давайте посмотрим, что это такое.
Чтобы объяснить состав директив, мы рассмотрим реальный вариант использования в виде цифровой доски объявлений. Мы хотим реализовать pinboard, который отображает pins. Каждый pin должен отображать информационный текст в виде подсказки при наведении курсора мыши. Кроме того, каждый pin можно перетаскивать и изначально поворачивать.
Код такого приложения может выглядеть примерно так.
<pinboard>
<pin image="rocket"></pin>
<pin image="beer"></pin>
<pin image="keyboard"></pin>
<pin image="testing"></pin>
<pin image="coffee"></pin>
</pinboard>
У нас есть компонент и мы проецируем на него кучу контактов (pins). На этом этапе наши контакты (pins) будут отображаться так, как показано на рисунке сверху. Это означает, что они не повернуты изначально, их нельзя перетаскивать, и мы не будем отображать всплывающую подсказку. Все функции, упомянутые выше, отсутствуют.
Конечно, мы могли бы пойти дальше и реализовать эти функции прямо в компоненте pin. Но, к счастью, наша кодовая база уже содержит несколько удобных директив с желаемой функциональностью.
В нашем распоряжении есть DragableDirective
, RotateDirective
и TooltipDirective
. Давайте используем эти директивы атрибутов, чтобы добавить недостающие функции к нашим pin.
<pinboard #dragZone>
<pin rotate="45deg" tooltip="Ship new products" dragable [dragzone]="pinboard"
image="rocket"
></pin>
<pin rotate="-20deg" tooltip="A good beer after a day of coding" dragable [dragzone]="pinboard"
image="beer"
></pin>
<pin rotate="0deg" tooltip="My favourite Keyboard, the Moonlander" dragable [dragzone]="pinboard"
image="keyboard"
></pin>
<pin rotate="10deg" tooltip="Write tests for better code" dragable [dragzone]="pinboard"
image="testing"
></pin>
<pin rotate="25deg" tooltip="No coffee no code" dragable [dragzone]="pinboard"
image="coffee"
></pin>
</pinboard>
Каждый pin
теперь применяет директиву атрибута rotate
и передает заданные начальные градусы поворота. Затем есть директива атрибута tooltip
с текстом всплывающей подсказки и, что не менее важно, атрибут dragable
с дополнительным вводом dragZone
.
Зона перетаскивания необходима, потому что вы хотите иметь возможность перетаскивать только контакты внутри доски.
Это хороший подход, но он имеет некоторые недостатки. Чтобы функция PinComponent
была полной, разработчик должен помнить, какие директивы необходимы, и должен применять все директивы самостоятельно.
Разве не было бы удобнее, если бы мы могли предоставить PinComponent
функции перетаскивание, всплывающей подсказки и поворота прямо из коробки и при этом повторно использовать наши директивы?
Зачем нам нужна директива композиции?
До сих пор мы могли повторно использовать директивы в наших компонентах, используя наследование. Например, чтобы получить функциональность перетаскивания, мы могли бы расширить наш PinComponent
.
export class PinComponent extends DragableDirective implements OnInit {
}
Приятно то, что используя наследование, мы можем наследовать все функции Angular, такие как HostBinding
или HostListeners
и т.д. И это также хорошо работает с проверкой типов шаблонов и минификаторами.
Но у этого подхода есть свои ограничения. Как насчет всплывающей подсказки или функции поворота? Мы можем расширить только один класс, верно?
Кроме того, у нас нет возможности сузить общедоступный API PinComponent
. Общедоступный API директив проникает в производные классы.
Это не оптимальное решение; поэтому теперь мы получаем директиву композиции.
Директива композиции
Новый API композиции директив вводит свойство hostDirectives
в декораторе Компонентов и Директив.
Значение свойства представляет собой массив объектов конфигурации. Каждый объект конфигурации содержит обязательный атрибут directive
и два необязательных свойства input
и output
.
hostDirectives?: (Type<unknown> | {
directive: Type<unknown>;
inputs?: string[];
outputs?: string[];
})[];
Давайте продолжим и попробуем использовать это совершенно новое свойство в нашем PinComponent
для добавления всплывающей подсказки, поворота и перетаскивания.
@Component({
selector: 'pin',
template: `
<img [src]="'assets/' + image + '.svg'"/>
`,
hostDirectives: [
{directive: TooltipDirective},
{directive: DragableDirective},
{directive: RotateDirective},
]
})
export class PinComponent implements OnInit {
При этом мы также можем удалить директиву атрибута перетаскивания из pin в нашем HTML.
<pin rotate="25deg" tooltip="No coffee no code" [dragzone]="pinboard"
image="coffee" (pinGrabbed)="pinGrabbed()"
></pin>
Если бы нам не нужно было бы передавать tooltip
и ввод rotate
, мы могли бы также удалить эти атрибуты, поскольку теперь они предоставляются hostDirectives
в PinComponent
. Но нам по -прежнему нужны эти атрибуты, а также атрибут dragzone
, потому что эти атрибуты являются вводными данными.
Давайте продолжим и запустим наше приложение. Вместо отличных функций мы получаем кучу ошибок компиляции:
ERROR
src/app/pin.component.ts:20:17 - error NG2014: Host directive TooltipDirective must be standalone 20 {directive: TooltipDirective}, ~~~~~~~~~~~~~~~~
Это понятное сообщение об ошибке, которое информирует нас об одном из ограничений hostDirectives
.
hostDirectives
можно использовать только с автономными директивами. Давайте продолжим и преобразуем наши директивы в автономные директивы.
Автономные компоненты были представлены в качестве предварительного просмотра для разработчиков в Angular 14.
Чтобы преобразовать наши директивы в автономные директивы, мы должны добавить свойство standalone
со значением true
в декораторе директив и переместить их из массива declarations
в массив imports
в AppModule
.
Давайте попробуем.
Всплывающая подсказка прерывается при наведении курсора, значок не поворачивается, а pin нельзя перетащить. Почему так? Похоже, что ввод директив больше не работает. Почему? Ведь мы по прежнему передаем их в HTML в качестве атрибутов компонента pin.
Всякий раз, когда вы используете hostDirectives
, все Inputs
и Outputs
по умолчанию скрыты. Мы явно должны предоставить общедоступный API для объектов конфигурации inputs
и outputs
.
hostDirectives: [
{directive: TooltipDirective, inputs: ['tooltip']},
{directive: DragableDirective, inputs: ['dragzone']},
{directive: RotateDirective, inputs: ['rotate']}
]
Это отличная функция, поскольку она дает нам полный контроль над общедоступным API нашего компонента. Давайте запустим наш код.
Мы получаем всплывающую подсказку при наведении курсора, получаем вращение и, конечно же, pin можно перетаскивать. Все функции работают. А как насчет свойства outputs
?
Точно так же, как мы настроили наши Inputs
, мы также можем настроить наши Outputs
. Например, наш DragableDirective
генерирует событие, которое уведомляет вас, когда вы захватываете Pin. Мы можем использовать свойство outputs
для включения события pinGrabbed
в наш общедоступный API.
hostDirectives: [
{directive: TooltipDirective, inputs: ['tooltip']},
{directive: DragableDirective, inputs: ['dragzone'], outputs: ['pinGrabbed']},
{directive: RotateDirective, inputs: ['rotate']}
]
Псевдонимы
Еще одной приятной особенностью композиции директив - сглаживание inputs
и inputs
. dragZone
довольно общее название для нашего Input. В контексте pin было бы точнее назвать Input pinBoard
, а не dragzone
.
Давайте используем синтаксис псевдонима для переименования свойства dragzone
в DragableDirective
.
hostDirectives: [
// ...
{directive: DragableDirective, inputs: ['dragzone: pinBoard'], outputs: ['pinGrabbed']},
// ...
]
После создания псевдонима, мы можем использовать ввод pinBoard
на pin.
<pin rotate="0deg" tooltip="My favourite Keyboard, the Moonlander" [pinBoard]="pinboard"
image="keyboard" (pinGrabbed)="pinGrabbed()"
></pin>
Псевдоним работает точно так же для outputs
.
Заключение
Композиция директив - это уникальная и захватывающая функция, которая предлагает следующие преимущества.
- Мы можем применить к хосту столько директив, сколько захотим. Ограничений нет.
- По умолчанию все Inputs и Outputs скрыты. Мы можем использовать свойства
inputs
иoutputs
для включения в наш общедоступный API и сделать их видимыми. - Композиция директив работает с проверкой типа шаблона.
- Все функции директив, такие как
HostBinding
, Injection Tokens и т.д., работают с директивной композицией. - Директивы хоста могут быть связаны цепочкой. У вас могут быть директивы хоста, построенных на основе других директив хоста.
Как бы все это и не выглядело великолепно, все таки таже имеются некоторые ограничения:
- Как было обнаружено на протяжении всего поста, директивы хоста должны быть автономными.
- Компоненту может соответствовать только одна директива. Обязательно используйте директиву только один раз внутри цепочки.
- Компоненты не могут использоваться в качестве директив хоста.
Добро пожаловать в мир совершенства Angular.