Общение компонентов с помощью сигналов¶
Сигналы определяют будущее Angular. Однако сама концепция Signals — это лишь одна часть общей истории. Нам также нужен способ взаимодействия с (суб)компонентами через сигналы. В Angular 17.1 появились входные сигналы, а в Angular 17.2 мы получили двустороннее связывание на основе сигналов и поддержку запросов к контенту и представлению. В дополнение к входным сигналам в версии 17.3 появился новый API вывода.
В этой главе я покажу, как использовать эти новые возможности.
Исходный код
📁 Исходный код (см. разные ветки)
Входные сигналы¶
Входные сигналы позволяют нам получать данные через привязки свойств в виде сигналов. Для описания использования сигнальных входов я использую простой OptionComponent
, представляющий — для простоты — невыбираемую опцию. Здесь представлены три таких компонента:
Определение входного сигнала¶
Входные сигналы — это аналог традиционного декоратора @Input
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Эта функция input
подхватывается компилятором Angular и выдает исходный код для привязки свойств. Поэтому использовать ее следует только вместе со свойствами. Другие концепции взаимодействия, рассмотренные здесь, также используют эту технику.
Наличие функции вместо декоратора позволяет сообщить TypeScript о правильном типе и о том, включает ли она undefined
. В примере, показанном ранее, label
становится InputSignal<string>
— входным сигналом, предоставляющим string
. Значение undefined
невозможно, так как input.required
определяет обязательное свойство.
Сигнал InputSignal
всегда доступен только для чтения и может использоваться как сигнал Signal
. Например, шаблон выше запрашивает его текущее значение, вызывая геттер (label()
).
Привязка к входному сигналу¶
В случае с нашим InputSignal<string>
вызывающий должен передать string
:
1 2 3 |
|
Если эта строка приходит из Signal, мы должны прочитать ее в шаблоне:
1 |
|
Вычисляемые сигналы и эффекты как замена крючкам жизненного цикла¶
Все изменения переданного Сигнала будут отражены InputSignal
в компоненте. Внутри компонента оба сигнала связаны через граф, который поддерживает Angular. Хуки жизненного цикла, такие как ngOnInit
и ngOnChanges
, теперь могут быть заменены на computed
и effect
:
1 2 3 4 5 6 7 8 |
|
Опции для входных сигналов¶
Вот некоторые дополнительные опции для настройки InputSignal
:
Исходный код | Описание |
---|---|
label = input<string>(); | Необязательное свойство, представленное InputSignal<string | undefined> |
label = input(‘Hello’); | Необязательное свойство, представленное InputSignal<string> with an initial value of Hello |
label = input<string | undefined>(‘Hello’); | Необязательное свойство, представленное InputSignal<string | undefined> с начальным значением Hello . |
Требуемые входы не могут иметь значение по умолчанию!¶
По определению, input.required
не может иметь значение по умолчанию. На первый взгляд это логично, однако здесь есть подводный камень: Если вы попытаетесь прочитать значение требуемого ввода до того, как он будет привязан, Angular выбросит исключение.
Следовательно, вы не можете напрямую получить доступ к нему в конструкторе. Вместо этого можно использовать ngOnInit
или ngOnChanges
. Кроме того, использование входов внутри computed
или effect
всегда безопасно, так как они впервые срабатывают только после инициализации компонента:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Псевдонимы для входных сигналов¶
И input
, и input.require
также принимают объект-параметр, который позволяет определить alias
:
1 |
|
В этом случае вызывающая сторона должна привязаться к имени свойства, определенного псевдонимом:
1 2 3 4 5 6 7 |
|
В большинстве случаев следует избегать использования псевдонимов, поскольку они создают ненужную непрямую связь. Часто встречающееся исключение из этого правила — переименование одного из свойств директивы в соответствии с настроенным селектором атрибутов.
Трансформатор для входных сигналов¶
Трансформаторы уже были доступны для традиционных @Inputs
. Они позволяют преобразовать значение, переданное через привязку свойства. В следующем случае используется трансформатор booleanAttribute
, который можно найти в angular/core
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Этот трансформатор преобразует строки в булевы значения:
1 |
|
Также, если атрибут присутствует, но ему не было присвоено значение, предполагается true
:
1 |
|
Тип этого сигнала — InputSignal<boolean, unknown>
. Первый параметр типа (boolean
) представляет собой значение, полученное от трансформатора; второй (unknown
) — это значение, связанное в шаблоне вызывающей стороны и переданное трансформатору. Кроме booleanAttribute
, @angular/core
также предоставляет трансформатор numberAttribute
, который преобразует переданные строки в числа.
Если вы хотите реализовать собственный трансформатор, просто предоставьте функцию, принимающую связанное значение и возвращающую значение, которое должно быть использовано вызываемым компонентом:
1 2 3 |
|
Затем зарегистрируйте эту функцию в вашем input
:
1 2 3 4 5 6 7 |
|
Двустороннее связывание данных с помощью сигналов модели¶
Входные сигналы доступны только для чтения. Если вы хотите передать сигнал, который может быть обновлен вызываемым компонентом, вам нужно создать так называемый модельный сигнал. Чтобы продемонстрировать это, я использую простой TabbedPaneComponent
:
Вот как потребитель может использовать этот компонент:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Ему передается несколько TabComponent
. Кроме того, сигнал current
привязывается через двустороннее связывание. Для этого TabbedPaneComponent
должен предоставить сигнал модели, используя model
:
1 2 3 4 5 |
|
Здесь 0
— это начальное значение. Опции аналогичны опциям для ввода: model.required
определяет обязательное свойство, и вы можете указать псевдоним через объект options. Однако трансформатор не может быть определен.
Если этот компонент обновляет сигнал модели, новое значение распространяется до сигнала, связанного в шаблоне:
1 |
|
Двусторонняя привязка данных как комбинация входа и выхода¶
Как обычно в Angular, двусторонние привязки на основе сигналов могут быть определены с входом (только для чтения) и соответствующим выходом. Имя Output
должно быть именем Input
с суффиксом Change
. Таким образом, для current
нам нужно определить currentChange
:
1 2 3 4 5 |
|
Для настройки вывода мы используем новый API вывода. Для запуска события приложение должно вызвать метод emit
выхода:
1 2 3 |
|
Запросы содержимого с помощью сигналов¶
Представленный в предыдущем разделе TabbedPaneComponent
также позволяет нам продемонстрировать еще один вариант: Запросы содержимого, которые получают проецируемые компоненты или директивы.
Как показано выше, в TabbedPaneComponent
передается несколько TabComponents
. Они проецируются в представление TabbedPaneComponent
. Однако в данный момент мы хотим отображать только один из них. Следовательно, TabbedPaneComponent
должен получить программный доступ к своим TabComponents
. Это можно сделать с помощью новой функции contentChildren
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Функция contentChildren
является аналогом традиционного декоратора @ContentChildren. Так как TabComponent
был передан в качестве так называемого locator
, он возвращает Сигнал с Массивом, содержащим все спроецированные TabComponent
.
Наличие проецируемых узлов в виде сигнала позволяет нам реактивно проецировать их с помощью computed
. В приведенном примере эта опция используется для получения сигнала currentTab
.
Проецируемый TabComponent
использует этот сигнал, чтобы узнать, должен ли он быть видимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Для этого нам нужно знать, что мы можем получить всех родителей, расположенных в DOM, с помощью инъекции зависимостей. Сигнал visible
является производным от сигнала currentTab
.
Эта процедура обычна для реактивного мира: Вместо того, чтобы императивно задавать значения, они декларативно выводятся из других значений.
Запросы содержимого для потомков¶
По умолчанию запрос содержимого раскрывает только прямые дочерние элементы содержимого. «Внуки», как на 3-й вкладке ниже, игнорируются:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Чтобы также получить информацию о таких узлах, мы можем установить опцию descendants
в значение true
:
1 |
|
API вывода¶
Для обеспечения симметричности API в Angular 17.3 появился новый API вывода. Как уже было показано ранее, функция output
теперь используется для определения события, предоставляемого компонентом. Как и в случае с новым API ввода, компилятор Angular подхватывает вызов output
и выдает соответствующий код. Для запуска события используется метод emit
возвращаемого OutputEmitterRef
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Предоставление Observables в качестве выходов¶
Помимо этого простого способа настройки выходов, вы можете использовать Observable
в качестве источника для выхода. Для этого в RxJS interop layer есть функция outputFromObservable
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Функция outputFromObservable
преобразует Observable в OutputEmitterRef
. В показанном примере оператор scan
запоминает предыдущую активированную вкладку, а skip
гарантирует, что при первоначальной установке current
не будет выдано событие. Последнее обеспечивает равенство возможностей с показанным ранее примером.
Запросы просмотра с сигналами¶
В то время как запрос содержимого возвращает проецируемые узлы, запрос вида возвращает узлы из своего собственного представления. Это узлы, найденные в шаблоне соответствующего компонента. В большинстве случаев предпочтительным решением является использование привязки данных. Однако в некоторых ситуациях необходимо получить программный доступ к дочернему представлению.
Чтобы продемонстрировать, как запрашивать дочерние элементы представления, я использую простую форму для установки имени пользователя и пароля:
Оба поля input
помечены как required
. Если при нажатии Save
валидация не проходит, то фокус должно получить первое поле с ошибкой валидации. Для этого нам нужен доступ к директиве NgForm
, которую FormModule
добавляет к нашему тегу form
, а также к узлам DOM, представляющим поля input
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
|
Оба действия выполняются с помощью функции viewChild
. В первом случае в качестве локатора в примере передается тип NgForm
. Однако простое определение местоположения полей с типом не работает, так как может быть несколько дочерних полей с этим типом. Поэтому входы помечаются хендлами (#userName
и #password
), а в качестве локатора передается имя соответствующего хендла.
Дочерние элементы представления могут быть представлены различными типами: Тип соответствующего компонента или директивы, ElementRef
, представляющий его узел DOM, или ViewContainerRef
. Последний тип используется в следующем разделе.
Нужный тип можно указать с помощью опции read
, использованной в предыдущем примере.
Запросы и ViewContainerRef¶
Бывают ситуации, когда необходимо динамически добавить компонент в placeholder
. Примером могут служить модальные диалоги или тосты. Простой способ добиться этого — использовать директиву *ngComponentOutlet
. Более гибким способом является запрос к ViewContainerRef
плацдарма.
Контейнер представлений можно рассматривать как невидимый контейнер вокруг каждого компонента и статического HTML. Завладев им, вы можете добавлять другие компоненты или шаблоны.
Чтобы продемонстрировать это, я использую простой пример, показывающий тост:
В примере используется ng-container
в качестве заполнителя:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Свойство read
дает понять, что мы хотим читать не компонент-плейсхолдер, а его ViewContainerRef
. Метод createComponent
инстанцирует и добавляет ToastComponent
. Возвращаемый ComponentRef
используется для установки свойства label
нового компонента. Для этого используется его метод setInput
. Через две секунды метод destroy
снова удаляет тост.
Для простоты здесь был жестко закодирован компонент ToastComponent
. В более общих реальных сценариях используемый компонент можно настроить, например, вызвав сервисный метод, приняв тип Компонента и уведомив другой Компонент, который добавит Компонент этого типа в placeholder.
Программная настройка вывода¶
В предыдущем примере для присвоения значения входу title
компонента ToastComponent
была вызвана функция setInput
. Здесь я хочу обсудить, как определить обработчики событий для таких динамически добавляемых компонентов.
Предположим, что ToastComponent
показывает ссылку подтверждения:
При нажатии на эту ссылку он выдает событие confirmed
:
1 2 3 4 5 6 7 8 9 |
|
Чтобы создать обработчик этого события, мы можем напрямую использовать свойство instance
возвращаемого ComponentRef
. Оно указывает на добавленный экземпляр компонента и, следовательно, предоставляет доступ ко всем его свойствам:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Метод OutputEmitterRef
subscribe
позволяет определить обработчик события. В нашем случае он просто удаляет тост с помощью destroy
и записывает полученную строку в консоль.
Однако в этом примере есть небольшая ошибка. Независимо от того, нажмет пользователь на ссылку подтверждения или нет, пример вызывает destroy
через 5 секунд. Следовательно, тост может быть удален дважды: один раз после подтверждения, а другой раз после отображения в течение 5 секунд.
К счастью, уничтожение компонента дважды не приводит к ошибке. Чтобы решить эту проблему, мы можем ввести флаг destroyed
. В следующем разделе показан более мощный подход: Потребление выходов как Observables
.
Потребление выходов в качестве Observables
¶
Несмотря на то, что OutputEmitterRef
предоставляет метод subscribe
, он не является Observable
. Однако оригинальный EventEmitter
, используемый вместе с декоратором @Output
, был таковым. Чтобы вернуть все возможности, связанные с выводами на основе Observable
, вы можете использовать функцию outputToObservable
, которая является частью RxJS interop layer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Функция outputToObservable
преобразует OutputEmitterRef
в Observable
. В приведенном примере она используется для выражения события подтверждения и 5 секундного тайм-аута как наблюдаемых. Оператор race
гарантирует, что будет использован только тот Observable
, который первым выдал значение.
Observable
, возвращаемый оператором outputToObservable
, завершается, когда Angular уничтожает компонент вывода. По этой причине нет необходимости отписываться вручную.
Паритет между запросами к содержимому и представлению¶
До сих пор мы работали с contentChildren
для запроса нескольких проектируемых дочерних элементов и viewChild
для получения одного узла в представлении. Однако у обеих концепций есть паритет возможностей: Например, существует функция contentChild
и viewChildren
.
Кроме того, все опции, которые мы использовали выше для запросов к представлению или содержимому, такие как использование дескрипторов в качестве локаторов или использование свойства read
, работают для обоих типов запросов.
Заключение¶
Несколько новых функций заменяют декораторы свойств и помогают настроить концепции связывания данных. Эти функции подхватываются компилятором Angular и выдают соответствующий код.
Функция input
определяет входы для привязки свойств, model
определяет входы для двухсторонней привязки данных, а contentChild(ren)
и viewChild(ren)
заботятся о запросах к содержимому и виду. Использование этих функций приводит к появлению сигналов, которые могут быть спроецированы с помощью computed
и использованы в эффектах.