Loop array of IDs and fetch data by Observable - arrays

I have an array of device_ids. The array is extended dynamically by selecting items on a UI.
Initially and whenever the array is updated, I need to iterate through it and fetch data for each device_id. G
Getting data is basically a database request (to Firestore) which returns an Observable. By using switchMap I fetch some data from other database requests.
In the end I'd like to have something like an array of objects / Observables with all data I can subscribe to. My goal is using Angular's async pipe in my HTML then.
Is that possible (e.g. with RxJS)? I'm not sure how to solve this...
This is how my code currently looks like:
// Init
devices$: Observable<any;
// Array of device_ids
device_ids = ['a', 'b'];
// Get device data. This will initially be called on page load.
getDeviceItemData(device_ids) {
// Map / loop device_ids
device_items.map(device_id => {
// getDeviceById() returns an Observable
return this.getDeviceById(device_id).pipe(
// Switch to request additional information like place and test_standard data
switchMap(device => {
const place$ = this.getPlaceById(device['place_id]');
const test_standard$ = this.getTestStandardById(device['test_standard_id]');
// Request all data at the same time and combine results via pipe()
return zip(place$, test_standard$)
.pipe(map(([place, test_standard]) => ({ device, place, test_standard })));
})
).subscribe(device_data => {
// Do I need to subscribe here?
// How to push device_data to my Observable devices$?
// Is using an Observable the right thing?
});
});
}
// Add device
addDevice(device_id) {
// Add device_id to array
this.device_ids.push(device_id);
// Fetch data for changed device_ids array
getDeviceItemData(this.device_ids);
}
Preferred HTML / Angular code by using async pipe:
<div *ngFor="device of devices$ | async">{{ device.name }}</div>
Thank you for any help!

This is definitely possible with rxjs. You are on the right path. I've made a small modification to your getDeviceItemData() method.
Once the device info is retrieved by id, then you can use forkJoin to execute the two calls to get place and test_standard data parallelly and then map over that data to return the object that you need which has info of device, place and test_standard as an observable. And since we're mapping over device_ids, it'll produce an array of observables containing the required objects so that you can easily club them with async pipe.
Please note: You do not have to subscribe to the devices$ because the async pipe will automatically do that for you.
Please see the below code:
// Init
devices$: Observable<{ device: any, place: any, test_standard: any }>[];
// Array of device_ids
device_ids = ['a', 'b'];
getDeviceId: (x) => Observable<any>;
getPlaceById: (x) => Observable<any>;
getTestStandardById: (x) => Observable<any>;
getDeviceItemData() {
this.devices$ = this.device_ids.map((id) =>
this.getDeviceId(id).pipe(
switchMap((device) => {
return forkJoin([
this.getPlaceById(device['place_id']),
this.getTestStandardById(device['test_standard_id'])
]).pipe(map((y) => ({ device, place: y[0], test_standard: y[1] })));
})
)
);
}
In your HTML, you'll have to do:
EDIT: Since devices$ is an array of observables, we need to iterate over individual observables and apply async pipe on every observable.
<div *ngFor="device of devices$">
<div *ngIf="device | async as device">
<div>{{ device.device.name }}</div>
<div>{{ device.place}}</div>
<div>{{ device.test_standard }}</div>
</div>
</div>

With forkJoin you can wait for all observables to complete. Alternatively you could use combineLatest which will give you the latest data combination, every time one observable emits data. This will cause more events, but will not wait for completion of all.
getDeviceItemData(device_ids) {
const arrayOfObservables = device_ids.map(device_id => {
return this.getDeviceById(device_id).pipe(...);
}
return combineLatest(arrayOfObservables); // Observable<Data[]>
}

Related

How do I partition an observable of an array of values in one step still utilizing async pipe?

I receive an observable containing an array of objects. I want to partition the array into two based on a property of each object. I'd like to do this in such a way that the resulting arrays are in observables that can use the async pipe in the template, rather than having to manually manage subscriptions. I'm using RxJS 6 with Angular 9.
The trouble I'm having is in trying to partition based on individual elements while returning arrays to the destructuring assignment. I have tried variations of
public groupA: Observable<IItem[]>;
public groupB: Observable<IItem[]>;
...
// assigned values are not arrays
[this.groupA, this.groupB] = partition(
this.service.item$.pipe(
// split array into stream of its elements
concatAll(),
),
(item: IItem) => belongsInGroupA(item.myGroup)
);
and
[this.groupA, this.groupB] = partition(
this.service.item$,
(items: IItem[]) => {
return ???;
// How do I partition each item without first splitting into individual elements?
}
);
StackBlitz demo
I know I could build the two arrays using something like map (or, more appropriately, tap), but I don't know how to auto-(un)subscribe using async with that, if it's even possible.
partition seems intended to divide streams of one item at a time, but my need feels like something well within the domain of Angular+RxJS and I just lack the understanding. Can you help?
private version1() {
[this.groupCabbages, this.groupKings] = partition(
new Observable((observer) => {
source.subscribe((res: IItem[]) => {
for(let i = 0; i< res.length; i++) {
observer.next(res[i]);
}
observer.complete();
})
}),
(item: IItem) => {
return item.myGroup === 'cabbages';
}
);
this.groupCabbages = this.groupCabbages.pipe(toArray());
this.groupKings = this.groupKings.pipe(toArray());
// elements are correctly sorted, but streamed one at a time
this.groupCabbages.subscribe(res => {
console.log('v1 cabbages', res);
});
this.groupKings.subscribe(res => {
console.log('v1 kings', res);
});
}
Working Stackblitz :- https://stackblitz.com/edit/rxjs-cazbuc?devtoolsheight=33&file=index.ts
You can transform your array elements to observable emits by mergeMap, then groupBy the property, and emit an arrays for each group.
Something like this perhaps:
private versionZ() {
source
.pipe(
mergeMap((items: IItem[]) => from(items)),
groupBy((item: IItem) => item.myGroup),
mergeMap((group: Observable<any>) => group.pipe(toArray()))
)
.subscribe(t => console.log(t));
}
Result:
You just have to apply toArray() operator
this.groupCabbagesForView = this.groupCabbages.pipe(toArray())
// in view
groupCabbagesForView | async
then it is consumable by the view async pipe. View will get the list of items instead of item by item.
RxJS partition is used to split observable streams. You on the other hand need to create two different streams based on a condition applied to elements of each notification of the stream. If the number of elements in each emission is fairly limited and if you need to create only a limited number of observables, I'd say it'd be quicker to create multiple observables individually using the map operator.
Controller
ngOnInit() {
this.cabbages$ = this.source$.pipe(
map((items: IItem[]) =>
items.filter((item: IItem) => item.myGroup === 'cabbages')
)
);
this.kings$ = this.source$.pipe(
map((items: IItem[]) =>
items.filter((item: IItem) => item.myGroup === 'kings')
)
);
}
Template
<ng-container *ngIf="(cabbages$ | async) as cabbages">
Cabbages:
<div *ngFor="let cabbage of cabbages">
{{ cabbage | json }}
</div>
</ng-container>
<br>
<ng-container *ngIf="(kings$ | async) as kings">
Kings:
<div *ngFor="let king of kings">
{{ king | json }}
</div>
</ng-container>
Working example: Stackblitz
Note: Each async pipe would trigger a separate subscription to the source$ observable stream. If too many async pipes are needed it might lead to performance issues.
If I understand your issue correctly, then the essence is that you want to obtain 2 observables with values derived from a single observable. The general way to do this would be to multicast the original, then use that to create the derived observables.
temp$ = obs1$.pipe(share());
obs2$ = temp$.pipe(map(value => ...));
obs3$ = temp$.pipe(map(value => ...));
So to apply this to your example:
temp$ = this.service.item$.pipe(
map(arr => splitArr(arr, belongsInGroupA));
// splitArr returns [arrA, arrB]
);
this.groupA = temp$.pipe(map(value => value[0]));
this.groupB = temp$.pipe(map(value => value[1]));
The remaining question is how to efficiently split the array, but this is normal array manipulation. If a single-pass is important then use reduce.

Type script and Azure POI values

I am new to React/Type script
trying to figure out how I can access the value of the variable data2 for my return statement
How I can add the results to a collection and have the entire collection available outside of the block. So maybe like a dataSource collection.
Want to be able to return dataSource collection to the calling function.
const getAllLocations = (query: string): GeoJSON.FeatureCollection<GeoJSON.Point> => {
let data2: GeoJSON.FeatureCollection<GeoJSON.Point> = {};
let dataSource = {};
searchURL
.searchPOI(atlasService.Aborter.timeout(10000), query, {
maxFuzzyLevel: 4,
view: "Auto",
})
.then((results) => {
data2 = results.geojson.getFeatures();
console.log("inside");
console.log(data2);
dataSource.Add(data1);
});
console.log("outside");
console.log(data2);
console.log(datasource);
return data2, datasource; // Both these are empty
};
Not sure of the purpose of the dataSource in your code. You add data1 to it, but that's undefined in your code. Note that there is a DataSource class in Azure Maps for use with the map. If you are trying to use that, you should create that outside of the function and reuse it. Here is a good example: https://azuremapscodesamples.azurewebsites.net/index.html?sample=Load%20POIs%20as%20the%20map%20moves
The console log statements after 'outside' will have no data since the data hasn't been loaded yet. Calling any service is an asynchronous process. Azure Maps makes use of promises (then) as ways to notify you when the service has responded. Your current code is trying to return the data synchronously and that will never happen. The same would be true if you tried making a fetch call (exact same principal). What you can do is have your function return a promise and then in the code you use to call this function, handle it accordingly, or add your post processing code in the "then" function.
One way is to add it to the data source in the "then" function. Once it is added there, it will be available in the rest of your app. For example:
const getAllLocations = (query: string): GeoJSON.FeatureCollection<GeoJSON.Point> => {
let data2: GeoJSON.FeatureCollection<GeoJSON.Point> = {};
searchURL
.searchPOI(atlasService.Aborter.timeout(10000), query, {
maxFuzzyLevel: 4,
view: "Auto",
})
.then((results) => {
data2 = results.geojson.getFeatures();
console.log("inside");
console.log(data2);
//Do something with the response.
});
};
Functions in JavaScript (and most programming languages) can only return a single result. If you want to return multiple results you need to wrap them into a single object. For example:
return {
data2: data2,
dataSource: dataSource
};

Merging observables and get the stream in one subscribe

I have an end point that returns a list of favorites, then when this list returns i get each of these favorites and send to another endpoint to get the specific information of each of these favorite:
this.favoriteData = [];
const observables = [];
favoriteList.forEach(favorite => {
observables.push(this.assetService.getAsset(favorite.symbol)
.pipe(takeWhile((response: AssetModel) => response.id !== 0)));
});
merge(...observables)
.subscribe(res => {
this.favoriteData.push(res);
this.showLoading = false;
});
As you can see the getAsset() function calls an endpoint, and it is inside an forEach, and im saving each of these response inside an array and spread this array inside a merge function of RXJS, but when i subscribe to the merged observable and append the response to the array favoriteData, the subscribe function behavior is like, binding one by one of the response data:
i want a behavior that waits all the requests to complete and then get all of the responses in one stream, im trying to use the forkJoin operator but the tslint tells that is deprecated and to use map operator, and i dont know how to use it to do what i want
You could use a combineLatest instead, which waits for all observables to complete before emitting.
combineLatest(...observables)
.subscribe(res => {
this.favoriteData.push(res);
this.showLoading = false;
});
You can use from to generate an Observable which emits each element of the base array one at a time. Then, using mergeMap and toArray, you emit all required data at the end:
const favoriteData$ = from(favoriteList).pipe(
mergeMap(favorite => this.assetService.getAsset(favorite.symbol)
.pipe(takeWhile((response: AssetModel) => response.id !== 0))),
toArray()
);

Is it possible to make ng-repeat delay a bit redrawing of array content

I'm using ng-repeat to (guess) put array content in table.
Content is drawn dynamically, and it works well, when I'm modifying single elements of an array. But when I reload a whole array, there is this moment, when array is reassigned with new value, and ng-repeat draws blank table (which is actually logically correct). Is there a way to delay redrawing of content that way, the ng-repeat ignores the moment when the array is empty? Like the content is switched to new content without the 'clear' time.
I'm assigning new elements to array this way:
items = newItems;
where items is the array ng-repeat uses and newItems is an array of items freshly downloaded from database. The newItems is complete, when the assignment occurres. I'm not doing items = []; before the assignemt.
I'm usign angular 1.3
EDIT:
the ng-repeat:
<tr ng-repeat="order in submittedOrders">
stuff
<\tr>
js:
`$scope.reloadView = function() {
$scope.submittedOrders = OrdersService.getOrdersByStatus(ORDER_STATUS.submitted);
};`
Can it be the that the table is cleared in the first place, before call to database(service takes data from database) and during the wait, the table is cleared?
You may have to make use of Observables and async pipe of Angular.
Here are few steps you can take:
Convert your newItems to a rxjs Subject.
newItems$ = new Subject();
Whenever you get new values for your array, emit them via subject.
this.newItems$.next(newItems);
Make the items an observable of newItems$, and filter out empty arrays.
items = this.newItems$.pipe(
filter((a:any[]) => {
return a.length != 0;
})
);
In your template, use async pipe to iterate over array.
*ngFor="item of items | async"
Below is relevant parts of code that can get you started.
import { Observable, of, from, Subject } from 'rxjs';
import { filter, mapTo } from 'rxjs/operators';
...
newItems$ = new Subject();
items = this.newItems$.pipe(
filter((a:any[]) => {
return a.length != 0;
})
);
...
// A test method - link it to (click) handler of any div/button in your template
// This method will emit a non-empty array first, then, after 1 second emit an empty
// array, and then, after 2 seconds it will emit a non-empty array again with updated
// values.
testMethod() {
this.newItems$.next([3,4,5]);
setTimeout((v) => {
console.log("Emptying the array - should not be displayed browser");
this.newItems$.next([]);
}, 1000);
setTimeout((v) => {
console.log("Updating the array - should be displayed in browser");
this.newItems$.next([3,4,4,5]);
}, 2000);
}

Displaying data from Firebase in React without arrays

I am new to both React and Firebase. I struggled a bit to get data from the database, even though the instructions on the Firebase website were pretty straightforward.
I managed to print data in the view by using this code:
Get data from DB and save it in state:
INSTRUMENTS_DB.once('value').then(function(snapshot) {
this.state.instruments.push(snapshot.val());
this.setState({
instruments: this.state.instruments
});
From Firebase, I receive and Object containing several objects, which correspond to the differen instruments, like shown in the following snippet:
Object {
Object {
name: "Electric guitar",
image: "img/guitar.svg"
}
Object {
name: "Bass guitar",
image: "img/bass.svg"
}
// and so on..
}
Currently, I print data by populating an array like this:
var rows = [];
for (var obj in this.state.instruments[0]) {
rows.push(<Instrument name={this.state.instruments[0][obj].name}
image={this.state.instruments[0][obj].image}/>);
}
I feel like there's a better way to do it, can somedody give a hint? Thanks
I user firebase a lot and mu solution is little ES6 helper function
const toArray = function (firebaseObj) {
return Object.keys(firebaseObj).map((key)=> {
return Object.assign(firebaseObj[key], {key});
})
};
I also assign the firebase key to object key property, so later I can work with the keys.
The native map function only works for arrays, so using directly it on this object won't work.
What you can do instead is:
Call the map function on the keys of your object using Object.keys():
getInstrumentRows() {
const instruments = this.state.instruments;
Object.keys(instruments).map((key, index) => {
let instrument = instruments[key];
// You can now use instrument.name and instrument.image
return <Instrument name={instrument.name} image={instrument.image}/>
});
}
Alternatively, you can also import the lodash library and use its map method which would allow you to refactor the above code into:
getInstrumentRowsUsingLodash() {
const instruments = this.state.instruments;
_.map(instruments, (key, index) => {
let instrument = instruments[key];
// You can now use instrument.name and instrument.image
return <Instrument name={instrument.name} image={instrument.image}/>
});
}
Side note:
When you retrieve you data from Firebase you attempt to update the state directly with a call on this.state.instruments. The state in React should be treated as Immutable and should not be mutated with direct calls to it like push.
I would use map function:
_getInstrumentRows() {
const instruments = this.state.instruments[0];
if (instruments) {
return instruments.map((instrument) =>
<Instrument name={instrument.name}
image={instrument.image}/>);
}
}
In your render() method you just use {_getInstrumentRows()} wherever you need it.

Resources