rxjs: recursive Observable emission inside map - javascript

I am pulling my hair out a little with attempting to group data recursively in rxjs. There seems to be alot of good examples around with different use cases but I cant seem to refactor the code around to fit my requirements.
The central problem that I can infer is that in my map operator I have a conditional return which is either a non-observable or an observable. Is there a way I can refactor to account for this discrepency?
Ideally this function would group an original "flat" array by an arbitrary amount of columns that are passed in as the cols argument.
Each time the criteria is satisfied it would
append to an array in the format
{ key: col_name, elements : [...col_elements] } where elements would be another array of elements in the same format, or a straight list of elements.
The below function works when only grouping the columns once ( and thus never requiring the observable within map to emit ).
//group data n times based on passed string[] of column attributes
group_data(elements: Observable<any>, cols: string[], index=0) : Observable<any> {
let col = cols[index]
return elements.pipe(
//groupby column value
RxOp.groupBy((el:any) => this.get_groupingValue(el, col)),
//place key inside array
RxOp.mergeMap((group) => group.pipe(
RxOp.reduce((acc, cur) => [...acc, cur], ["" + group.key]))
),
// map to key:group
RxOp.map((arr:any) =>
cols.length <= index + 1?
// no more grouping req
{ 'key': arr[0], 'elements': arr.slice(1) } :
//group recursively, returns Observable instead of array:(
{ 'key': arr[0], 'elements':
this.group_data(from(arr.slice(1)), cols, index + 1)
.pipe(
RxOp.tap(t=> console.log(t)), // not logged
RxOp.reduce((acc, val) => [...acc, val], [])
)
}),
RxOp.toArray()
)
}
//simplified data example:
data = [{id: 'idA', type: 'project', parents: null },
{id: 'idB', type: 'project', parents: null },
{id: 'idC', type: 'episode', parents: ['idA'] },
{id: 'idD', type: 'episode', parents: ['idB'] },
{id: 'idE', type: 'scene', parents: ['idA', 'idC'] },
{id: 'idF', type: 'scene', parents: ['idB', 'idD'] }]
// 1 column passed works correctly as below
group_data(elements: from(data), ['project'])
/* outputted data:
[{key: 'idA',
elements: [ {id: 'idC', type: 'episode', parents: ['idA'] },
{id: 'idE', type: 'scene', parents: ['idA', 'idC'] }]},
{key: 'idB',
elements: [ {id: 'idD', type: 'episode', parents: ['idA'] },
{id: 'idF', type: 'scene', parents: ['idA', 'idC'] }]},
{key: null,
elements: [ {id: 'idA', type: 'project', parents: [] }
{id: 'idB', type: 'project', parents: [] }]}
]
*/
// 2 columns not working correctly
group_data(elements: from(data), ['project', 'episode'])
/*[{key: 'idA',
elements: Observable},
{key: 'idB',
elements: Observable},
{key: null,
elements: Observable}
]*/

