RxJS 6 filter and map an observable array of items

28,657

Solution 1

mergeAll merges an observable of observables.

You want

cards: Observable<PlanCard[]> = plans.pipe(
  map(plans => plans.filter(plan => plan.is_active)), // An array comes down the pipe, you don't want to filter out the arrays but the elements in the array
  map(plans => plans.map(plan => new PlanCard(plan, 1, 1))), // An array of active plans can be mapped into an array of PlanCards
  reduce((results, plans) => [...results, ...plans], []) // Reduce all the arrays into a single array
);

You can use scan instead of reduce if you want the accumulator of the reduce to come down the pipe each time a new array comes down, reduce only fires after all arrays have come down the pipe.

I don't usually use a filter followed by a map but it is easier to see what is going on, I would usually do it in a single step with a reduce,

plans => plans.reduce((results, plan) => plan.is_active ? [...results, new PlanCard(plan, 1, 1)] : results, [])

Is the same as a filter followed by a map but does it in a single iteration instead of two.

It can be quite confusing when you have observables of arrays, you need to consider what functions you want to apply to the observable and which you want to apply to the array that comes down the pipe.

const { from } = rxjs;
const { map, reduce } = rxjs.operators;

const plans = from([
  [{id: 1, is_active: true}, {id: 2, is_active: false}, {id: 3, is_active: true}, {id: 4, is_active: true}],
  [{id: 5, is_active: true}, {id: 6, is_active: true}],
  [{id: 7, is_active: false}, {id: 8, is_active: true}, {id: 9, is_active: true}],
  [{id: 10, is_active: true}, {id: 11, is_active: false}, {id: 12, is_active: true}, {id: 13, is_active: false}],
]);

function PlanCard(plan) {
  this.id = plan.id;
}

plans.pipe(
  map(plans => plans.reduce((results, plan) => plan.is_active ? [...results, new PlanCard(plan, 1, 1)] : results, [])),
  reduce((results, plans) => [...results, ...plans], [])
).subscribe(results => { console.log(results); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.min.js"></script>

Solution 2

This had me scratching my head for a while, so a brief example which may be of help if you're looking to filter an observable array of objects. Kudos to @Adrian for pointing me in the right direction.

If you have a service that returns an observable array of objects, e.g:

dataSub = new BehaviorSubject<Array<object>>([]);

getItems(): Observable<Array<object>>  {
    return this.dataSub.asObservable();
}

You can filter with a given criteria with the following. In this example, I want only elements where the property 'selected' is true.

constructor() {
  this.getItems()
    .pipe(
      map(items => items), /* Don't forget to add this! */
      filter(item => item['selected'] === true)
    )
    .subscribe(data => {
      console.log(data);
    });
}

The (rookie) error which led to my confusion was omitting the 'map', so the filter function was being applied to the whole array, rather than each element.

Share:
28,657

Related videos on Youtube

Jan Nielsen
Author by

Jan Nielsen

Technologist, programmer, entrepreneur.

Updated on July 09, 2022

Comments

  • Jan Nielsen
    Jan Nielsen almost 2 years

    In my Angular 7.1.1 application with RxJS 6.3.3, an observable array of plans should be transformed into an observable array of active plan cards. The active filter is:

    filter(plan => plan.is_active)
    

    and the transformation of plan to card is:

    map(plan => new PlanCard(plan, 1, 1))
    

    so, presumable, the answer is something like:

    plans: Observable<Plan[]> = getPlans();
    
    cards: Observable<PlanCard[]> = plans.pipe(
      mergeAll(),
      filter(plan => plan.is_active),
      map(plan => new PlanCard(plan, 1, 1)),
      toArray()
    );
    

    But alas, cards is empty.

    The plans can correctly be transformed to cards via:

    cards: Observable<PlanCard[]> = plans.pipe(
      map(plans => plans.map(plan => new PlanCard(plan, 1, 1)))
    );
    

    but adding a filter prior does not filter the plans, unfortunately:

    cards: Observable<PlanCard[]> = plans.pipe(
      filter(plans => plans.filter(plan => plan.is_active)),
      map(plans => plans.map(plan => new PlanCard(plan, 1, 1)))
    );
    

    Any better ideas?

    export const planData: Plan[] = [{
      name: 'P1',
      is_active: true
    }, {
      name: 'P2',
      is_active: true
    }, {
      name: 'P3',
      is_active: true
    }, {
      name: 'P4',
      is_active: true
    }, {
      name: 'P5',
      is_active: false
    }];
    
    export const plans = createObservable(planData);
    
    • user184994
      user184994 over 5 years
      You need to map the array as well then, so something like map(plans => plans.map(plan => new PlanCard(plan, 1, 1)), and it looks like you want to use the arrays filter function, not the Observables
    • Jan Nielsen
      Jan Nielsen over 5 years
      Right, @user184994 -- I started with that approach -- I'll update my question with that failed attempt as well.
  • Alex Okrushko
    Alex Okrushko over 3 years
    map(items => items) is a noop
  • Aouidane Med Amine
    Aouidane Med Amine over 3 years
    replace the map & filter operators with : map((items: Item[]) => items.filter((item: Item) => item.selected=== true)))