Как Мы Делаем Основные Компоненты Taiga Ui Более Гибкими: Концепция Контроллеров Компонентов В Angular

В процессе эволюции наша библиотека компонентов пользовательского интерфейса Taiga мы начали замечать, что некоторые более сложные компоненты имеют @Input просто для того, чтобы передать его значение в @Input другого нашего базового компонента внутри себя.

Иногда такое гнездование происходит даже в три слоя.

Мы справились с помощью хитрых директив, которые мы назвали контроллерами.

Они полностью решили проблему вложенности и уменьшили вес библиотеки.

В этой статье я покажу, как мы организовали общую систему настройки всех полей ввода, используя эту концепцию и возможности DI Angular.

Как мы делаем основные компоненты Taiga UI более гибкими: концепция контроллеров компонентов в Angular



Текстовое поле в бывшей «Тайге»: удачный случай, когда можно использовать контроллеры

У нас есть базовый компонент ввода под названием Primitive Textfield.

Как мы делаем основные компоненты Taiga UI более гибкими: концепция контроллеров компонентов в Angular

Этот компонент представляет собой нативный инпут, стилизованный под нашу тему, с оберткой для него.

Он не работает с формами Angular и нужен для построения полноценных элементов управления.

Самая первая версия текстового поля была довольно простой и использовалась в нескольких компонентах составного ввода.

Но вскоре все стало усложняться: добавлялись новые возможности, а @Inputs компонента становилось все больше и больше.



Как мы делаем основные компоненты Taiga UI более гибкими: концепция контроллеров компонентов в Angular

К моменту, когда мы начали готовить версию Taiga с открытым исходным кодом, на основе Textfield было создано 17 компонентов ввода.

Отсюда начали развиваться две фундаментальные проблемы:

  • Компоненты имеют некоторые @Inputs исключительно для того, чтобы передать их дальше в текстовое поле, без какого-либо преобразования.

    Получается, что при добавлении нового такого поля в текстовое поле нам нужно по-хорошему расширить с его помощью все 17 компонентов.

  • Некоторые @Inputs используются довольно редко, но они должны быть у всех компонентов.

    Это начинает раздувать бандл: мы добавляем один @Inputs в текстовое поле и один такой же в компоненты.

    В 10 проектах все поля ввода имеют свойство, которое теперь нужно только одному из них.

    Не хорошо.

Что ж, попробуем сделать по-другому.



Вариант одноуровневой директивы

Среди текстовых полей @Input было три для всплывающей подсказки, с ними мы и разберемся в первую очередь.

Что и как мы хотим показать, переносится в текстовое поле, а оно внутренне реализует логику отображения подсказки: по наведению или по фокусу поля ввода с клавиатуры (доступность для пользователей без мыши).



Как мы делаем основные компоненты Taiga UI более гибкими: концепция контроллеров компонентов в Angular

Эти три @Inputs просто перекидываются в текстовое поле из других компонентов и используются не очень часто.

К тому же подобные подсказки встречаются не только в полях ввода.

Давайте сделаем отдельную директиву контроллера для настройки всплывающих подсказок в нашей библиотеке:

  
  
  
  
  
  
   

@Directive({ selector: '[tuiHintContent]' }) export class TuiHintControllerDirective { @Input('tuiHintContent') content: PolymorpheusContent = ’’; @Input('tuiHintDirection') direction: TuiDirection = 'bottom-left'; @Input('tuiHintMode') mode: TuiHintMode | null = null; }

Это минимум для нашего контроллера — три @Input с необходимой нам информацией.

Селектор директив содержит только «tuiHintContent», поскольку если содержимого нет, нам нет смысла выравнивать или перекрашивать всплывающую подсказку.

Эта директива уже может быть прикреплена к текстовому полю или любому из его родительских элементов.

Затем мы внедряем объект директивы с помощью DI и получаем из него данные в наше текстовое поле.

Но есть еще пара моментов, которые хотелось бы принять во внимание.

Теперь при изменении директивы @Input изменения в текстовом поле не будут проверяться стратегией OnPush, что не соответствует поведению Angular @Inputs. Для этого добавим поток, который будет запускаться при изменении @Input контроллера.

Для удобства я предлагаю перенести такой поток в абстрактный класс Controller, от которого будут наследоваться все контроллеры:

export abstract class Controller implements OnChanges { readonly change$ = new Subject<void>(); ngOnChanges() { this.change$.

next(); } }

Когда входные данные директивы изменятся, будет вызван ngOnChanges, который уведомит поток.

Наследуем директиву от абстрактного класса:

@Directive({ selector: '[tuiHintContent]' }) export class TuiHintControllerDirective extends Controller { // .

}

Теперь давайте разберемся, как мы будем обрабатывать изменение $ в компоненте.

Самый простой вариант — внедрить эту директиву и ChangeDetectorRef в текстовое поле, вызывая markForCheck при каждой проблеме изменения$.

Этот вариант подходит, если контроллер будет использоваться только с одним компонентом:

