I want to remove duplicate objects by name property by I am getting No overload matches this call in rxjs - rxjs

allEmployees$ = this.http.get<IEmployees[]>('../../assets/employees').pipe(
map(allEmployees =>
allEmployees.map(Employee =>
<IEmployees>({
id: Employee.id,
name: Employee.name,
email: Employee.email,
gender: Employee.gender,
productid: Employee.productid,
productName: 'N/A',
})
)),
switchMap(data => data.reduce((acc, curr) => {
const exists = acc.find(v => v['name'] === curr['name']);
return exists ? acc : acc.concat(curr);
}, []))
);
Please see pic for more info:

It looks like the Typescript compiler is having trouble inferring the type of the array in the seed parameter of reduce. Try casting it to IEmployees[]

Related

Modify Observable array before subscribing by rxjs operators

Having two models look like this:
export class Game {
id: number;
name: string;
platform: number;
}
export class Platform {
id: number;
name: string;
}
having an observable array of the Game object that has a property of platformId which is related to the Platform object. for better understand, I created two separate methods for getting a list of my games and another method for getting a platform based on id.
getGames(): Observable<Game[]> {
return of([
{
id: 1,
name: 'god of war',
platform: 1,
},
{
id: 2,
name: 'halo',
platform: 2,
},
]);
}
getPlatform(id): Observable<Platform> {
if (id === 1)
return of({
id: 1,
name: 'ps4',
});
if (id === 2)
return of({
id: 2,
name: 'xbox',
});
}
now I'm with help of two operators of rxjs (switchMap,forkJoin) reach to this point:
this.getGames()
.pipe(
switchMap((games: Game[]) =>
forkJoin(
games.map((game) =>
forkJoin([this.getPlatform(game.platform)]).pipe(
map(([platform]) => ({
game: game.name,
platform: platform.name,
}))
)
)
)
)
)
.subscribe((v) => {
console.log(v);
this.gamesV2 = v;
});
and my final result:
[
{
game: "god of war"
platform: "ps4"
},
{
game: "halo"
platform: "xbox"
}
]
is it possible to achieve this in a simpler way?
StackBlitz
By flattening an inner observable array of getPlatformV2:
this.getGames()
.pipe(
switchMap(games =>
combineLatest(
games.map(game =>
this.getPlatformV2(game.id).pipe(
map(platform => ({
game: game.name,
platform
}))
)
)
)
)
)
Extra: Regarding Game and Platform, you should use TS types or interfaces instead of classes if you won't implement a constructor on those.
I find another way in StackOverflow thanks to Daniel Gimenez and putting it here, if anyone has better and simpler, I really appreciate it to share it with me.
Create another method which returns the name of the platform:
getPlatformV2(id): Observable<string> {
const platforms = [
{
id: 1,
name: 'ps4',
},
{
id: 2,
name: 'xbox',
},
];
return of(platforms.find(x=>x.id===id).name);
}
}
and instead of using two forkjoin, I used concatMap
this.getGames()
.pipe(
switchMap((games) => games),
concatMap((game) =>
forkJoin({
game: of(game.name),
platform: this.getPlatformV2(game.platform),
})
),
toArray()
)
.subscribe(console.log);

RxJS group by field and return new observable

