Here's the deal: I have a HTTP get request that returns a JSON list of objects. Using RxJS I subscribe to receive the data of that list. Now, for each of the objects in that list I want to perform another HTTP request and then place the results of that request in an Array.
So far I've been able to do this but I can't seem to figure out how to maintain the order of the initial list with data. This probably has to do with the fact that the whole Observable mechanism is asynchronous.
Here's my code:
ngOnInit(): void {
this.shiftInformationService.getShifts("2016-11-03T06:00:00Z", "2016-11-06T06:00:00Z")
.subscribe(shifts => {
shifts.forEach(shift => {
this.addDataToAreaChart(shift.startDateTime, shift.endDateTime, shift.description);
});
});
}
addDataToAreaChart(startDate: string, endDate: string, description: string) {
this.machineStatisticsService
.getCumulativeMachineStateDurations(startDate, endDate)
.subscribe(s => {
this.areaChartData = [];
this.areaChartData.push(new AreaChartData(startDate, endDate, description, s));
});
}
What I want is to maintain the order of calling from the shifts.forEach loop when pushing the data in the areaChartData array.
Any ideas? Help would be appreciated!
UPDATE: SOLVED!
Final code:
ngOnInit(): void {
var startDate = new Date();
startDate.setDate(startDate.getDate() - 3);
this.shiftInformationService.getShifts(DateUtil.formatDate(startDate), DateUtil.formatDate(new Date()))
.subscribe(shifts => {
Observable.from(shifts)
.concatMap((shift) => {
return this.machineStatisticsService
.getCumulativeMachineStateDurations(shift.startDateTime, shift.endDateTime)
.map((result) => {
return {
"addDataToAreaChartValue": result,
"shift": shift
}
});
})
.subscribe(s => {
this.areaChartData = [];
this.areaChartData.push(
new AreaChartData(
s.shift.startDateTime,
s.shift.endDateTime,
s.shift.description + ' ' + s.shift.startDateTime.slice(5, 10),
s.addDataToAreaChartValue
)
);
});
});
}
Thanks to Michael!
Use concatMap to process in sequence.
Projects each source value to an Observable which is merged in the output Observable, in a serialized fashion waiting for each one to complete before merging the next.
Use map to append/transform value in observable.
Applies a given project function to each value emitted by the source Observable, and emits the resulting values as an Observable.
So, you need to do this
ngOnInit(): void {
this.shiftInformationService.getShifts("2016-11-03T06:00:00Z", "2016-11-06T06:00:00Z")
.subscribe(shifts => {
Rx.Observable.from(shifts) // create observable of each value in array
.concatMap((shift) => { // process in sequence
return this.addDataToAreaChart(
shift.startDateTime,
shift.endDateTime,
shift.description
).map((result) => {
return {
"addDataToAreaChartValue" : result, // addDataToAreaChart result
"shift": shift // append shift object here, so we can access it on subscribe
}
});
})
.subscribe(s => {
//this.areaChartData = []; // why??
this.areaChartData.push(
new AreaChartData(
s.shift.startDate,
s.shift.endDate,
s.shift.description,
s.addDataToAreaChartValue
)
);
});
});
}
addDataToAreaChart(startDate: string, endDate: string, description: string) {
return this.machineStatisticsService
getCumulativeMachineStateDurations(startDate, endDate);
}
Related
I am new to rxjs and am trying to do two requests. When I try to see the result, I get Observable.
copy() {
const obj = {};
this.create(skill)
.pipe(
mergeMap((res) => {
return [res, forkJoin(this.levels.map((level) => this.level(level)))];
}),
)
.subscribe((res) => {
console.log(res);
});
}
level(level) {
return this.create(level);
}
Output:
object of created skill,
Observable {_isScalar: false, _subscribe: ƒ}
I get the response of the first request normally and the second one comes to me as "Observable".
I'm not completely sure I understand what you're trying to do :-)
The function you pass to mergeMap() should "usually" return an observable. Currently, you are returning an array.
When you return array, mergeMap will simply emit each array element; which is why you receive those two emissions created skill, Observable.
However, if you return Observable, mergeMap will subscribe to it and emit.
I think this could work for you:
copy() {
this.create(skill).pipe(
mergeMap(createdSkill => forkJoin(this.levels.map(l => this.level(l))).pipe(
map(createdLevels => ([createdSkill, createdLevels]))
)
)
.subscribe(
([skill, levels]) => console.log({skill, levels})
);
}
It might be easier to follow if we break it down into smaller chunks:
createLevels() {
return forkJoin(this.levels.map(l => this.level(l));
}
copy() {
this.create(skill).pipe(
mergeMap(createdSkill => createLevels()).pipe(
map(createdLevels => ([createdSkill, createdLevels]))
)
)
.subscribe(
([skill, levels]) => console.log({skill, levels})
);
}
Looking it this way seems like we could instead build the copy() method in a simpler way:
copy(skill) {
forkJoin(
this.createSkill(skill),
this.createLevels()
)
.subscribe(
([skill, levels]) => console.log({skill, levels})
);
}
ForkJoin might not be the optimal operator here. I suggest having a look at the operator decision tree. I believe you want something like this though?
copy() {
const obj = {};
forkJoin(
[
this.create(skill),
...this.levels.map((level) => this.level(level))
]
).subscribe((res) => {
console.log(res);
});
}
level(level) {
return this.create(level);
}
I use Subject in my app in order to make CRUD operations. I subscribe to it in a couple of components, but it doesn't seem to change my values in real time. I have to resort to reloading the page to get the most recent values. This is probably due to my lack of understanding of how everything should be organised, but I haven't found any simmilar issues here. Could you please help me fix what I do wrong?
(using BehaviourSubject yields the same results).
Here is the code for my service:
#Injectable({ providedIn: 'root' })
export class AppBookshelfService {
filesAndFolders: ItemFile[] = [];
filteredArrOfFolders = [];
filesSubj = new Subject();
curentParent = 0;
getFiles() {
this.http.get<any>('http://localhost:3000/api/items').subscribe(
(data: any) => {
this.filesAndFolders = data.items;
this.filesAndFolders = data.items.filter(item => item.isDeleted === 0);
this.filesSubj.next(data.items.filter(item => item.isDeleted === 0));
data.items.forEach(item => {
if(item.parentId !== 0 && item.isFolder === 1) {
this.filteredArrOfFolders.push(item);
}
}); }
)
}
getFile(id: number): Observable<ItemFile> {
return this.http.get<ItemFile>("http://localhost:3000/api/items/" + id);
}
emitIdForFile(parentId: number) {
this.curentParent = parentId;
}
getCurentParent() {
return this.curentParent;
}
postFile(item: ItemFile) {
return this.http.post<ItemFile>("http://localhost:3000/api/items/", item).subscribe(result => {})
}
Here is the code of one of my key components, that does the subscribing:
ngOnInit(): void {
this.bookshelfService.getFiles();
this.sub = this.bookshelfService.filesSubj.subscribe(data => {
this.files = data;
console.log(this.files)
let groupOfCheckboxes = {};
this.files.forEach(element => {
groupOfCheckboxes[element.id.toString()] = new FormControl("");
});
this.checkboxForm = new FormGroup(groupOfCheckboxes);
});
// this.getFolders();
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
startCreatingFolder() {
this.isFolderBeingCreated = !this.isFolderBeingCreated;
}
createFile(id: number) {
this.bookshelfService.emitIdForFile(id);
this.router.navigate(['create']);
}
createFolder(event, id) {
const folder = {
name: this.folderNameInput.nativeElement.value,
description: "",
imageLink: "",
isDeleted: 0,
parentId: id,
isFolder: 1
}
this.bookshelfService.filesAndFolders.push(folder);
this.bookshelfService.getFiles();
this.bookshelfService.postFile(folder);
this.isFolderBeingCreated = false;
}
Here is a link to the full repository:
https://github.com/Not-a-whale/BookshelfApp
Here is the app working on Heroku with the said flaw:
https://bookshelf-app-nikita.herokuapp.com/
The subject is Hot(Multicast), You Should subscribe to it before emit occurs:
ngOnInit(): void {
this.sub = this.bookshelfService.filesSubj.subscribe(data => {
this.files = data;
console.log(this.files)
let groupOfCheckboxes = {};
this.files.forEach(element => {
groupOfCheckboxes[element.id.toString()] = new FormControl("");
});
this.checkboxForm = new FormGroup(groupOfCheckboxes);
});
this.bookshelfService.getFiles();
// this line must be after subscribe
// this.getFolders();
}
An Observable is cold when data is produced inside the Observable and the Observable is hot when the data is produced outside the Observable. As we just saw the hot Observable is able to share data between multiple subscribers. We call this behaviour “multicasting”.
Generating a random number is not a good real life usecase. A good usecase would be DOM events. Let’s say we’re tracking clicking behaviour and have multiple subscribers do something with the coordinates:
The data is produced outside of the Observable itself. Which makes it hot, because the data is being created regardless of if there is a subscriber or not. If there is no subscriber when the data is being produced, the data is simply lost.
More Info
If you want to save data_list in memory and then use it multiple times use a behaviorsubject to hold it, subject has no memory to hold the events.
behaviorsubject needs a default value so you must initialize it with an empty list.
When you call service just next the behaviorsubject with new data, then subscribe to it before service call to receive the changes events
// RxJS v6+
import { BehaviorSubject } from 'rxjs';
const subject = new BehaviorSubject(123);
// two new subscribers will get initial value => output: 123, 123
subject.subscribe(console.log);
subject.subscribe(console.log);
// two subscribers will get new value => output: 456, 456
subject.next(456);
// new subscriber will get latest value (456) => output: 456
subject.subscribe(console.log);
// all three subscribers will get new value => output: 789, 789, 789
subject.next(789);
// output: 123, 123, 456, 456, 456, 789, 789, 789
If you look at the picture both arrays consist of same kind of object. first I create it with empty data as placeholder, but second one I create it with data coming from server.
writeValue(v: any) {
console.log('aaa');
console.log(v);
console.log('aaa');
this.form = new FormArray([]);
for (const value of v) {
console.log('bbb');
console.log(value);
console.log('bbb');
this.form.push(new FormControl(value));
}
this.form.valueChanges.subscribe(res => {
if (this.onChange) {
this.onChange(this.form.value);
}
});
}
for first case it goes through all of the writeValue code, for second one it doesn't go through the for(const values of v) code. why is this happening? when I print them out they seem to be the same other than one difference [{...}] vs [] in browser tools.
If you want to see how I create them. the first one is routes and the second one is routeslocal. I put them in angular formcontrol, and thats how it gets to writeValue via controlvalueaccessor. If you want to know how it works you could check my previous question here. there is more code, but it doesn't include the service.
ngOnInit() {
const routes: any[] = [];
routes.push({ ...dataI });
this.requestForm = this.fb.group({
statusId: null,
requestVehicles: this.fb.array([
this.fb.group({
garageId: 0,
routes: new FormControl(routes),
endDateTime: 0,
})
])
});
if (this.data.isEdit) {
this.Title = 'Edit';
this.data.fService.getRequest(this.data.requestId).subscribe(thisRequest => {
this.requestForm = this.fb.group({
statusId: thisRequest.status,
requestVehicles: this.fb.array([
])
});
thisRequest.requestVehicles.forEach((element, index) => {
const routeslocal: any[] = [];
element.routes.forEach((elementt, indexx) => {
this.data.fService.getAddressPoint(elementt).subscribe(sbed => {
const newRoute = {
addressPointId: sbed.addressPointId,
municipalityId: sbed.municipalityId,
regionId: sbed.regionId,
rvId: element.rvId,
sequenceNumber: indexx,
settlementId: sbed.settlementId,
regionName: sbed.regionName,
municipalityName: sbed.municipalityName,
settlementName: sbed.settlementName,
description: sbed.description,
};
routeslocal.push({...newRoute});
});
});
this.requestVehicles.push(this.fb.group({
endDateTime: new Date(element.endDateTime),
garageId: element.garageId,
routes: new FormControl(routeslocal),
}));
});
});
});
});
}
}
The opening line, [] or [{}], is immediately drawn in the console.
In the case of [], there was nothing in the array at logging time, so the browser draw it as an empty array. But the data was present when you looked at it and clicked on the small triangle, later.
You can reproduce this behavior with this code in your console:
;(function(){ let arr=[]; setTimeout(()=>{ arr[0] = {b:3}; }); return arr;})()
So the difference you saw is related to the (a)synchronicity of array filling.
Vato, you has two functions in your service:getRequest(requestId) and getAddressPoint(requestVehicles). The idea is return a whole object. You can create the function in the own service or in the component. I'd like in the service, and that return an objservable. You must use forkJoin and swithMap So . It's for me impossible check if work
**Update, see the stackblitz
getFullRequest(id): Observable<any> {
return this.getRequest(id).pipe(
switchMap((request: any) => {
//here you has the request. We create an array of observables
return forkJoin(
request.requestVehicles.map(
(r: any) => this.getAddressPoint(r))).pipe(map((res: any[]) => {
res.forEach((x: any, index: number) => {
x.sequenceNumber = index
})
return {
statusId: request.statusID,
routes: res
}
})
)
}))
}
then, in your component
if (this.data.isEdit) {
this.Title = 'Edit';
this.data.fService.getFullRequest(this.data.requestId).subscribe(thisRequest => {
this.requestForm = this.fb.group({
statusId: thisRequest.status,
requestVehicles: thisRequest.routes
});
Update 2 briefly explain about switchMap and forkJoin.
When we make this.getRequest(id) we received in request an object. In this object we has in requestVehicles an array (can be an array of objects or an array of numbers -or strings-). With each element of this array we can make a call, But instead of make the calls one to one, we want to make all these together. For this we use forkJoin. forkJoin received an array of observables and, in subscribe received the response in an array
//if we has an observable like:
getValue(id:number):Observable<any>{
return of({one:id})
}
//and an array like
myArray=[1,2]
//and an array of response whe we can store the responses
response:any[]
//we can do
for (let id of myArray)
{
this.getValue(id).susbcribe(res=>{
this.response.push(res)
})
}
//or
observables:any[]
for (let id of myArray)
{
this.observables.push(this.getValue(id))
}
forkJoin(this.observables).subscribe((res;any[])=>{
//in res[0] we have the result of this.getValue(1)
//in res[1] we have the result of this.getValue(2)
//so, simply
this.response=res
})
//or in a compact way
//with each element of the array
observables=myArray.map(x=>this.getValues(x))
forkJoin(this.observables).subscribe((res;any[])=>{
this.response=res
})
Well, there are two problems more. We want add a new propertie "sequenceNumber" to all the response. So we use res.forEach(...) to add the property. And we want return an object with somes properties of our original request (statusID) and in "routes" the array with the response. So we use map to transform the response. In our simple example above
//not return simple {one:1}
//return {id:1,one:1}
getResponse(2).pipe.map(res=>{
return {
id:1,
one:res.one
}
}
I'm trying to retrieve data from my service function, but am running into issues. My LandingPage component code, shown below, sends a keystroke to my service function, which then returns an object full of data.
But I cannot get the service object to return to my LandingPage. Here is how I create my service:
task.service.ts
addKey(keystroke) {
const query = "https://api.themoviedb.org/3/search/tv?api_key=";
fetch(query + key + '&language=en-US&query=' + keystroke)
.then((show) => {
show.json().then((obj) => {
// grab the items we want from the response
let resultItems = obj.results.map((show, index) => {
return {
id: show.id,
poster: show.poster_path,
rating: show.vote_average,
backdrop: show.backdrop_path,
};
});
// return our newly formed object
return { data: resultItems }
});
});
}
Here is where I am trying to receive the service data, in my:
landingpage.component.ts
getKey(keystroke) {
this.TaskService.addKey(keystroke)
.subscribe(res => {
this.shows = res.data; // trying to receive service info here
});
}
When I try to build, I receive the following error in my LandingPage component:
Property 'subscribe' does not exist on type 'void'.
I've tried using map instead of subscribe, but it returns a similar error.
How can I send the object result from my service, to my component?
It looks like you're missing a return in your service method, I've also changed to using http from our discussion in the comments:
addKey(keystroke): Observable<any> {
const query = "https://api.themoviedb.org/3/search/tv?api_key=";
return this.http.get(query + key + '&language=en-US&query=' + keystroke)
.map(show => {
show.json().then((obj) => {
// grab the items we want from the response
let resultItems = obj.results.map((show, index) => {
return {
id: show.id,
poster: show.poster_path,
rating: show.vote_average,
backdrop: show.backdrop_path,
};
});
// return our newly formed object
return { data: resultItems }
});
});
}
If you really want to use fetch you can if you:
Set the method return signature to Promise<any>
Use then instead of map in the service
Use then instead of subscribe in the component
I have a chain of observables which follow this logic:
getStyles() --> getPrices()
For every config.id in configs, getStyles() returns a style Object, this style Object is passed to getLease where a price is appended to it and is then pushed an array called "style"
getStyles(configs: any) {
configs.forEach(config => {
this._APIService.getStyleByID(config.id).subscribe(
res => {
res.config = config;
this.getLease(res);
}
);
});
}
getLease(style: any): void {
this._priceService.getPrice().subscribe(
price => {
style.price = price;
this.garage.push(style);
console.log(this.garage);
});
}
}
The issue I am experiencing is that there is a duplicate call being made because the style array has 2x as many style objects as it needs to have. The issue being that Angular is calling this._APIService.getStyleByID() twice when I expect it to call once. How do I fix my Observables to only be called once?
Try switchMap, it's like mergeMap but it cancels the last request if it changes:
getStylesWithoutYear(): void {
this._APIService.getStylesWithoutYear()
.switchMap(styles => {
styles.years.forEach(year => {
year.styles.forEach(style => {
this._APIService.getStyleByID(style.id)
})
})
})
.subscribe(style => {
console.log('style', style)
})
}