constructor( @Inject(ChangeDetectorRef) private readonly changeDetectorRef: ChangeDetectorRef, @Optional() @Inject(TuiHintControllerDirective) readonly hintController: TuiHintControllerDirective | null, ) { if (!hintController) { return; } hintController.change$.

pipe(takeUntil(this.destroy$)).

subscribe(() => { changeDetectorRef.markForCheck(); }); }

Вот как его можно использовать.

Вариант не окончательный — позже мы его причесаем и абстрагируем.

Чтобы текстовое поле имело подсказку, вам просто нужно прикрепить директиву «tuiHintContent» к самому компоненту текстового поля или к любому из его родительских элементов.

На этом этапе мы хорошо очищаем вес нашего бандла: все компоненты-обёртки больше не имеют @Inputs для пересылки.

И каждая созданная сущность не будет содержать ненужных для нее полей.

Недостаток такой схемы проявляется, если контроллер можно использовать для разных компонентов: нам нужно запомнить этот поток и дублировать такой код в каждом компоненте, использующем контроллер.



Абстрагирование проверки изменений в поставщиках

Чтобы компонент мог сразу получить сущность директивы и использовать ее без подписки на изменения или проверки на null, воспользуемся возможностями DI-провайдеров в Angular:

constructor( @Inject(TUI_HINT_WATCHED_CONTROLLER) readonly hintController: TuiHintControllerDirective, ) {}

Это тот тип работы, который мы хотим получить в текстовом поле.

Добавим токен TUI_HINT_WATCHED_CONTROLLER и провайдера для него:

export const TUI_HINT_WATCHED_CONTROLLER = new InjectionToken('watched hint controller'); export const HINT_CONTROLLER_PROVIDER: Provider = [ TuiDestroyService, { provide: TUI_HINT_WATCHED_CONTROLLER, deps: [[new Optional(), TuiHintControllerDirective], ChangeDetectorRef, TuiDestroyService], useFactory: hintWatchedControllerFactory, }, ]; export function hintWatchedControllerFactory( controller: TuiHintControllerDirective | null, changeDetectorRef: ChangeDetectorRef, destroy$: Observable<void>, ): Controller { if (!controller) { return new TuiHintControllerDirective(); } controller.change$.

pipe(takeUntil(destroy$)).

subscribe(() => { changeDetectorRef.markForCheck(); }); return controller; }

Итак мы создали токен, при внедрении которого будет происходить подписка на изменения в директиве через HINT_CONTROLLER_PROVIDER. Мы добавим этого провайдера в «поставщики» компонента текстового поля, чтобы получать текущие значения ChangeDetectorRef и ТуиДестройСервис.

Мы тут же предоставим этот сервис, он просто подхватывает хук ngOnDestroy компонента-инжектора и после этого идет дальше и завершает сабж, который сам и есть (если не понимаете, то лучше посмотреть реализацию по ссылке) .

Остается только добавить провайдера и внедрить токен:

@Component({ //.

providers: [HINT_CONTROLLER_PROVIDER], }) export class TuiPrimitiveTextfieldComponent { constructor( //.

@Inject(TUI_HINT_WATCHED_CONTROLLER) readonly hintController: TuiHintControllerDirective, ) {} }

Теперь мы можем прикрепить директиву подсказки к самому текстовому полю или к любому компоненту или элементу, внутри которого текстовое поле скрыто.

При изменении директивы @Input изменения будут проверяться для текстового поля благодаря безопасной подписке на фабрике.

Работать с таким контроллером внутри текстового поля крайне приятно: мы просто внедряем готовую сущность из DI и можем сразу использовать ее в шаблоне или геттерах, не беспокоясь о проверке изменений.

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

Мы в библиотеке сделали это после появления второго контроллера, но пока можем оставить так.



Что дальше?

Мы разобрали лишь один очень простой случай создания контроллера.

В текстовом поле @Inputs, которое я описал, также есть ряд настроек, которые мы перенесли в более сложный контроллер.

Он многоуровневый и работает при любой вложенности: вы можете назначить один параметр самому текстовому полю, один — компоненту, который его обертывает, а третий — определить для всех полей ввода в форме.

Более того, любую из настроек можно переопределить по умолчанию на любом уровне вложенности.

И все это делалось только на чистом Angular DI, пусть и с использованием его по максимуму.

Я готов написать об этом статью-продолжение, но сначала хочу узнать у вас, нужно ли это.

Если вам будет интересно прочитать статью о более умных трюках, дайте мне знать.



Общий

С помощью небольшого количества кода и трюков с Angular DI мы смогли убрать ненужное повторное использование кода, сделать сущности приложения легче, а также облегчить связку как библиотеки, так и приложений, которые ее используют. Решение не самое простое для понимания и может быть избыточным для небольших приложений и библиотек, но в нашем случае оно очень помогло выгрузить большую часть пакета, спрятав ее в небольшом наборе точной и аккуратной работы с DI, спрятанной за лаконичный API. Теги: #Разработка веб-сайтов #JavaScript #typescript #angular #внедрение зависимостей #инверсия управления #библиотека компонентов
Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.