Кастомные Emitter'ы и Subject'ы в Angular: инкапсулируем логику Toggle и MultiSelect

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В крупных проектах на 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(v => v.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 });
    }

		setCheckedForAll(checked: boolean) {
      		this.list = this.list.map(v => ({...v, checked}));					
    }

		next(v: T[]) {
      	this.eventEmitter.emit(v);
				suer.next(v);
    }
}

юзаем в компоненте:

export class SampleComponent {
		@Input set data(v: SampleDto) {
    		this.multiSelector = new EntityMultiSelector<SampleDto>;
        this.selectedSamples = this.multiSelector.eventEmiter;
    }
    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://habr.com/ru/post/533946/


Интересные статьи

Интересные статьи

В этой статье мы рассмотрим, как система управления 1С-Битрикс справляется с большими нагрузками. Данный вопрос особенно актуален сегодня, когда электронная торговля начинает конкурировать по обороту ...
Битрикс24 — популярная в малом бизнесе CRM c большими возможностями даже на бесплатном тарифе. Благодаря API Битрикс24 (даже в облачной редакции) можно легко интегрировать с другими системами.
Если честно, к Д7 у меня несколько неоднозначное отношение. В некоторых местах я попискиваю от восторга, а в некоторых хочется топать ногами и ругаться неприличными словами.
Cтатья будет полезна тем, кто думает какую выбрать CMS для интернет-магазина, сравнивает различные движки, ищет в них плюсы и минусы важные для себя.
В статье описаны необходимые параметры сервера для оптимальной работы сайта на платформе 1С-Битрикс.