В процессе эволюции наша библиотека компонентов пользовательского интерфейса Taiga мы начали замечать, что некоторые более сложные компоненты имеют @Input просто для того, чтобы передать его значение в @Input другого нашего базового компонента внутри себя.
Иногда такое гнездование происходит даже в три слоя.
Мы справились с помощью хитрых директив, которые мы назвали контроллерами.
Они полностью решили проблему вложенности и уменьшили вес библиотеки.
В этой статье я покажу, как мы организовали общую систему настройки всех полей ввода, используя эту концепцию и возможности DI Angular.
Текстовое поле в бывшей «Тайге»: удачный случай, когда можно использовать контроллеры
У нас есть базовый компонент ввода под названием Primitive Textfield.Этот компонент представляет собой нативный инпут, стилизованный под нашу тему, с оберткой для него.
Он не работает с формами Angular и нужен для построения полноценных элементов управления.
Самая первая версия текстового поля была довольно простой и использовалась в нескольких компонентах составного ввода.
Но вскоре все стало усложняться: добавлялись новые возможности, а @Inputs компонента становилось все больше и больше.
К моменту, когда мы начали готовить версию Taiga с открытым исходным кодом, на основе Textfield было создано 17 компонентов ввода.
Отсюда начали развиваться две фундаментальные проблемы:
- Компоненты имеют некоторые @Inputs исключительно для того, чтобы передать их дальше в текстовое поле, без какого-либо преобразования.
Получается, что при добавлении нового такого поля в текстовое поле нам нужно по-хорошему расширить с его помощью все 17 компонентов.
- Некоторые @Inputs используются довольно редко, но они должны быть у всех компонентов.
Это начинает раздувать бандл: мы добавляем один @Inputs в текстовое поле и один такой же в компоненты.
В 10 проектах все поля ввода имеют свойство, которое теперь нужно только одному из них.
Не хорошо.
Вариант одноуровневой директивы
Среди текстовых полей @Input было три для всплывающей подсказки, с ними мы и разберемся в первую очередь.Что и как мы хотим показать, переносится в текстовое поле, а оно внутренне реализует логику отображения подсказки: по наведению или по фокусу поля ввода с клавиатуры (доступность для пользователей без мыши).
Эти три @Inputs просто перекидываются в текстовое поле из других компонентов и используются не очень часто.
К тому же подобные подсказки встречаются не только в полях ввода.
Давайте сделаем отдельную директиву контроллера для настройки всплывающих подсказок в нашей библиотеке:
Это минимум для нашего контроллера — три @Input с необходимой нам информацией.@Directive({ selector: '[tuiHintContent]' }) export class TuiHintControllerDirective { @Input('tuiHintContent') content: PolymorpheusContent = ’’; @Input('tuiHintDirection') direction: TuiDirection = 'bottom-left'; @Input('tuiHintMode') mode: TuiHintMode | null = null; }
Селектор директив содержит только «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 #внедрение зависимостей #инверсия управления #библиотека компонентов-
Войдите В Мир Контекстной Рекламы
19 Oct, 24 -
Одежда Для Снаряжения – История
19 Oct, 24 -
Что Такое Cdn И Как Он Вообще Работает?
19 Oct, 24 -
Лазерная Коррекция Зрения
19 Oct, 24 -
Концентрированное Внимание
19 Oct, 24 -
В Томске Создают Команду Роботов-Футболистов
19 Oct, 24