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

Предисловие

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

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

(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), а я не большой фанат так делать. Вот как бы выглядела директива:

266d93ff5122a2b9a684bdcd737154e8.png

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

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

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

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

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

View компонента

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

Декораторы

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

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

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

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

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

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

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

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

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

Проба пера:

14bef07ed34033bfdcda38b6b16af175.png

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

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

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

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

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

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

View-model:

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

View:

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

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

b94b439209155ce7642a608cc11e24e8.png

View-model:

4cc016562d8d31cfdc4f8eb8fab0664a.png2722c4a773584cd40fd44a4877fb2532.png

Заключение

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

© Habrahabr.ru