I've following interfaces and Observable<Machine[]>, what I want to achive is group by Machine symbol property in Observable<Machine[]> and return mapped observable Observable<Order[]>.
export interface Machine {
symbol: string;
price: number;
amount: number;
id: number;
}
export interface Order {
symbol: string;
machines: OrderMachine[];
}
export interface OrderMachine {
price: number;
amount: number;
id: number;
}
I've tried to use RxJS gropBy operator but it seems it return grouped array one by one.
machines: Machine[] = [
{ amount: 1, id: 1, symbol: "A", price: 1 },
{ amount: 1, id: 2, symbol: "A", price: 2 }
];
of(machines).pipe(
takeUntil(this.unsubscribe),
mergeMap(res => res),
groupBy(m => m.symbol),
mergeMap(group => zip(of(group.key), group.pipe(toArray()))),
map(x => { // here I have probably wrong model [string, Machine[]]
const orderMachines = x[1].map(y => { return <OrderMachine>{price: y.price, amount: y.amount, id: y.id }})
return <Order>{ symbol: x[0], machines: orderMachines } })
);
as in result I have Observable<Order> istead ofObservable<Order[]>.
expected result model:
orders: Order[] = [
{
symbol: "A",
machines: [
{ amount: 1, price: 1, id: 1 },
{ amount: 1, price: 2, id: 2 }
]
}
];
Here a possible solution based on your approach but with a few changes:
const machines = [
{ amount: 1, id: 1, symbol: "A", price: 1 },
{ amount: 1, id: 2, symbol: "A", price: 2 },
{ amount: 1, id: 3, symbol: "B", price: 3 }
];
from(machines) // (1)
.pipe(
// (2)
groupBy((m) => m.symbol),
mergeMap((group) => group.pipe(toArray())),
map((arr) => ({
symbol: arr[0].symbol, // every group has at least one element
machines: arr.map(({ price, amount, id }) => ({
price,
amount,
id
}))
})),
toArray(), // (3)
)
.subscribe(console.log);
(1) I changed of(machines) to from(machines) in order to emit the objects from machines one by one into the stream. Before that change the whole array was emitted at once and thus the stream was broken.
(2) I removed takeUntil(this.unsubscribe) and mergeMap(res => res) from the pipe since there is no reason to have them in your example. takeUntil wouldn't have any effect since the stream is finite and synchronous. An identity function (res => res) applied with mergeMap would make sense in a stream of streams which is not the case in your example. Or do you actually need these operators for your project because you have an infinite stream of observables?
(3) toArray() is what transforms Observable<Order> to Observable<Order[]>. It waits until the stream ends and emits all streamed values at once as an array.
edit:
The op has mentioned that he rather needs a solution that is compatible with an infinite stream but because toArray only works with finite streams the provided answer above would never emit anything in such scenario.
To solve this I would avoid using groupBy from rxjs. It cvan be a very powerful tool in other cases where you need to split one stream into several groups of streams but in your case you simply want to group an array and there are easier methods for that that.
this.store.pipe(
select(fromOrder.getMachines)
map((arr) =>
// (*) group by symbol
arr.reduce((acc, { symbol, price, amount, id }) => {
acc[symbol] = {
symbol,
machines: (acc[symbol] ? acc[symbol].machines : [])
.concat({ price, amount, id })
};
return acc;
}, {})
),
)
.subscribe((result) =>
// (**)
console.log(Object.values(result))
);
(*) you could use a vanilla groupBy implementation that returns an object of the shape {[symbol: string]: Order}.
(**) result is an object here but you can convert it to an array easily but applying Object.values(result)
#kruschid Thank you very much for your reply, it works properly but unfortynetelly, it doesn't work when I want to use it with my store (ngrx), type is ok but it stops to show log after mergeMap method:
this.store.pipe(select(fromOrder.getMachines),
mergeMap(res => res), // Machine[]
groupBy((m) => m.symbol),
tap(x => console.log(x)), //this shows object GroupedObservable {_isScalar: false, key: "A", groupSubject: Subject, refCountSubscription: GroupBySubscriber}
mergeMap((group) => group.pipe(toArray())),
tap(x => console.log(x)), // this is not printed in console
map((arr) => <Order>({
symbol: arr[0].symbol,
machines: arr.map(({ price, amount, id }) => ({
price,
amount,
id
}))
})),
toArray())) // (3)

RxJS logic which solves a filter/merge issue

