[Angular, TypeScript] Кастомные Emitter'ы и Subject'ы в Angular: инкапсулируем логику Toggle и MultiSelect
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В крупных проектах на Angular часто можно встречать повторяющееся поведение в компонентах. Такое поведение желательно выносить из компонента в отдельные классы, которые можно переиспользовать. Рассмотрю два достаточно популярных кейса: переключатель и множественный выбор сущностей.Кейс 1: Переключалка (Toggle)Часто в исходниках приходится видеть примерно такой код:
export class SampleComponent {
@Output somethingSelected = new EventEmitter<boolean>()
...
private _selected = false;
toggleSelected() {
this._selected = !this._selected;
this.somethingSelected.emit(this._selected);
}
}
либо такой:
export class SampleComponent {
@Output somethingSelected = new EventEmitter<boolean>()
...
private _selected$ = new BehaviorSubject<boolean>(false);
toggleSelected() {
this._selected$.next(!this._selected$.value);
this.somethingSelected.emit(this._selected$.value);
}
}
Вроде бы ничего страшного, если проект небольшой, компоненты тоже. Но если таких переключалок добрый десяток, а то и добрая сотня, начинаешь вспоминать принцип DRY. Нужно какое то решение для уменьшения количества бойлерплейта в коде.Попробуем унаследоваться от BehavoirSubject и добавить туда метод toggle()
export class ToggleSubject extends BehaviorSubject<boolean> {
toogle() {
this.next(!this.value);
}
}
Таким образом код компонента у нас приобретает вид:
export class SampleComponent {
@Output somethingSelected = new EventEmitter<boolean>()
...
private _selected$ = new ToggleSubject(false);
toggleSelected() {
this._selected$.toggle();
this.somethingSelected.emit(this._selected$.value);
}
}
уже получше, но кода стало меньше не намного. Попробуем вовсе избавиться от метода toggleSelected и приватного свойства _selected. Можно создать класс ToggleSwitcher и унаследовать его от EventEmitter
export class ToggleSwitcher extends EventEmitter<boolean> {
get value(): boolean {
return this._value
}
constructor(private _value = false) {
super();
}
toggle() {
this.emit(!this.value);
}
emit(v: boolean) {
this._value = v;
super.emit(v);
}
}
теперь наш компонент приобретает такой вид:
export class SampleComponent {
@Output somethingSelected = new ToggleSwitcher()
...
}
в шаблоне для переключения можем использовать somethingSelected.toggle() для получения текущего значения somethingSelected.value для задания значения somethingSelected.emit(true / false). Если нужно значение по умолчанию true, можем его передать в конструктор ToggleSwitcher. Поскольку мы унаследовались от EventEmitter, проблем с эмитом событий также не будет.
@Output somethingSelected = new ToggleSwitcher(true)
Плюс такого решения очевиден: минимум бойлерплейта, все просто и лаконично. Однако перфекционист может сказать, что тут нарушается SRP. Ведь EventEmitter у нас служит для эмита событий, а мы через наследование вешаем на него еще дополнительную логику по переключению. Что ж, есть еще один вариант. Можем не наследоваться от EventEmitter, а получать его из свитчера.
export class ToggleSwitcher extends BehaviorSubject<boolean> {
eventEmitter = new EventEmitter<boolean>();
next(v: boolean) {
this.eventEmitter.emit(v);
super.next(v);
}
toggle() {
this.next(!this.value)
}
}
Но тогда в компоненте будет на одну строчку больше кода, чем в предыдущем варианте
export class SampleComponent {
somethingSwitcher = new ToggleSwitcher(false);
@Output somethingSelected = this.somethingSwitcher.eventEmitter;
}
Кейс 2: множественный выборТакже наиболее часто встречающийся кейс: на странице отображается список сущностей, должна быть возможность выбирать из списка нужные сущности, нужно показывать общее количество сущностей, количество выбранных сущностей, должна быть кнопка выбрать все и очистить выбор. В Output() нужно эмиттить массив выбранных сущностей.Также должна быть возможность показывать в шаблоне через ngFor выбрана ли сущность или нет. Поэтому в *ngFor будем ложить не массив сущностей, а массив стейтов, содержащих сущность и состояние: выбран / не выбран
export class EntityCheckedState<T> {
entity: T;
checked: boolean
}
export class EntityMultiSelector<T> extends BehaviorSubject<T[]> {
private _list: EntityCheckedState<T>[];
eventEmitter = new EventEmitter<T[]>();
get list(): EntityCheckedState<T>[] {
return this._list;
}
set list(v: EntityCheckedState<T>[]) {
this._list = v;
this.next(this.list.filter(({checked}) => checked).map(({entity}) => entity));
}
constructor(v: T[], defaultChecked = false) {
super(defaultChecked? v : []);
this.eventEmitter.emit(defaultChecked? v : []);
this._list = v.map(entity => ({entity, checked: defaultChecked}));
}
setCheckedForEntity(entity: T, checked: boolean) {
this.list = this.list.map(v => (v.entity === entity ? { ...v, checked } : v));
}
setCheckedForAll(checked: boolean) {
this.list = this.list.map(v => ({...v, checked}));
}
next(v: T[]) {
this.eventEmitter.emit(v);
super.next(v);
}
}
юзаем в компоненте:
export class SampleComponent {
@Input() set data(v: SampleDto[]) {
this.multiSelector = new EntityMultiSelector<SampleDto>(v);
this.selectedSamples = this.multiSelector.eventEmitter;
}
multiSelector: EntityMultiSelector<SampleDto>;
@Output() selectedSamples: EventEmitter<SampleDto[]>
}
Как это будет выглядеть в шаблоне:
<app-sample-entity *ngFor = "let state of multiSelector.list"
[data] = "state.entity"
[checked] = "state.checked"
(checked) = "multiSelector.setCheckedForEntity(state.entity, $event)"
></app-sample-entity>
Всего: {{multiSelector.list.length}} Выбрано: {{multiSelector.value.lenght}}
<button (click) = "multiSelector.setSelectedForAll(false)">Очистить</button>
работающая версия кода:https://stackblitz.com/edit/angular-ivy-kyaeac?file...nent.htmlПохожим способом можно инкапсулировать и множество иных кейсов. Буду рад вашим идеям в комментариях. Конструктивная критика приветствуется.
===========
Источник:
habr.com
===========
Похожие новости:
- [Angular, TypeScript] Декларативная работа с перечисляемыми типами через Record: как не городить switch или вложенные if
- [JavaScript, Программирование, Node.JS] Дино (Deno): Создать API для отдыха с помощью JWT (перевод)
- [JavaScript, Программирование, TypeScript] Кастомизация компонентов Ant Design и оптимизация бандла
- [JavaScript, Node.JS, TypeScript] Оптимизация трафика при синхронизация состояний через Jsonpatch
- [JavaScript, Интерфейсы, ReactJS, TypeScript] Использование Effector в стеке React + TypeScript
- [Разработка веб-сайтов, Программирование, TypeScript] ТайпСкрип: Ох уж эта весёлая система типов
- [Разработка веб-сайтов, JavaScript] JavaScript исполняется 25 лет: краткая история языка и скидка 50% на WebStorm
- [Высокая производительность, JavaScript, Программирование, TypeScript] Производительность TypeScript (перевод)
- [JavaScript, Программирование, Angular] RxJS и Angular: искусство отписки от уведомлений (перевод)
- [Разработка веб-сайтов, JavaScript, Angular, ReactJS, TypeScript] Schedulers в RxJS
Теги для поиска: #_angular, #_typescript, #_toggle, #_multiselect, #_angular, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:19
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В крупных проектах на Angular часто можно встречать повторяющееся поведение в компонентах. Такое поведение желательно выносить из компонента в отдельные классы, которые можно переиспользовать. Рассмотрю два достаточно популярных кейса: переключатель и множественный выбор сущностей.Кейс 1: Переключалка (Toggle)Часто в исходниках приходится видеть примерно такой код: export class SampleComponent {
@Output somethingSelected = new EventEmitter<boolean>() ... private _selected = false; toggleSelected() { this._selected = !this._selected; this.somethingSelected.emit(this._selected); } } export class SampleComponent {
@Output somethingSelected = new EventEmitter<boolean>() ... private _selected$ = new BehaviorSubject<boolean>(false); toggleSelected() { this._selected$.next(!this._selected$.value); this.somethingSelected.emit(this._selected$.value); } } export class ToggleSubject extends BehaviorSubject<boolean> {
toogle() { this.next(!this.value); } } export class SampleComponent {
@Output somethingSelected = new EventEmitter<boolean>() ... private _selected$ = new ToggleSubject(false); toggleSelected() { this._selected$.toggle(); this.somethingSelected.emit(this._selected$.value); } } export class ToggleSwitcher extends EventEmitter<boolean> {
get value(): boolean { return this._value } constructor(private _value = false) { super(); } toggle() { this.emit(!this.value); } emit(v: boolean) { this._value = v; super.emit(v); } } export class SampleComponent {
@Output somethingSelected = new ToggleSwitcher() ... } @Output somethingSelected = new ToggleSwitcher(true)
export class ToggleSwitcher extends BehaviorSubject<boolean> {
eventEmitter = new EventEmitter<boolean>(); next(v: boolean) { this.eventEmitter.emit(v); super.next(v); } toggle() { this.next(!this.value) } } export class SampleComponent {
somethingSwitcher = new ToggleSwitcher(false); @Output somethingSelected = this.somethingSwitcher.eventEmitter; } export class EntityCheckedState<T> {
entity: T; checked: boolean } export class EntityMultiSelector<T> extends BehaviorSubject<T[]> { private _list: EntityCheckedState<T>[]; eventEmitter = new EventEmitter<T[]>(); get list(): EntityCheckedState<T>[] { return this._list; } set list(v: EntityCheckedState<T>[]) { this._list = v; this.next(this.list.filter(({checked}) => checked).map(({entity}) => entity)); } constructor(v: T[], defaultChecked = false) { super(defaultChecked? v : []); this.eventEmitter.emit(defaultChecked? v : []); this._list = v.map(entity => ({entity, checked: defaultChecked})); } setCheckedForEntity(entity: T, checked: boolean) { this.list = this.list.map(v => (v.entity === entity ? { ...v, checked } : v)); } setCheckedForAll(checked: boolean) { this.list = this.list.map(v => ({...v, checked})); } next(v: T[]) { this.eventEmitter.emit(v); super.next(v); } } export class SampleComponent {
@Input() set data(v: SampleDto[]) { this.multiSelector = new EntityMultiSelector<SampleDto>(v); this.selectedSamples = this.multiSelector.eventEmitter; } multiSelector: EntityMultiSelector<SampleDto>; @Output() selectedSamples: EventEmitter<SampleDto[]> } <app-sample-entity *ngFor = "let state of multiSelector.list"
[data] = "state.entity" [checked] = "state.checked" (checked) = "multiSelector.setCheckedForEntity(state.entity, $event)" ></app-sample-entity> Всего: {{multiSelector.list.length}} Выбрано: {{multiSelector.value.lenght}} <button (click) = "multiSelector.setSelectedForAll(false)">Очистить</button> =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:19
Часовой пояс: UTC + 5