Как стать автором
Обновить

Кастомные декораторы в Angular приложениях

Время на прочтение5 мин
Количество просмотров4.2K

Предисловие

Я занимаюсь разработкой web-приложений на Angular уже более 6-ти лет и в силу своего не малого опыта я постепенно отошел от кодинга типичных фичей, таких как сверстать формочку и отправить данные на бэк, к более глобальным и важным аспектам разработки web-приложения - архитектуре. Довольно часто мне приходиться делать как обычный code-review на этапе разработки приложений, так и меня могут позвать оценить качество уже написанного приложения. И вот недавно я занимался просмотром кода очередного продукта и заметил то, чем хотел бы поделиться с Вами, коллеги.

Функционал во всем приложении следующий - на каждой странице, которая отвечает за отображение списков неких сущностей (абстрагируемся) почти всегда есть форма, отвечающая за вывод детальной информации о выбранной сущности из списка. На данной форме есть типичные кнопки с вариантами действий: "Отменить" и "Сохранить" внесенные изменения. Логика проста, не внесли данные в форму - кнопка "Сохранить" не активна. Внесли данные - кнопка "Сохранить" активна. Разработчики, естественно, использовали атрибут disabled и как наверняка вы догадались интерполировали его и записывали в него результат выполнения ряда логических условий. И все бы ничего, но иногда это выглядело следующим образом:

<p-button
(click)="createOrUpdate()"
[disabled]="someControlVeryLongName.invalid || !someControlVeryLongName.dirty || (loading$ | async) === true || (subloading$ | async) === true"

На первый взгляд мы видим только одну проблему - ооооооочень большое выражение, для такой малой "персоны", как disabled. Но проблема не заканчивается, а только начинается в данном компоненте и везде, где есть похожая логика. Дело в том, что атрибут disabled пропускает событие click в любом случае... Это означает, что данную проверку мы вынуждены тащить в компонент, а именно в данном случае в метод createOrUpdate. Вторая проблема (неудобство) - это потоки: loading$ и subloading$. В методе мы должны их скомбинировать со значениями dirty и invalid контрола и если все все хорошо, позволить отправку данных на обновление или создание некой сущности. Третья проблема, вся логика дублируется по всем компонентам в приложении - DRY.

Пути решения

Конечно можно было бы не предавать этой проблеме столь важное значение (работает - не трогай), но у меня появился "спортивный интерес" к тому, как ее можно "красиво" решить.

Первое, что приходит в голову это перенести логику на другой механизм, которыми Angular в прямом смысле кишит. Сперва я подумал о директиве. Что если создать атрибут-директиву и просто вешать ее на кнопки, где нужно отслеживать состояние disabled от нужных мне условий? Так как проект использует state-management NGXS - достать потоки из него внутри директивы не проблема. Но что делать для состояния invalid/dirty , на которые директива должна обращать анимание? Причем на каких-то страницах это FormGroup, а на других FormControl. Зная иерархию контролов (все формы в приложении написаны с использованием реактивных форм RF) я понимаю, что данные флаги есть у AbstractControl, а он является базовым для FormGroup и FormControl. Мне остается просто прокинуть в директиву нужный контрол, а input-параметр сделать типа AbstractControl. Казалось бы проблема решена, но... Если вы обратите внимание на контрол кнопки - это элемент из UI-Framework PrimeNG. У него нет API, которое мне поможет повлиять на параметр disabled. Даже если и было, то мне пришлось бы использовать dirty-checking (принудительно вызывать detect changes), а я не большой фанат так делать. Вот как бы выглядела директива:

Есть ли другие способы? Я уверен, что есть, но на сколько они эффективны и объемны не знаю. И тут я подумал о декораторах...

Великий и могучий Angular

