How to combine two observables to create new observable? - javascript

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 };
})
);

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 map data correctly before subscription?

I have following function:
this.localStorage.getItem('user').subscribe(user => {
this.user = user;
this.authSrv.getOrders(this.user.einsender).pipe(map(orders => {
map(order => { order["etz"] = "23"; return order})
return orders;
})).subscribe(orders => {
this.orders = orders;
this.completeOrders = orders;
console.log(orders);
this.waitUntilContentLoaded = true;
})
})
The result without the map is:
[{id: 1, etz: "21"}]
With the map from above I try to enter the array, then the order and in the order I try to change the etz property but somehow nothing changes. Can someone look over?
I appreciate any help!
I see multiple issues here.
Try to avoid nested subscriptions. Instead you could use one of the RxJS higher order mapping operators like switchMap. You could find differences b/n different higher order mapping operators here and here.
To adjust each element of the array you need to use Array#map method in addition to the RxJS map operator.
You could use JS spread operator to adjust some of the properties of the object and retain other properties.
Try the following
this.localStorage.getItem('user').pipe(
switchMap(user => {
this.user = user;
return this.authSrv.getOrders(this.user.einsender).pipe(
map(orders => orders.map(order => ({...order, order['etz']: '23'})))
});
})
).subscribe(
orders => {
this.orders = orders;
this.completeOrders = orders;
console.log(orders);
this.waitUntilContentLoaded = true;
},
error => {
// good practice to handle HTTP errors
}
);
map is an operator that goes in a pipe like this:
someObs$.pipe(map(arg => { return 'something'}));
You've done this:
someObs$.pipe(map(arg => {
map(arg => { return 'something' }) // this line here does nothing
return arg;
}));
It doesn't make any sense to use map inside the function you've given to map

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'),
),
);

Chaining rxjs operators

I have a function called fetch which is implemented like this:
fetch(): Observable<Option> {
return this.clientNotebookService.getClientNotebook()
.pipe(
map(
clientNotebook => {
this.person = _.find(clientNotebook.persons, i => i.isn === this.isn);
return {
value: this.person.address['TownIsn'],
name: this.person.address['TownName']
};
}
)
);
}
where person is of type RelatedPerson and isn if of type string. The problem with this function is that it doesn't know anything about isn, that's to say, isn should be provided to it in some way.
This value can be extracted in the following way:
this.activatedRoute.params.subscribe(
params => this.isn = params['id']
)
And I would like to do this within the fetch method by combining rxjs operators.
You can chain them like below, just extract the 'id' and pass it to fetch
const isn=this.activatedRoute.params.pipe(pluck('id'))
isn.pipe(mergeMap(isn=>fetch(isn)))
Then add a param to your fetch, then it is available inside the function
fetch(isn){ .....
You can use combineLatest that emits an array of the latest value from both streams every time either emit a value
combineLatest(
this.activatedRoute.params.pipe(map(params => params['id'])),
this.clientNotebookService.getClientNotebook()
).pipe(map(([isn, clientNotebook]) => {
this.person = clientNotebook.persons.find(p => p.isn === isn); // No need for _
return {
value: this.person.address['TownIsn'],
name: this.person.address['TownName']
};
}));

mapping a Promise function to an array does not persist results

I have an object with two arrays as properties:
I want to populate the arrays by running promises in series.
I fetch the result of the promises, and map a function to decorate all the items in my arrays.
While one array get populated and persist, the other get populated only while in the map function, but at the end the array is returned still empty.
Can you help to understand why?
I check the Promise is actually returned, and indeed in one case it works, not in the other.
this is my pseudo-code:
function formatMyObject( arrayOfIds ) {
// initialize the objet
var myObj = {
decorators = [],
nodes = []
...
}
// I map the Promise reconciliate() and push the results in the array:
return reconciliateNode(arrayOfIds)
.then( data => {
data.map( node => {
// I fetch results, and myObj.nodes
myObj.nodes.push( { ... })
})
})
return myObj
})
.then( myObj => {
// myObj.nodes is now a NON empty array
// I want to the same with myObj.decorators:
var data = myObj.nodes
// I think I am doing just as above:
data.map( node =>
decorateNode(node.source)
.then( decoration => {
decoration = decoration[node.source]
myObj['decorators'].push( {
...
} )
// I check: the array is NOT empty and getting populated:
console.log('myObj.decorators', myObj)
debugger
})
)
// instead now, just after the map() function, myObj.decorators is EMPTY!
console.log('myObj.decorators', myObj);
debugger
return myObj
)
... // other stuff
}
As in the second case the map callback returns a promise, that case is quite different from the first case.
In the second case, you would need to await all those promises, for which you can use Promise.all.
The code for the second part could look like this:
.then( myObj => {
return Promise.all(myObj.nodes.map(node => decorateNode(node.source)));
}).then(decorations => {
myObj.decorators = decorations.map(decoration => {
decoration = decoration[node.source];
return ({
...
});
})
console.log('myObj.decorators', myObj);
return myObj;
})

Categories

Resources