[QB Dev Blog] Новые фичи

[QB Dev Blog] Новые фичи

Eugen Tatuev

Влил обновление с парой новых фич, подробности далее.

Store.clear()

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

На данный момент применено целиком на профиль, квоты, ордера, пропозалы, контракты, админку, и на несколько вложенных там.

Пример 1:

Quote - имеем родительскую компоненту QuoteComponent, внутри которой происходит весь роутинг квоты (центр и билд). Внутри чистим QuoteStore, который запишет в sub-state "quote" наши дефолты.

Применение Store.clear()

Однако, мы имеем еще 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
Десериализация BOM в квоте
Десериализация BOM в ордерах
В противном случае получим чистый анонимный объект без методов и "красивую" консоль при попытке state.obj.doSomeCoolStuffAndGetMyCats() :)
Грамотное же именование поможет не упустить возможные сайд-эффекты.

ComponentBase

Некоторое время назад Стас подал хорошую идею по использованию базового класса для компонент, который сможет в общем виде выполнять некоторые рутинные моменты без необходимости копипасты. ComponentBase призван быть базовым для всех наших компонент. При необходимости в будущем отделить "страницы" там от контролов - там и отделим :)

Методы:

trackSubscription(subscription: Subscription): void

Передаем туда все наши подписки, и они автоматические будут отписаны по ngOnDestroy()

markForChangeDetectionCheck(): void

Внутри вызывает ChangeDetectorRef.markForCheck(). Используется в тех редких случаях, когда в OnPush компонентах надо запустить change detection изнутри.

Пример 1:

Отнаследовались

Нужно передать в базовый класс экземпляр Injector сервиса посредством вызова super() в конструкторе.

передаем Injector

Ну и пользуемся его услугами :)

Используем нужные методы

Больше ничего в случе подписок делать не нужно.

Важно!
Если в компоненте вам понадобиться воспользоваться ngOnDestroy() по другим причинам, то необходимо вызвать родительский метод вручную, иначе болтаться нашим подпискам во веки вечные :)
Вызываем родительский ngOnDestroy()


Пример 2:

На данный момент известный многим ComponentWithSaveDialog уже унаследован от ComponentBase и переделан под Injector, а значит все наследники получили этот функционал. Определенный там метод clear() переименован в ngOnDestroy().

бывший ComponentWithSaveDialog.clear()

Т.е. любой наследник от ComponentWithSaveDialog теперь не обязан наследоваться сам от OnDestroy() и ничего вызывать, бывший clear() отработает автоматически. Однако, если он все таки нужен - не забываем про super.ngOnDestroy(), который дернет этот самый бывший clear(), который, в свою очередь, вызовет "деструктор" от ComponentBase.

Важно!
Зачем в ComponentBase передается Injector? А затем, чтобы базовый класс мог получить все необходимые ему зависимости, без нужны явно инжектить в нашу компоненту 101 сервис, и все скопом передавать в super().
ComponentBase использует injector в своих корыстных целях
При этом, т.к. 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()

Вызываем встроенный validate() и передаем FormGroup

3) Меняем сигнатуру на @ViewChild(MyProfileFormComponent) accountInfoComponentRef: FormComponentBase, нам достаточно базового класса для вызова isValid()
4) В родительской в момент сохранения проверяем нашу компоненту и действуем согласно результату. В случае неуспеха сработает отображение валидации благодаря #2.

Применение isValid()


P.S.

В момент рефакторинга MyProfile была замечена страшная штука с ручной проверкой значений контролов в дочерней компоненте перед сохранением. Не надо так, ведь в стейте есть и оригинал в данном случае, и текущие значения. И тот факт, что мы изменили сигнатуру в #3 тоже помешает так вот действовать.

Зерги отакуют!11

Ентот страх был убран, и перенесен в стейт.

Вычисляем значение в новое поле
Используем при необходимости
Важно!
Напоминаю, что бизнес-логику лучше определять там, т.к. редьюсеры это чистой воды pure function, и мы в последствии будет писать юнит-тесты под них и радоваться жизни (или материться, хотя одно другому не мешает :) ).




Report Page