Custom Emitters and Subjects in Angular: Encapsulating Toggle and MultiSelect Logic

In large Angular projects, you can often see repetitive behavior in components. It is desirable to move this behavior out of the component into separate classes that can be reused. I will consider two fairly popular cases: switch and multiple choice of entities.





Case 1: Toggle

Often in the source code you see something like this:





export class SampleComponent {
	@Output somethingSelected = new EventEmitter<boolean>()
  ...
  private _selected = false;
  toggleSelected() {
  		this._selected = !this._selected;
      this.somethingSelected.emit(this._selected);
  }
}
      
      



or like this:





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=src/app/app.component.html





.





I would be glad to have your ideas in the comments. Constructive criticism is encouraged.








All Articles