DevGang
Авторизоваться

Директивная композиция 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. Но, к счастью, наша кодовая база уже содержит несколько удобных директив с желаемой функциональностью. 

В нашем распоряжении есть DragableDirectiveRotateDirective и 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.

Давайте попробуем.

Состояние наведения PinComponent со сломанной всплывающей подсказкой. Директива всплывающей подсказки выполняется, но текст всплывающей подсказки не определен.
Состояние наведения PinComponent со сломанной всплывающей подсказкой. Директива всплывающей подсказки выполняется, но текст всплывающей подсказки не определен.

Всплывающая подсказка прерывается при наведении курсора, значок не поворачивается, а pin нельзя перетащить. Почему так? Похоже, что ввод директив больше не работает. Почему? Ведь мы по прежнему передаем их в HTML в качестве атрибутов компонента pin.

Всякий раз, когда вы используете hostDirectives, все Inputs и Outputs по умолчанию скрыты. Мы явно должны предоставить общедоступный API для объектов конфигурации inputs и outputs

hostDirectives: [
  {directive: TooltipDirective, inputs: ['tooltip']},
  {directive: DragableDirective, inputs: ['dragzone']},
  {directive: RotateDirective, inputs: ['rotate']}
]

Это отличная функция, поскольку она дает нам полный контроль над общедоступным API нашего компонента. Давайте запустим наш код.

Повернутый и наведенный PinComponent отображает текст всплывающей подсказки, и его можно изменить с помощью перетаскивания.
Повернутый и наведенный PinComponent отображает текст всплывающей подсказки, и его можно изменить с помощью перетаскивания.

Мы получаем всплывающую подсказку при наведении курсора, получаем вращение и, конечно же, 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 и inputsdragZone довольно общее название для нашего 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.

#Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу