AngularInDepth уходит от Medium. Более свежие статьи размещаются на новой платформе inDepth.dev. Спасибо за то, что участвуете в глубоком движении!

Связывание данных в Angular - это круто. Действительно. Просто украсьте общедоступное свойство - или сеттер, если на то пошло - в классе компонента с помощью @Input и после быстрого <app-child [prop]="parentProp"></app-child>, вуаля! Свойство родительского компонента привязано и готово к использованию по желанию. Генерировать события так же просто: определите EventEmitter, украсьте его @Output и испустите его во время внутреннего выполнения. Так просто, так красиво ... Реализовать двустороннюю привязку, конечно, немного сложнее. Тем не менее, его использование просто фантастическое: [(ngModel)]="parentProp".

Однако со временем я обнаружил некоторые трудности, связанные с этим потоком. Прежде всего, <router-outlet> не предлагает средств для привязки данных к загруженным компонентам или отправки событий от них родителям. Во-вторых, заполнение пары промежуточных компонентов несколькими входными свойствами и генераторами событий только для того, чтобы передавать через них данные, не только беспорядочно, но и требует много времени. Ваши компоненты раздуваются. Кроме того, вы в конечном итоге напишете множество бесполезных тестов. Наконец, изменение общедоступного API этих глубоко вложенных дочерних компонентов, как правило, становится болезненной практикой. Все родители должны быть тщательно проверены на наличие следов измененных / удаленных свойств, в противном случае весьма вероятны ошибки.

Существуют различные решения некоторых или всех этих проблем с помощью уже имеющихся инструментов. Экземпляр службы, внедренный как в родительский, так и в дочерний, вероятно, является самым прямым из них. Однако он также является одним из самых тесно связанных. Другим решением может быть проекция контента, но это не всегда практично. Представьте себе многослойное сложное дерево компонентов, и вы поймете, что я здесь имею в виду. Внедрение зависимостей предоставляет третье решение, поскольку родительский компонент может быть захвачен путем навигации по дереву DI. Тем не менее, ребенок должен знать тип или интерфейс родителя, чтобы найти его, а это не всегда возможно. О да, чтобы этого избежать, вы можете предоставить токен инъекции от родителя, и он будет работать. Если, конечно, вы не используете ChangeDetectionStrategy.OnPush для дочернего элемента, что мы все должны делать, и динамически изменять передаваемое значение. У управления состоянием есть свое предостережение: поскольку подключение презентационных (немых) компонентов напрямую к определенному состоянию нарушает их возможность повторного использования, вместо этого они должны быть обернуты контейнерными (интеллектуальными) компонентами, а это означает дополнительную работу и обслуживание.

Войти в угловой контекст

Во-первых, отказ от ответственности: я являюсь автором статьи Angular Context (или ngx-context), которая будет основной темой здесь. Он разработан с учетом проблемных вопросов, описанных выше, и направлен на решение большинства, если не всех, из них. Библиотека, как видно из названия, действительно основана на контексте React. Тем не менее, реализация полностью от него не зависит и фактически адаптирована для Angular.

Что ж, я начал искать глобально применимый метод для передачи свойств и событий через компоненты между родителями верхнего уровня и глубоко вложенными дочерними элементами. Моей отправной точкой, что неудивительно, стала потрясающая система внедрения зависимостей, встроенная в Angular. Это правда, что было несколько трудностей, например, как захватить неизвестного родителя или как сначала синхронизировать разные типы данных, но я считаю, что конечный результат приличный и достаточно зрелый, чтобы поделиться с другими разработчиками Angular. Итак, давайте кратко рассмотрим, что Angular Context может сделать для вас и как.

Установка и включение углового контекста в ваш проект

Библиотека размещена в npm, поэтому ее можно установить, запустив приведенный ниже код в своем терминале:

npm install --save ngx-context

После установки все, что вам нужно сделать, это импортировать NgxContextModule в корневой модуль следующим образом (несвязанные метаданные скрыты для простоты):

import { NgxContextModule } from 'ngx-context';

@NgModule({
  imports: [ NgxContextModule ]
})
export class AppModule {}

Односторонняя привязка данных с угловым контекстом

Предположим, что нам нужно передать данные из AppComponent в индикатор выполнения из NgxBootstrap через воображаемый компонент с именем OneWayComponent. То, что вы делаете с Angular Context, довольно просто: поместите поставщик контекста на AppComponent и потребитель контекста на индикатор выполнения.

Как видите, мы поместили ContextProviderComponent внутрь исходного компонента, которым в данном случае является AppComponent, и обернули его вокруг любого компонента, который прямо или косвенно может быть заинтересован в предоставленных данных, т.е. OneWayComponent здесь. Затем мы поместили ContextConsumerDirective на индикатор выполнения, и после простого сопоставления результат выглядит следующим образом:

На OneWayComponent пока нет свойств, но мы смогли правильно отобразить индикатор выполнения. Все, что нам нужно было сделать, это дать базовую пару «ключ-значение», определяющую, какое имя свойства представляет то, что в контексте атрибута contextMap. Точно так же любое сопоставление в компоненте поставщика влияло на то, как мы потребляли данные, т. Е. ссылаясь на сопоставленное имя вместо оригинала. Теперь давайте немного усложним задачу и попробуем добавить процент на шкале. Здесь задействована проекция контента, поэтому нам нужно будет объявить для этого свойство.

Здесь мы поместили ContextConsumerComponent внутрь OneWayComponent и смогли использовать единственное предоставленное свойство progress. Нам не нужно было украшать наше новое свойство, потому что его значение исходит от потребительского компонента, а не от родительского. Вот как выглядит результат:

Это было несложно, но как насчет того, чтобы избавиться от этого ненужного свойства? Мы можем сделать это с помощью ContextDisposerDirective, который в основном является структурной директивой, которую можно разместить на <ng-template> и использовать для использования предоставленных данных в качестве входных переменных шаблона.

И снова мы можем удалить свойство и ContextConsumerComponent из OneWayComponent. Результат будет таким же, как и раньше, а средний компонент останется чистым.

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

AppComponent настроен на 0 в качестве начального прогресса и теперь имеет интервал, увеличивающий значение прогресса на 10 каждую секунду. Вы можете увидеть результат ниже:

Двусторонняя привязка данных с угловым контекстом

Односторонняя привязка хоть и полезна, но составляет лишь половину картины. Мы создаем интерактивные приложения, и нам нужно как-то фиксировать ввод пользователя. В Angular рекомендуется использовать реактивные формы, но мы еще вернемся к этому. А пока давайте попробуем что-нибудь еще с компонентом рейтинга из NgxBootstrap, чтобы увидеть худший случай.

В этом сценарии, как и в первом примере, данные привязаны к дочернему элементу с помощью ContextConsumerDirective. Сложная часть состоит в том, что для того, чтобы эта работа работала, нужно знать и имитировать внутреннюю структуру рейтингового компонента. Откровенно говоря, это было бы ужасно, не потому, что это больше трудозатрат, а, скорее, из-за того, что любое будущее изменение рейтингового компонента может нарушить привязки. Поэтому, несмотря на доступность, избегайте этого всеми способами.

Однако присмотритесь к onRating. Вместо метода вызывается публичное свойство. Причина в лексическом объеме. Я не собираюсь объяснять закрытие в этом посте. Однако стоит упомянуть, что обработчики событий на родительских объектах должны быть стрелочными функциями, а не обычными функциями или методами. В противном случае this не будет представлять экземпляр родительского компонента и будет сбивать с толку.

Мы можем лучше. Вместо директивы потребителя можно использовать ContextConsumerComponent для получения контекста и использования его в шаблоне.

Вы, должно быть, заметили: мы не можем выполнить двустороннюю привязку напрямую с помощью директивы ngModel. Вместо этого мы установили с ним одностороннюю привязку и вызываем предоставленную стрелочную функцию всякий раз, когда изменяется значение модели. Это решение, несмотря на то, что дает тот же результат и менее сложное, чем предыдущее, все же не то, что мы хотели бы получить. Затем мы удалим лишние свойства из TwoWayComponent.

Еще раз, промежуточный компонент свободен от свойств и методов, которые могут быть привязаны только к дочернему компоненту. Мы приближаемся к этому, но есть еще одна вещь, которую нужно сделать: Реализация реактивной формы.

Никаких дополнительных свойств, никаких обратных вызовов событий, никакого ControlValueAccessor… Это настолько чисто, насколько возможно. Однако директива NgIf необходима, потому что значение control обязательно для правильной работы FormControlDirective. Имейте в виду, что в настоящее время библиотека не может гарантировать порядок свойств, подлежащих синхронизации или удалению, и начальное значение может быть undefined.

Вы можете найти демонстрационное приложение ниже.



Заключение

Angular Context - не серебряная пуля, и я не предлагаю широкого внедрения. Однако он предлагает альтернативный подход к общей проблеме и иногда может оказаться полезным. Он довольно новый и требует доработки. Тем не менее, он также хорошо протестирован и готов к производству. Попробуйте и дайте нам знать, как мы можем это улучшить. Любые отзывы будут приветствоваться.

Спасибо 👋

Приходите, поздоровайтесь с нами в Twitter. Мы группа любителей Angular из Турции, организуем встречи, публикуем статьи и вносим свой вклад в проекты с открытым исходным кодом вокруг Angular. - NG Турция