Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RxJS 6 filter and map an observable array of items

Tags:

angular

rxjs

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);
like image 237
Jan Nielsen Avatar asked Dec 06 '18 22:12

Jan Nielsen


People also ask

How do you map an array to an Observable?

To convert from array to observable you can use Rx. Observable. from(array) . To convert from observable to array, use obs.

How do you filter Observable data using RxJS operators?

RxJS filter() Filtering Operator The RxJS filter() operator is like the well-known Array Array . prototype. filter() method. This operator takes values from the source Observable, passes them through a predicate function and only emits those values that get TRUE.

What is the use of map in Observable?

RxJS map() Transformation Operator RxJS map() operator is a transformation operator used to transform the items emitted by an Observable by applying a function to each item. It applies a given project function to each value emitted by the source Observable and then emits the resulting values as an Observable.

Does map return an Observable?

http. get(apiURL) returns an Observable . map is an observable operator which calls a function for each item on its input stream and pushes the result of the function to its output stream. In this case each input item is a Response object.


2 Answers

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>
like image 93
Adrian Brand Avatar answered Oct 16 '22 17:10

Adrian Brand


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.

like image 1
Matt Saunders Avatar answered Oct 16 '22 15:10

Matt Saunders