The approach i needed to take was to restructure so that I could use mergeMap instead of map - which meant i needed two observables at the condition instead of one. From there i just needed to refactor so that the mapping to key, elements was done after the mergeMap.
This is still my first foray into rxjs, so my explanation isn't great and my summary of the problem also wasnt great. Importantly, at least its behaving as expected now.
//group data n times based on passed string[] of column attributes
group_data(elements: Observable<any>, cols: string[], index=0) : Observable<any> {
let col = cols[index]
let grouping = elements.pipe(
//groupby column value
RxOp.groupBy((el:any) => this.get_groupingValue(el, col)),
//place key inside array
RxOp.mergeMap((group) => group.pipe(
RxOp.reduce((acc, cur) => [...acc, cur], ["" + group.key]))
)
)
return grouping.pipe(
RxOp.mergeMap((arr) =>
(
cols.length <= (index +1) ?
//no more grouping required
of(arr.slice(1)) :
//group again
this.group_data(from(arr.slice(1)), cols, index + 1))
// reduce result and put the key back in
.pipe(
RxOp.reduce((acc, cur) => [...acc, cur], ["" + arr[0]])
)
),
// map to key:group
RxOp.map(arr => ({
key: arr[0],
elements: arr.slice(1)
})
),
RxOp.toArray()
)

Related

Array of inner subscriptions with rxjs

I'm trying to improve the flattening and reduce the chaining inside my Rxjs code.
A REST call of my service getAllItems() returns the following Items[]:
[{
id: 101,
name: 'Car',
owner: 1
},
{
id: 102,
name: 'Table',
owner: 2
}]
I have another endpoint getOwnerInfo(id:number) which provides information based in the "owner" id, information which I want to combine, so the final answer should look like:
[{
id: 101,
name: 'Car',
owner: {
id: 1,
username: 'Mikelinin',
name: 'Mikel',
},
},
{
id: 102,
name: 'Table',
owner: {
id: 2,
username: 'Thominin',
name: 'Thomas',
},
}]
My current implementation .subscribes once to getAllItems call and then in the subscription body it iterates per element subscribing to getOwnerInfo.
I've been looking through other flattening examples, but basically they "break" the array, and they never put it back together. I need the output to be an array.
I've tried to use from(), concatMap(), and mergeMap() but seems I am unable to combine both requests properly.
Map your array of items into an array of observables that each emit the updated item. Then join the array of observables together.
getAllItems().pipe(
// turn item[] into observable<updatedItem>[]
map(items => items.map(item => getOwnerInfo(item.id).pipe(
map(owner => ({...item, owner}))
),
// join observable<updatedItem>[] into updatedItem[]
switchMap(itemCalls => forkJoin(itemCalls))
).subscribe(console.log);
Or you can do the mapping and joining in one step:
getAllItems().pipe(
// source item[], do stuff, emit updatedItem[]
switchMap(items => forkJoin(
items.map(item => getOwnerInfo(item.id).pipe(
map(owner => ({...item, owner}))
))
))
).subscribe(console.log);
The following will use from to convert a distinct array of owner ids into a stream of owner ids. Then it will use toArray to convert that stream back into an array and map the items array to an array of items with their respective owner object.
this.getAllItems().pipe(
switchMap(items => {
const ownerIds = Array.from(new Set(items.map(x => x.owner)));
return from(ownerIds).pipe(
concatMap(x => this.getOwnerInfo(x))
toArray(),
map(owners => items.map(x => ({ ...x, owner: owners.find(y => y.id === x.owner) })))
)
})
)

JavaScript forEach does not fully iterate over indices when remove an index

I have an array of objects, which I am iterating over, and trying to remove a specific object from the list.
Running over the array I remove the first index and iterates only 2 times instead of 3 times.
const data = [{
id: 1,
name: 'test1'
},
{
id: 2,
name: 'test2'
},
{
id: 3,
name: 'test1'
}]
data.forEach((item: any, index:any) => {
if (item.name === 'test1') {
data.splice(index, 1); // Remove one record then and it stops early
}
});
Does anyone help me that iterate the fully if removed anything during the iteration?
You're removing an index from the array while traversing the array. So the array that you're working on is being altered on each iteration.
Since you've removed index 0 from the array the length is now 2 so it stops on the second iteration.
Instead of using forEach use filter (see Stackblitz for example), which doesn't mutate the original array and returns the new array, which you could then reassign.
let data = [{
id: 1,
name: 'test1'
},
{
id: 2,
name: 'test2'
},
{
id: 3,
name: 'test1'
}];
console.log('BEFORE', data); // Original data
data = data.filter((item: any, index: any) => item.name !== 'test1');
console.log('AFTER', data); // Only `test2` remains now

Dynamic array filtering by object property

I have a react live search dropdown component that filters through an array of objects by a search term. It filters my objects by title and then returns a list of all the related objects. This works fine.
Current:
Data Structure
data: [
{ id: 1, title: 'Some title here' },
{ id: 2, title: 'Another title' },
{ id: 3, title: 'last title' },
]
Component
<LiveSearch
term={term}
data={data} />
Inside Live search component
Filter data by term and render list
return data
.filter(item => item.title.toLowerCase().includes(term.toLowerCase())
.map((item, idx) => <li key={idx}>{item.title}</li>
My objects to search by are getting more advanced and what I would like to be able to do is pass into my component an array of property names I would like to compare to the search term.
My thinking process behind it is to loop through the object properties and if on of the properties matches the term the loop breaks and returns true adding that object to the list of items to be displayed.
Goal
Data Structure
data: [
{ id: 1, country: 'Canada', title: 'Some title here' },
{ id: 2, country: 'Australia', title: 'Another title' },
{ id: 3, country: 'Netherlands', title: 'last title' },
]
Component
<LiveSearch
searchFields={['country', 'title']}
term={term}
data={data} />
Inside Component filtering
return data
.filter(item => {
// Dynamic filtering of terms here
})
.map((item, idx) => <li key={idx}>{item.title}</li>
Inside the filter I'm trying to get a loop through the array and dynamically produce logic similar to this
item.searchFields[0].toLowerCase().includes(term.toLowerCase()) ||
item.searchFields[1].toLowerCase().includes(term.toLowerCase())
But obviously could loop over an infinite number of searchfields/properties
Use Array#some()
Something like
term = term.toLowerCase()
return data
.filter(item => {
return searchFields.some(field => item[field].toLowerCase().includes(term))
}).map(...
Check if some of the searchFields match:
// Checks wether a value matches a term
const matches = (value, term) => value.toLowerCase().includes(term.toLowerCase());
// Checks wether one of the fields in the item matcues the term
const itemMatches = (fields, term) => item => fields.some(field => matches(item[field], term);
// Filter the data to only contain items where on of the searchFields matches the term
const result = props.data.filter( itemMatches(props.searchFields, props.term) );
return result.map(item => <li key={idx}>{item.title}</li>);
You can use Array .some combined with .filter
let result = data.filter(obj =>
searchFields.some(s =>
obj[s] != undefined && obj[s].toLowerCase() === term
));
let data = [
{ id: 1, country: 'Canada', title: 'Some title here' },
{ id: 2, country: 'Australia', title: 'Another title' },
{ id: 3, country: 'Netherlands', title: 'last title' },
], searchFields = ["country", "title"], term = "canada";
let result = data.filter(obj =>
searchFields.some(s =>
obj[s] != undefined && obj[s].toLowerCase() === term
));
console.log(result);

How to get JSON keys and add extra fields?

I'm trying to get the key of these json objects in order to create a new object with extra filed to create table headers in a React app. JSON data:
let example = [
{
id: 1,
city: 'New York',
},
{
id: 2,
city: 'Paris',
},
]
The function:
getKeys() {
return example.map((key) => {
return {
cityName: key, // gets the whole array
capital: false,
};
});
}
I tries Object.keys( example);, it returns integers; 0, 1.
How can I get the keys in this case? Thanks.
You are trying to map the keys for an array since example is an array. If the data are consistent throughout the array get the first element example[0] and do Object.keys().
So Object.keys(example[0])
There's no need to get the keys if you just want to add a property to the items in the array. I think there's a misunderstanding about .map, which gives you a single item/object in the array, not the keys.
Something like this perhaps?
let example = [{
id: 1,
city: 'New York',
}, {
id: 2,
city: 'Paris',
}];
const modifiedArray = function(arr) {
return arr.map(item => {
return {
id: item.id,
cityName: item.city,
capital: false,
};
})
}
const newArray = modifiedArray (example);
console.log(newArray )

Applying a property to each element in several arrays, then returning a flat map with one array

I have a collection that looks like this
[
{
count: 123,
description: 'some description',
articles: [
{...}
]
},
{
count: 234,
description: 'some description',
articles: [
{...}
]
}
]
Each object in the collection has a collection of articles. What I need is to apply the description to each article object in the respective collection in each element of the primary collection. I also want to end up with a flat array containing only articles. Clearly I'm using mergeMap incorrectly, but I'm not sure how to do it.
I have tried this
json$.pipe(
// Filter out empty arrays
filter(section => section.count > 0),
// Apply the descriptions
map(section => section.articles.map(a => (Object.assign(a, section.sectionName)))),
mergeMap(x => x.articles)
).subscribe(x => console.log(x));
But the articles do not have the description property in them, and it's not a flat array of articles. I've tried a few things but I'm unsure how to proceed
You only need to concatMap the outer observable, after adjusting each article.
const { Observable } = Rx;
const { map, concatMap, filter } = Rx.operators;
const json$ = Observable.from([
{
count: 123,
description: 'some description 123',
articles: [
{id: 1},
{id: 2},
]
},
{
count: 234,
description: 'some description 234',
articles: [
{id: 3},
{id: 4},
]
}
]);
const withDescription$ = json$.pipe(
filter(({count}) => count > 0),
concatMap(
({articles, description}) => Observable.from(articles).map(a => ({...a, description}))
)
);
withDescription$.subscribe(console.log);
<script src="https://unpkg.com/#reactivex/rxjs#^5/dist/global/Rx.min.js"></script>
If you don't need any special behavior on the inner observable, you could simplify to:
const withDescription$ = json$.pipe(
filter(({count}) => count > 0),
concatMap(
({articles, description}) => articles.map(a => ({...a, description}))
),
);

Categories

Resources