'Filtering an observable array into another observable array (of another type) by an observable property of the items

As the title probably doesn't explain to well what I am doing, I'll give a small example:

A REST api returns a list of objects. For each of them, a checkbox should be displayed and the selected values used for further processing. However, the api might be called multiple times (to refresh the data), so I am dealing with Observables and the result (i.e. the selected objects) should also be available as an Observable.

I have created a simple Plunker to illustrate this: I receive an observable in my Component with an array of objects (here just strings to simplify):

var observable = Observable.of(["Hello", "World"]);

Based on this, I create objects of type Item while store the object and the information whether they are checked or not (as an Observable/BehaviorSubject):

this.items$ = observable.map(arr => arr.map(s => new Item(s)))
//...
class Item {
   public readonly isChecked$ : BehaviorSubject<boolean> = new BehaviorSubject(false);

   constructor(public readonly value : string) { }
}

Then, I display them using the solution from How to two-way bind my own RxJS Subject to an [(ngModel)]? :

<li *ngFor="let item of items$ | async as items" >
   <input id="cb-{{item.value}}" type="checkbox" [ngModel]="item.isChecked$ | async" (ngModelChange)="item.isChecked$.next($event)"  />
   <label for="cb-{{item.value}}">{{item.value}}</label>
</li>

However, I now have real problems to get an Observable<string[]> containing the selected values. Basically, I have an Observable<Item[]> and I need to:

  1. Access the observables of the array items (item.isSelected$)
  2. Filter based on their value
  3. Convert the result back to an Observable, i.e. collect the values (at one time) in an array and return it as an Observable

Number three is what I was not able to achieve: As soon as I flatMap the array for example, there seems to be "no way back" to create an array again - I do not want to end up with an Observable<string> (notifying me when a new item is checked), but with an Observable<string[]> which is raised each time the set of the selected items changes.

The best I could come up with is the code in the plunker (which relies on subscribe + manually inserting the values into a second array):

this.items$
    .switchMap(items => items.map(item => item.isChecked$.map(isChecked => ({ checked: isChecked, value: item.value}))))
    .mergeAll()
    .subscribe(o => {
        var selected = this._selectedItems.slice(0);
        if (o.checked)
            selected.push(o.value);
        else {
            var index = selected.indexOf(o.value);
            selected.splice(index, 1);
        }

        this._selectedItems = selected;
        this._selectedItems$.next(this._selectedItems);
    });

Is there a more elegant function in rxjs allowing me to achieve this (without the need for the second array)?



Solution 1:[1]

If you want to push values into another observable, you should use a Subject. A Subject acts as both an observer and an observable.

So your filter can push values into the new observable by using the next() method, something like this

mySubject.next(val)

Another part of your code can subscribe to this subject, and react accordingly

Solution 2:[2]

Consider keeping a BehaviorSubject up to date with your item list, then you can map your checkbox events to the states of all your items.

const item_subject$ = new BehaviorSubject([]); // BehaviorSubject<Item[]>
items$.scan((acc, item) => acc.push(item), [])
      .subscribe(item_subject$);

const checked_subject$ = new Subject(); // Subject<string[]>
items$.subscribe(item =>
  item.isChecked$.subscribe(_ => 
    checked_subject.next(
      item_subject$.getValue()
                   .filter(inner_item => inner_item.isChecked$.getValue())
                   .map(inner_item => inner_item.value)
    )
  )
);

Timing:

checked_subject$ should never have false positives thanks to the happens-before of isChecked$ updating its value to isChecked$ emitting to its subscribers. The only racey part might be the assumed happens-before of scan in line 2 pushing a new item into the item_subject vs. that item changing state. Not a realistic concern I imagine, since users would have to click instantly when the new items renders.

Solution 3:[3]

for that kind of problems I use Angular Material CDK Collection

You can select or deselect items and subscribe to the model to get it in reactive way.

const model = new SelectionModel(
  true,   // multiple selection or not
  [2,1,3] // initial selected values
);

// select a value
model.select(4);
console.log(model.selected.length) //->  4

// deselect a value
model.deselect(4);
console.log(model.selected.length) //->  3

// toggle a value
model.toggle(4);
console.log(model.selected.length) //->  4

// check for selection
console.log(model.isSelected(4)) //-> true

// sort the selections
console.log(model.sort()) //-> [1,2,3,4]

// listen for changes
model.changed.subscribe(s => console.log(s));

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mikkel
Solution 2 concat
Solution 3 G. Alarcón