This is more a logical problem then a RxJS problem, I guess, but I do not get it how to solve it.
[input 1]
From a cities stream, I will receive 1 or 2 objects (cities1 or cities2 are test fixtures).
1 object if their is only one language available, 2 objects for a city with both languages.
[input 2]
I do also have a selectedLanguage ("fr" or "nl")
[algo]
If the language of the object corresponds the selectedLanguage, I will pluck the city. This works for my RxJS when I receive 2 objects (cities2)
But since I also can receive 1 object, the filter is not the right thing to do
[question]
Should I check the cities stream FIRST if only one object exists and add another object. Or what are better RxJS/logical options?
const cities1 = [
{city: "LEUVEN", language: "nl"}
];
const cities2 = [
{city: "BRUSSEL", language: "nl"},
{city: "BRUXELLES", language: "fr"}
];
const selectedLang = "fr"
const source$ = from(cities1);
const result = source$.pipe(
mergeMap((city) => {
return of(selectedLang).pipe(
map(lang => {
return {
lang: city.language,
city: city.city,
selectedLang: lang
}
}),
filter(a => a.lang === selectedLang),
pluck('city')
)
}
)
);
result.subscribe(console.log)
If selectedLang is not an observable (i.e. you don't want this to change) then I think it would make it way easier if you keep it as a value:
const result = source$.pipe(
filter(city => city.language === selectedLang)
map(city => city.city)
);
There's nothing wrong from using external parameters, and it makes the stream easier to read.
Now, if selectedLang is an observable, and you want result to always give the city with that selectedLang, then you probably need to combine both streams, while keeping all the cities received so far:
const selectedLang$ = of(selectedLang); // This is actually a stream that can change value
const cities$ = source$.pipe(
scan((acc, city) => [...acc, city], [])
);
const result = combineLatest([selectedLang$, cities$]).pipe(
map(([selectedLang, cities]) => cities.find(city => city.language == selectedLang)),
filter(found => Boolean(found))
map(city => city.city)
)
Edit: note that this result will emit every time cities$ or selectedLang$ changes and one of the cities matches. If you don't want repeats, you can use the distinctUntilChanged() operator - Probably this could be optimised using an exhaustMap or something, but it makes it harder to read IMO.
Thanks for your repsonse. It's great value for me. Indeed I will forget about the selectedLang$ and pass it like a regular string. Problem 1 solved
I'll explain a bit more in detail my question. My observable$ cities$ in fact is a GET and will always return 1 or 2 two rows.
leuven:
[ { city: 'LEUVEN', language: 'nl', selectedLanguage: 'fr' } ]
brussel:
[
{ city: 'BRUSSEL', language: 'nl', selectedLanguage: 'fr' },
{ city: 'BRUXELLES', language: 'fr', selectedLanguage: 'fr' }
]
In case it returns two rows I will be able to filter out the right value
filter(city => city.language === selectedLang) => BRUXELLES when selectedLangue is "fr"
But in case I only receive one row, I should always return this city.
What is the best solution to this without using if statements? I've been trying to work with object destruct and scaning the array but the result is always one record.
// HTTP get
const leuven: City[] = [ {city: "LEUVEN", language: "nl"} ];
// same HTTP get
const brussel: City[] = [ {city: "BRUSSEL", language: "nl"},
{city: "BRUXELLES", language: "fr"}
];
mapp(of(brussel), "fr").subscribe(console.log);
function mapp(cities$: Observable<City[]>, selectedLanguage: string): Observable<any> {
return cities$.pipe(
map(cities => {
return cities.map(city => { return {...city, "selectedLanguage": selectedLanguage }}
)
}),
// scan((acc, value) => [...acc, { ...value, selectedLanguage} ])
)
}

Use one of the two incoming observable stream to filter data in angular

I have the below code I have the formcontrol value (its a multicheckbox list), the values is an array of true/false, [true, false, true, true, ...]. This is one of the many lists
I also have the data list - collection of objects of structure {id: number, desc: string, disabled: boolean, selected: boolean}
I need to retrieve the id matching the true by matching index, and set an observable
I have the below code
valueChanged(e: any, name: string, key: string, valuesArray: string) {
this.hasChanged = true;
from(name).pipe(
debounceTime(200),
withLatestFrom(this[name].value), // this[name] is form array, gives [true, false, false,..]
mergeMap(values => forkJoin(
of(values),
this[valuesArray].find(val => val.name === name).data
)),
mergeMap(([values, data]) => {
flatMap((val, i) => val ? data[i].id : null),
filter(id => id !== null),
map(ids => {
this.selectedFilters[key] = ids;
switch (key) {
case 'groupId':
this.curGroup.next(ids);
break;
}
});
})
);
}
I need help on the line flatmap where I want to use values, for each of the value (true/false), if val[i] is true, include data[i].id in the output i want he id array of true values [1,2,4,5,..]. I get the error
Argument of type '([values, data]: [[string, unknown], unknown]) => void' is not assignable to parameter of type '(value: [[string, unknown], unknown], index: number) => ObservableInput'.
Type 'void' is not assignable to type 'ObservableInput'.ts(2345)
Need help on how to use solve this issue. Thanks in advance.

Filter an Array in an Observable

Here is an edited sample from learnrxjs. I want to filter the values in the type array. But thats not how it works: 'This condition will always return 'true' since the types 'string[]' and 'string' have no overlap.'
I am new to rxjs and cant figure out how to filter the array. Any advices? Is it possible?
const source = from([
{ name: 'Joe', age: 31, type: ['a', 'b'] },
{ name: 'Bob', age: 25, type: ['a'] }
]);
//filter out people with type b
const example = source.pipe(filter(person => person.type != 'a'));
//output: "People with type b: Bob"
const subscribe = example.subscribe(val => console.log(`Type a: ${val.name}`));
the filter() you are applying takes a function with signature T => boolean meaning that you will have to return a boolean true/false so it can filter out elements from the stream.
Your elements T are of type Object {name:string, age:number, type:array} so to filter on values in the type Array you will need to use the Array.indexOf prototype function:
source.pipe(filter(person => person.type.indexOf('b') == -1) // filter out people who have type b

Resources