loop through BehaviorSubject<> and set prop of the object

Viewed 788

I am having this Multi-Select value change event where I would like to first filter selected object from the BehaviorSubject and then set the IsSelected prop to true and then for all remaining objects in the stream, set the IsSelected prop to false. Tried various things by looking online but have not found any solution. Please help.

Model:

export class SelectOptions {
  DisplayName: string;
  Value: string;
  IsSelected: boolean;
}

Component:

selectOptions$: BehaviorSubject < SelectOptions[] > = new BehaviorSubject<SelectOptions[]>([]);

ngOnInit() {
  this.reportService
    .getFiltersData(this.fieldNameEnum.AicSuites)
    .pipe(
      tap((res) => {
        console.log("--- data from api ---");
        this.selectOptions$.next(res);
        console.log(this.selectOptions$.value); = > this shows proper data
      })
    )
    .subscribe();
}

OnValueChange event:

// here data is a array of selected items from the multi-select
// the goal here is to whenever user select any item, i need to set that object IsSelected prop to true and send it to Api later on.
// if user select the same item again, it's actually de-selectiong the item and (data) array will always have selected items. so i need to set IsSelected = false for all remaining items from Observable stream.


markAsSelected(data) {
  data.forEach((selectedItem: SelectOptions) => {
    this.selectOptions$
      .pipe(
   ?? => no idea what to do here.tired several things but getting compilation error.
 )
})}

Any help would be appreciated. Any other approach to doing the same thing in a better way is welcome.

2 Answers

Your main question is about how to mutate a state in rxjs (add, delete, change value, ...). This can be achieved by using the scan operator.

Further info: In the scan later we are using the 1. param (accumulator) as oldOptions/state and the 2. param (value) as function (fn) that gets the oldOptions/state returns the new/updated state.

Step 1 - what represents your state

state: SelectOptions[];

Step 2 - what mutation can be done to your state

  • state override: currently when this.reportService.getFiltersData(this.fieldNameEnum.AicSuites) emits it overrides your whole state. This happens in the tap operations with the this.selectOptions$.next(res) line.
  • state mutation: You want to mutate your state when markAsSelected() is called. You want to invert the value of IsSelected of selectedOptions.

Step 3 - create functions that mutate the state you defined

You will see the usage and why we built it like we did in Step 4

  • state override
function stateOverride (newOptions) {
  return function (oldOptions) {
    return newOptions;
}
// shorter alternative syntax for the same function:
const stateOverride = (newOptions) => (oldOptions) => newOptions;

function stateMutation (selectedOptions) {
  return function (oldOptions) {
    // I use the DisplayName to compare the selectedOptions with the oldOptions 
    const selectedDisplayNames = selectedOptions.map(selectedOption => selectedOption.DisplayName)

    /* We map over every option. If this specific option is included in the selectedDisplayNames
    we create a new object with all old values but inverted IsSelected.
    Otherwise we let the oldOption as it is and do not mutate it.*/
    return oldOptions.map(oldOption => selectedDisplayNames.includes(oldOption.DisplayName)
      ? {...oldOption, ...{IsSelected: !=oldOption.IsSelected}}
      : oldOption
  }
}

Step 4 - Use the scan operator to build, update your state

private newState$ = this.reportService.getFiltersData(this.fieldNameEnum.AicSuites)

private optionsSelect$ = new Subject();

private selection$ = merge(
  newState$.pipe(map(v => stateOverride(v)),
  optionsSelect$.pipe(map(v => stateMutation(v))
).pipe(
  scan((oldOptions, fn) => fn(oldOptions), [])
  /* This is why we used this weird function syntax (newOptions) => (oldOptions) => in Step 3.
     1. We map the stateOverride/stateMutation to the Observables.
     2. Those return itself another function that takes the oldOptions
     3. In the scan the second param is now a function that takes the oldOptions and returns the updated state/options.*/
)

function markAsSelected(data): void {
  this.optionsSelect$.next(data);
}

FYI: If you understand this mechanism, you can update any state in pipes without side-effects, this way. If you understand the principle and follow the steps this will be a usefull tool for many different usecases. Feel free to ask for more informations. I update the answer, or can comment below.

try this,

  markAsSelected(data ) {
    data.forEach((selectedItem: SelectOptions) => {
      this.selectOptions$.asObservable().pipe(map(selectedObj => {
          selectedObj.forEach(obj => {
            if (selectedItem === obj) {
              selectedItem.IsSelected = true;
              console.log('true');
            } else {
              selectedItem.IsSelected = false;
            }

          });

        })
      );
    });
  }
Related