[QB Dev Blog] Новые фичи
Eugen TatuevВлил обновление с парой новых фич, подробности далее.
Store.clear()
Реализовал возможность очистки наших стейтов по необходимости. Метод доступен для любого нашего Store, и записывает в ассоциированный с ним стейт дефолтное значение. Удобно применять на ngOnDestroy() в главных компонентах модуля, которые оборачивают всю логику работы с данным стором.
На данный момент применено целиком на профиль, квоты, ордера, пропозалы, контракты, админку, и на несколько вложенных там.
Пример 1:
Quote - имеем родительскую компоненту QuoteComponent, внутри которой происходит весь роутинг квоты (центр и билд). Внутри чистим QuoteStore, который запишет в sub-state "quote" наши дефолты.
Однако, мы имеем еще QuoteCenterComponent и QuoteBuildComponent. Несмотря на то, что они используют общий QuoteStore - имеет смысл организовать чистку в обоих, т.к. при переходе из центра в, к примеру, редактирование квоты нам совсем не обязательно держать в памяти (и в sessionStorage соответственно) выгруженные квоты для qb-table.
Пример 2:
Админка - имеет компонент AdminComponent,в которой зачищает AdminStore целиком. Однако внутри много дочерних и достаточно крупных модулей, начинаем "детализировать" нашу отчистку.
ItemManager - имеем родительскую компоненту ItemManagerComponent. Чистим там ItemsManagerChildStore, так что по уходу в другой модуль админки данные больше не висят грузом на шее RAM клиентской машины. Он в свою очередь имеет вложенные ItemCenter, QuickAdd. По логике можно им обоим реализовать очистку. Однако, т.к. эти подкомпоненты не слишком крупные, можно не делать этого и воспользоваться преимуществом общего стейта - загруженные IProductLookupDTO для центра и таблицы использовать в тех же целях уже на QuickAdd.
Важно!
Нужно обратить внимание, что применение очистки теперь явно заставит НЕ использовать соседние сторы для данных (к примеру, продукты из product-manager использовались повсеместно в админке для дропдаунов). Вполне нормально, что компоненты сами себя будут обеспечивать данными, все равно их стоит делать более "точечными", используя подходящие к месту DTO. Однако, т.к. для тех же дропдаунов данные нужно почти всегда одинаковые, можно использовать общие модели и методы по "добыче" этих данных. На примере продуктов - ProductLookupDTO и "api/product/get-products-lookup". Однако хранение и очистку на выходе должен брать на себя каждый модуль сам, чтобы эффективнее использовать память в процессе работы приложения.
Важно!
Для всех транспортных моделек по обе стороны баррикад нужно использовать постфикс DTO. Если его нету, то это либо Entity, либо какой-то класс со своим функционалом, а не просто интерфейс. Почему важно - помимо консистенции и т.д. на фронте, к примеру, у нас идет сохранение стейта в sessionStorage, что подразумевает сериализацию. И если модель не просто модель, а имеет свои не статические методы и прочая, то нужно явно управлять десериализацией и создавать instance конкретных типов, как это сделано в BillOfMaterial для PricingItem-ов посредством реализованных хуков в Store
В противном случае получим чистый анонимный объект без методов и "красивую" консоль при попытке state.obj.doSomeCoolStuffAndGetMyCats() :)
Грамотное же именование поможет не упустить возможные сайд-эффекты.
ComponentBase
Некоторое время назад Стас подал хорошую идею по использованию базового класса для компонент, который сможет в общем виде выполнять некоторые рутинные моменты без необходимости копипасты. ComponentBase призван быть базовым для всех наших компонент. При необходимости в будущем отделить "страницы" там от контролов - там и отделим :)
Методы:
trackSubscription(subscription: Subscription): void
Передаем туда все наши подписки, и они автоматические будут отписаны по ngOnDestroy()
markForChangeDetectionCheck(): void
Внутри вызывает ChangeDetectorRef.markForCheck(). Используется в тех редких случаях, когда в OnPush компонентах надо запустить change detection изнутри.
Пример 1:
Нужно передать в базовый класс экземпляр Injector сервиса посредством вызова super() в конструкторе.
Ну и пользуемся его услугами :)
Больше ничего в случе подписок делать не нужно.
Важно!
Если в компоненте вам понадобиться воспользоваться ngOnDestroy() по другим причинам, то необходимо вызвать родительский метод вручную, иначе болтаться нашим подпискам во веки вечные :)
Пример 2:
На данный момент известный многим ComponentWithSaveDialog уже унаследован от ComponentBase и переделан под Injector, а значит все наследники получили этот функционал. Определенный там метод clear() переименован в ngOnDestroy().
Т.е. любой наследник от ComponentWithSaveDialog теперь не обязан наследоваться сам от OnDestroy() и ничего вызывать, бывший clear() отработает автоматически. Однако, если он все таки нужен - не забываем про super.ngOnDestroy(), который дернет этот самый бывший clear(), который, в свою очередь, вызовет "деструктор" от ComponentBase.
Важно!
Зачем в ComponentBase передается Injector? А затем, чтобы базовый класс мог получить все необходимые ему зависимости, без нужны явно инжектить в нашу компоненту 101 сервис, и все скопом передавать в super().
При этом, т.к. Injector для нашей компоненты мы получаем через конструктор используя механизмы Angular-овского DI (а не сохраняя его статически, как кто-то сделал в классе ServiceLocator, сохраняя туда инжектор от AppComponent), то инжектор мы получаем для своего контекста, годный такой, правильный. А значит базовый класс через него получит всё то же самое, что получила бы сама компонента. Ведь если если модуль компоненты "переинжектил заново" сервис, изначально определенный в AppModule, то выше упомянутый экземпляр инжектора в ServiceLocator будет получать не его, а версию для AppComponent
FormComponentBase (extends ComponentBase)
Еще один базовый класс, призванный облегчить работу с компонентами-формами.
Обязательные к наследованию:
abstract isValid(): boolean
Используется родительскими компонентами, чтобы опросить детей на предмет валидности (и запустить "отображение" валидации)
Свойства:
protected formBuilder: FormBuilder
Убирает необходимость в ручном инжекте билдера.
Методы:
protected validate(...forms: FormGroup[]): boolean
Передаем формы для валидации, и метод рекурсивно обходит формы любой сложности и вложенности, формы-массивы и т.д. Возвращает булевый флажок валидности, и запускает "отображение" валидации, помечая формы как touched
Пример:
Пример рефакторинга с использованием этого базового класса - MyProfileComponent + MyProfileFormComponent.
Было:
1) ViewChild(MyProfileFormComponent) cmpRef: MyProfileFormComponent в родительской, перед сохранением явно опрашивалась форма на предмет валидности => cmpRef.formGroupName.valid.
Проблема №1 - нет интерфейса, необходимо было все формы по имени, или в каждой форме определять свои методы по сбору инфы о валидности.
2) В дочернюю компоненту через @Input параметры передавалось св-во [triggerValidation]="triggerValidation", изначально установленное в false. В случае неуспешной проверки на валидность из #1 оно устанавливалось в тру. В дочерней компоненте внутри ngOnChanges была проверка вида
if(triggerValidation) { someCodeToMarkFormAsTouched },
которая в этот момент заставлял отработать отображение валидации.
Проблема №2 - необходим лишний параметр triggerValidation, дочерние компоненты обладали своим не универсальным кодом для markAsTouched.
Стало:
1) Убрали параметр triggerValidation и все его вызовы, код по markAsTouched и тд
2) Дочернюю MyProfileFormComponent унаследовали от FormComponentBase, и определили абстрактный isValid()
3) Меняем сигнатуру на @ViewChild(MyProfileFormComponent) accountInfoComponentRef: FormComponentBase, нам достаточно базового класса для вызова isValid()
4) В родительской в момент сохранения проверяем нашу компоненту и действуем согласно результату. В случае неуспеха сработает отображение валидации благодаря #2.
P.S.
В момент рефакторинга MyProfile была замечена страшная штука с ручной проверкой значений контролов в дочерней компоненте перед сохранением. Не надо так, ведь в стейте есть и оригинал в данном случае, и текущие значения. И тот факт, что мы изменили сигнатуру в #3 тоже помешает так вот действовать.
Ентот страх был убран, и перенесен в стейт.
Важно!
Напоминаю, что бизнес-логику лучше определять там, т.к. редьюсеры это чистой воды pure function, и мы в последствии будет писать юнит-тесты под них и радоваться жизни (или материться, хотя одно другому не мешает :) ).