Angular 5 Subscribe to an Observable inside SwitchMap - javascript

I'm trying to subscribe to an inner Observable. Here is what I'm doing:
getDates() {
if (this.booking.id) {
this.jobService.findById(this.booking.id)
.map(job => job[0])
.switchMap((job) => {
return job ? this.bookingService.findByPropertyId(job.property.id) : Observable.empty();
}, (job: Job, bookings: Booking[]) => {
this.mark_jobs_unavailable(job, bookings);
return job;
})
.subscribe(job => {
this.bookingJob = job;
});
}
}
My code is working fine, The only thing I'm trying to achieve is to subscribe to changes of my Bookings as well. Currently as I'm not sure where should I subscribe to the Observable that I get from bookingService.findByPropertyId, My app does not reflect to changes happening in bookings collection.

To get changes for Bookings, bookingService.findByProperyId() must return observable values whenever (i.e. it can return multiple values over a period of time) a change happens.

You can try something like this:
this.jobService.findById(this.booking.id)
.mergeAll() // or .mergeAll().first()
.switchMap(
job => this.bookingService.findByPropertyId(job.property.id),
(job, bookings) => [job, bookings]
)
.subscribe(([job, bookings]) => {
this.bookingJob = job;
this.mark_jobs_unavailable(job, bookings);
});
Now you have both job and bookings in the subscribe method

Related

ForEach wait for Observable Subscription

I have this array orderCodes that has the codes for specific orders, and then with the code I can get the details of that order (each order has multiple products), and I need to extract the code of each product inside the order details.
The getOrderDetails()is an Observable with results(an array of the products), and each resulthas a code which is what I need.
this.orderCodes.forEach((orderCode) => {
loadOrderDetails(orderCode);
getOrderDetails().subscribe((order: any) => {
if (order.results) {
order.results.map((result) => {
console.log(result.code);
});
}
});
});
I've tried with this forEach but since I'm subscribing to the Observable the forEach skips to the next iteration and I need it to wait
Any ideas?
rxjs way would be
from(this.orderCodes).pipe(
concatMap((orderCode) => // concatMap operator makes your items come "in order" one after another
defer(() => {
loadOrderDetails(orderCode);
return getOrderDetails();
}))
).subscribe((order: any) => {
if (order.results) {
order.results.map((result) => {
console.log(result.code);
});
}
});
or you could convert to promises and use async await (more elegant, but usually less prefered way in angular because of converting to promises and change detection issues if done wrong, but it depends...)
async myFunctionThatDoesAllThis(...) {
....
for(let orderCode of this.orderCodes) {
loadOrderDetails();
const order = await getOrderDetails().pipe(take(1)).toPromise(); // pipe(take(1)) could be skipped if getOrderDetails is just an http request.
if(order.results) {
order.results.forEach((result) => {
console.log(result.code);
});
}
}

How to make recursive HTTP calls using RxJs operators?

I use the following method in odder to retrieve data by passing pageIndex (1) and pageSize (500) for each HTTP call.
this.demoService.geList(1, 500).subscribe(data => {
this.data = data.items;
});
The response has a property called isMore and I want to modify my method in odder to continue HTTP calls if isMore is true. I also need to merge the returned values and finally return the accumulated values.
For example, assuming that there are 5000 records and until 10th HTTP call, the service returns true for isMore value. After 10th HTTP call, it returns false and then this method sets this.data value with the merged 5000 records. For this problem, should I use mergeMap or expand or another RxJs operator? What is the proper way to solve this problem?
Update: I use the following approach, but it does not merge the returned values and not increase the pageIndex. For this reason it does not work (I tried to make some changes, but could not make it work).
let pageIndex = 0;
this.demoService.geList(pageIndex+1, 500).pipe(
expand((data) => {
if(data.isComplete) {
return of(EMPTY);
} else {
return this.demoService.geList(pageIndex+1, 500);
}
})
).subscribe((data) => {
//your logic here
});
Update II:
of({
isMore : true,
pageIndex: 0,
items: []
}).pipe(
expand(data => demoService.geList(data.pageIndex+1, 100)
.pipe(
map(newData => ({...newData, pageIndex: data.pageIndex+1}))
)),
// takeWhile(data => data.isMore), //when using this, it does not work if the total record is less than 100
takeWhile(data => (data.isMore || data.pageIndex === 1)), // when using this, it causing +1 extra HTTP call unnecessarily
map(data => data.items),
reduce((acc, items) => ([...acc, ...items]))
)
.subscribe(data => {
this.data = data;
});
Update III:
Finally I made it work by modifying Elisseo's approach as shown below. Howeveri **I need to make it void and set this.data parameter in this getData() method. How can I do this?
getData(pageIndex, pageSize) {
return this.demoService.geList(pageIndex, pageSize).pipe(
switchMap((data: any) => {
if (data.isMore) {
return this.getData(pageIndex+1, pageSize).pipe(
map((res: any) => ({ items: [...data.items, ...res.items] }))
);
}
return of(data);
})
);
}
I want to merge the following subscribe part to this approach but I cannot due to some errors e.g. "Property 'pipe' does not exist on type 'void'."
.subscribe((res: any) => {
this.data = res;
});
getData(pageIndex, pageSize) {
return this.demoService.getList(pageIndex, pageSize).pipe(
switchMap((data: any) => {
if (!data.isCompleted) {
return this.getData(pageIndex+1, pageSize).pipe(
map((res: any) => ({ data: [...data.data, ...res.data] }))
);
}
return of(data);
})
);
}
stackblitz
NOTE: I updated pasing as argument pageIndex+1 as #mbojko suggest -before I wrote pageIndex++
UPDATE 2
Using expand operator we need take account that we need feed the "recursive function" with an object with pageIndex -it's necesarry in our call- for this, when we make this.demoService.getList(data.pageIndex+1,10) we need "transform the result" adding a new property "pageIndex". for this we use "map"
getData() {
//see that initial we create "on fly" an object with properties: pageIndex,data and isCompleted
return of({
pageIndex:1,
data:[],
isCompleted:false
}).pipe(
expand((data: any) => {
return this.demoService.getList(data.pageIndex,10).pipe(
//here we use map to create "on fly" and object
map((x:any)=>({
pageIndex:data.pageIndex+1, //<--pageIndex the pageIndex +1
data:[...data.data,...x.data], //<--we concatenate the data using spread operator
isCompleted:x.isCompleted})) //<--isCompleted the value
)
}),
takeWhile((data: any) => !data.isCompleted,true), //<--a take while
//IMPORTANT, use "true" to take account the last call also
map(res=>res.data) //finally is we only want the "data"
//we use map to return only this property
)
}
Well we can do a function like this:
getData() {
of({pageIndex:1,data:[],isCompleted:false}).pipe(
expand((data: any) => {
return this.demoService.getList(data.pageIndex,10).pipe(
tap(x=>{console.log(x)}),
map((x:any)=>({
pageIndex:data.pageIndex+1,
data:[...data.data,...x.data],
isComplete:x.isComplete}))
)
}),
takeWhile((data: any) => !data.isComplete,true), //<--don't forget the ",true"
).subscribe(res=>{
this.data=res.data
})
}
See that in this case we don't return else simple subscribe to the function and equal a variable this.data to res.data -it's the reason we don't need the last map
Update 3 by Mrk Sef
Finally, if you don't want your stream to emit intermittent values and you just want the final concatenated data, you can remove the data concatenation from expand, and use reduce afterward instead.
getData() {
of({
pageIndex: 1,
data: [],
isCompleted: false
})
.pipe(
expand((prevResponse: any) => this.demoService.getList(prevResponse.pageIndex, 10).pipe(
map((nextResponse: any) => ({
...nextResponse,
pageIndex: prevResponse.pageIndex + 1
}))
)
),
takeWhile((response: any) => !response.isCompleted, true),
// Keep concatenting each new array (data.items) until the stream
// completes, then emit them all at once
reduce((acc: any, data: any) => {
return [...acc, ...data.data];
}, [])
)
.subscribe(items => {
this.data=items;
});
}
It doesn't matter if you're total record change as long as api response give you the isMore flag.
I'm skipping the part how to implement reducer action event i'm assuming you've already done that part. So i will just try to explain with pseudo codes.
You have a table or something like that with pagination data. on intial state you can just create an loadModule effect or using this fn:
getPaginationDataWithPageIndex(pageIndex = 1){
this.store.dispatch(new GetPaginationData({ pageIndex: pageIndex, dataSize: 500}));
}
in your GetPaginationData effect
... map(action => {
return apicall.pipe(map((response)=> {
if(response.isMore){
return new updateState({data:response.data, isMore: responseisMore})
} else {
return new updateState({isMore: response.isMore}),
}
}})
`
all you have to left is subscribing store in your .ts if isMore is false you will not display the next page button. and on your nextButton or prevButton's click method you should have to just dispatch the action with pageIndex
I do not think recursion is the correct approach here:
interval(0).pipe(
map(count => this.demoService.getList(count + 1, 500)),
takeWhile(reponse => response.isMore, true),
reduce((acc, curr) => //reduce any way you like),
).subscribe();
This should make calls to your endpoint until the endpoint returns isMore === false. The beautiful thing about interval is that we get the count variable for free.
But if you are set on using recrsion, here is the rxjs-way to do that using the expand-operator (see the docs). I find it slightly less readable, as it requires an if-else-construct which increases code complexity. Also the outer 'counter' variable just isn't optimal.
let index = 1;
this.demoService.geList(index, 500).pipe(
expand(response => response.isMore ? this.demoService.geList(++index, 500) : empty()),
reduce((acc, curr) => //reduce here)
).subscribe();

double combineLatest doesn't emit update

In my project there are activities that people have created, joined, bookmarked or organized. I've read a lot of these question already. But most of the code was less complex or people forgot to subscribe...
I would like to get all the activities in a certain time period and then add creator information (name, image, etc) and add booleans if the user retrieving these activities has joined/bookmarked/organized this activity. The code I used before would provide live updates (ex. I join an activity, by adding my userId to the participants array and the activity.joined would update to true).
Previous code:
public getActivities(user: UserProfileModel): Observable<Array<ActivityModel>> {
const now: number = moment().startOf('day').unix();
const later: number = moment().startOf('day').add(30, 'day').unix();
return this.afs.collection<ActivityModel>(`cities/${user.city.id}/activities`, ref => ref
.where('datetime', '>=', now)
.where('datetime', '<=', later))
.valueChanges({ idField: 'id' })
.pipe(
map(activities => activities.map(activity => {
const bookmarked = activity.bookmarkers ? activity.bookmarkers.includes(user.uid) : false;
const joined = activity.participants ? activity.participants.includes(user.uid) : false;
const organized = activity.organizers ? activity.organizers.includes(user.uid) : false;
return { bookmarked, joined, organized, ...activity } as ActivityModel;
}))
);
}
The I wanted to add the creator as an observable object, so their latest changes in name or profile picture would be shown. But with this code change, my getActivities doesn't emit any updates anymore...
My new code:
public getActivities(user: UserProfileModel): Observable<Array<CombinedActivityCreatorModel>> {
const now: number = moment().startOf('day').unix();
const later: number = moment().startOf('day').add(30, 'day').unix();
return this.afs.collection<ActivityModel>(`cities/${user.city.id}/activities`, ref => ref
.where('datetime', '>=', now)
.where('datetime', '<=', later))
.valueChanges({ idField: 'id' })
.pipe(
concatMap(activities => {
const completeActivityData = activities.map(activity => {
const activityCreator: Observable<UserProfileModel> = this.getCreator(activity.creator);
const bookmarked = activity.bookmarkers ? activity.bookmarkers.includes(user.uid) : false;
const joined = activity.participants ? activity.participants.includes(user.uid) : false;
const organized = activity.organizers ? activity.organizers.includes(user.uid) : false;
return combineLatest([
of({ bookmarked, joined, organized, ...activity }),
activityCreator
]).pipe(
map(([activityData, creatorObject]: [ActivityModel, UserProfileModel]) => {
return {
...activityData,
creatorObject: creatorObject
} as CombinedActivityCreatorModel;
})
);
});
return combineLatest(completeActivityData);
})
);
}
The code has become a bit complex, that I don't see the solution myself. Anybody that can offer some assistance?
Looks like one of activityCreator doesn't emit a value, combineLatest requires all observables to emit at least once.
I would recommend you to debug how activityCreator behaves.
If it's fine that it doesn't emit you have 2 options: startWith to set a value for an initial emit, or defaultIfEmpty, it emits in case if stream is going to be closed without any emit.
activityCreator = this.getCreator(activity.creator).pipe(
// startWith(null), // for example if you want to trigger combineLatest.
// defaultIfEmpty(null), // in case of an empty stream.
);
another thing is concatMap it requires an observable to complete, only then it switches to the next one, parhaps mergeMap or switchMap fits here better.
Try the code below and add its output to the comments. Thanks.
const activityCreator: Observable<UserProfileModel> = this.getCreator(activity.creator).pipe(
tap(
() => console.log('getCreator:emit'),
() => console.log('getCreator:error'),
() => console.log('getCreator:completed'),
),
);

How to combine two observables to create new observable?

I have two services named 'PatientsService' and 'AppointmentService'. In third service 'AppointedPatientsService', I want to subscribe to AppointmentService to get all booked appointments with patientId and after that I want to repeatedly subscribe to PatientsService.getPatient(patientId) to get Patient's data with patientId. And then, I want to return new array named allAppointedPatients which holds all appointments with patient's data. I tried this...
getAppointments() {
let allAppointments: Appointment[] = [];
const allAppointedPatients: AppointedPatient[] = [];
return this.appointmentService.fetchAllAppointments().pipe(
take(1),
tap(appointments => {
allAppointments = appointments;
for (const appointment of allAppointments) {
this.patientsService.getPatient(appointment.patientId).pipe(
tap(patient => {
const newAppointment = new AppointedPatient(patient.firstName,
patient.lastName,
patient.address,
patient.casePaperNumber,
appointment.appointmentDateTime);
allAppointedPatients.push(newAppointment);
})
).subscribe();
}
return allAppointedPatients;
}),
pipe(tap((data) => {
return this.allAppointedPatients;
}))
);
}
This is not working and I know there must be better way to handle such scenario. Please help...
You are messing up the async code (observables) with sync code by trying to return the allAppointedPatients array synchronously.
Understand first how async code is working in Javascript and also why Observables (streams) are so useful.
Try the code below and make sure you understand. Of course, I was not able to test it so make your own changes if needed.
getAppointments(): Observable<AppointedPatient[]> {
return this.appointmentService.fetchAllAppointments()
.pipe(
switchMap(appointments => {
const pacientAppointments = [];
for (const appointment of allAppointments) {
// Extract the data aggregation outside or create custom operator
const pacientApp$ = this.patientsService.getPatient(appointment.patientId)
.pipe(
switchMap((pacient) => of(
new AppointedPatient(
patient.firstName,
patient.lastName,
patient.address,
patient.casePaperNumber,
appointment.appointmentDateTime
)
))
)
pacientAppoinments.push(pacientApp$);
}
return forkJoin(pacientAppointments);
});
}
You can use forkJoin:
forkJoin(
getSingleValueObservable(),
getDelayedValueObservable()
// getMultiValueObservable(), forkJoin on works for observables that complete
).pipe(
map(([first, second]) => {
// forkJoin returns an array of values, here we map those values to an object
return { first, second };
})
);

Put calls in a queue and waiting for updating the state in React.js

I've got a function that I run in the forEach loop in the child component, the function has to filter the data and update the state with the result.
The problem is that when I iterate the function with a loop it running all at the same time and within this, the state will not update properly with data.
The question is how to implement it better, probably there is a way with Promise or async /await or maybe something simpler that will make a work. As needed to put in the queue and wait until the state will be updated.
Simplified code is here
component child
this.props.data.forEach((item, i) => {
this.props.update(item);
});
component parent
function update(data) {
let filtered = this.state.data.filter(item => item.uid !== data.uid);
this.setState({data: filtered});
}
If i understand well you need something like this:
update = () => {
let filtered = this.state.data.filter(x => this.props.data.every(y => x.uid !== y.uid))
this.setState({
data: filtered
})
}
Why not iterating through your array in your parent component?
Child:
this.props.update(this.props.data); // pass the entire array
Parent:
function update(data) {
let filtered = data.map(d=> {
return this.state.data.filter(item => item.uid !== d.uid);
}
this.setState({data: filtered});
}

Categories

Resources