В реальных проектах, написанных на Angular, как правило, не требуется написание собственных декораторов, а если требуется, то скорее всего вы создаете свою библиотеку, для упрощения "жизни" коллегам программистам. Angular имеет множество build-in декораторов на все случаи жизни (Input, Output, Component, Directive, Optional, Inject и многие другие) и их действительно хватает, и их написание - нонсенс. Но в данной ситуации я не увидел действительно хорошего решения из базовых или типичных. Согласитесь, было бы удобно иметь инструмент под рукой, похожий на этот:

View-model компонента:

@Disabled(...)
public disabled$!: Observable<boolean>;

View компонента

<p-button [disabled]="!!(disabled$ | async)" (click)="createOrUpdate()">

Как по мне выглядит круто.

Декораторы

Если вы уже имели дело с TypeScript - вы знаете, что декораторы - это не фича Angular, а фича языка TypeScript. Она позволяет изменять сущность, к которой применяется, на специфическом уровне, который называется Reflection. Этот уровень предназначен для работы с типами и их параметрами (дескрипторами). Важно помнить, что декоратор это обычная функция, которая вызывается(применятется) к сущности. Всего типов декораторов 4:

  • Декоратор класса;

  • Декоратор свойства класса;

  • Декоратор метода класса;

  • Декоратор параметра метода (функции)

К каждой сущности можно применить более одного декоратора. В данной статье мы на реальном примере рассмотрим применение декоратора второго типа (декоратор свойства).

Написание декоратора

Любое написание декоратора сводится к одному - описанию функции, которая должна применить к целевой сущности "декорирование". То есть добавить нужные поля/методы по вашему правилу, а сущность этими полезностями будет пользоваться.

Сперва вспомним, что для каждого компонента, который нуждается в функционале по определению disabled для кнопки, всегда есть AbstractControl, а также потоки, от которых зависит состояние disabled.

Проба пера:

На первый взгляд некоторым может показаться, что это "магия в не Хогварса" и я хотел бы немного остановиться более детально и пояснить, что магии или колдовства здесь нет. Когда мы декорируем нашим декоратором свойство/метод/атрибут/класс - наш декоратор вызывается! (Как и любые другие декораторы) И в этот момент мы можем передавать какую-либо настройку для его дальнейшей работы, как в обычную функцию. В данном случае я просто хочу передать имя контрола в компоненте, от которого и зависит состояние disabled моей кнопки. Функция Object.defineProperty просто помогает мне объявить в компоненте свойство с таким же именем, к которому применяется мой декоратор. В нашем случае так:

  • target - компонент, в котором будет объявлено свойство;

  • key - имя нового свойства;

  • descriptor - дескриптор. Именно он описывает как себя будет вести и как будет доступно из вне наше новое свойство.

Третий параметр функции (дескриптор) параметрами enumerable и configurable говорит, что свойство доступно из вне для перебора (например для цикла for...in) и может конфигурироваться, так как мы можем применять и другие декораторы к текущему новому свойству. И конечно же метод get. То есть что делать и что возвращать, когда кто-то старается наше свойство прочитать. Именно здесь мы и встраиваем нашу логику по получению итогового состояния нашей кнопки.

Не просто так здесь использовалась именно анонимная функция, а не стрелочная, чтобы не потерять контекст. Контекстом и будет в итоге наш с вами компонент и именно через this мы получаем все данные, которые нам нужны для определения disabled.

View-model:

@Disabled('agentNameControl')
public disabled$!: Observable<boolean>;

View:

<p-button [disabled]="!!(disabled$ | async)" (click)="createOrUpdate()">

Улучшения декоратора

Конечно мы можем ошибиться в указании имени контрола, а также проверка потоков может быть избыточна или на оборот, их количество может увеличится. Для ограничения передаваемого ключа в качестве первого параметра декоратора я буду использовать оператор keyof, а потоки вынесу в дополнительный параметр декоратора и немного дополню саму логику определения состояния disabled.

View-model:

Заключение

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

Теги:
Хабы:
Всего голосов 11: ↑11 и ↓0+11
Комментарии27

Публикации

Истории

Работа

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн