How to properly get array data from observable? - javascript

I need two functions to return all data as well as specific filtered data, but my constructs are wrong. Below is what "think" I want, but am returning Subscriptions instead of arrays:
allItems() {
var collectionAll: AngularFirestoreCollection<Item> =
this._afs.collection<Item>('items');
var itemArray$: Observable<Item[]> =
collectionAll.valueChanges();
// Returns Subscription but I need Items[]
return itemArray$.subscribe(items => {
return items;
})
}
specificItems(name: string) {
var collectionSpecific: AngularFirestoreCollection<Item> =
this._afs.collection<Item>('items', ref =>
ref.where('name', '==', name));
var itemArray$: Observable<Item[]> =
collectionSpecific.valueChanges();
// Returns Subscription but I need Items[]
return itemArray$.subscribe(items => {
return items;
})
}
Also I would think that it would need to be an async function, but the subscribe function doesn't return a promise.
And I'm not even sure at what point I would actually be charged a read count from Firestore...?

If you want a promise, you need to convert the Observable to a Promise using toPromise:
specificItems(name: string): Promise<Item[]> {
var collectionSpecific: AngularFirestoreCollection<Item> =
this._afs.collection<Item>('items', ref =>
ref.where('name', '==', name));
var itemArray$: Observable<Item[]> =
collectionSpecific.valueChanges();
return itemArray$.toPromise();
}

Observables are very powerful, you should keep them as is.
allItems = this._afs.collection<Item>('items').valueChanges();
In your template, you can simply use the async pipe to read your data :
<div *ngFor="let items of allItems | async">...</div>
This is the most powerful way of using Angular for several reasons, so try learning it as soon as possible, because basically Angular = RxJS (not true of course, but it shows you how much you need RxJS in Angular)

Declare the below model in different location so that you can reuse the same model.
export class EmployeeRoster {
RosterDate: Date;
EmployeeId: number;
RosterDayName: string;
ProjectId: number;
ShiftId: number;
ShiftTime: string;
ShiftColor: string;
IsOnLeave: boolean;
}
Declare the below method in your service layer.
GetSavedEmployeeData(empIds: EmployeeIdlistToRoster[], sDate: string, eDate: string): Observable<EmployeeRoster[]> {
let empIdsValue = '';
if (empIds !== null && empIds.length > 0) {
empIds.forEach(function (em) {
empIdsValue += em.EmpId + ',';
});
}
//How to pass value as a parameter
const paramsdsf = new HttpParams()
.set('empIds', empIdsValue)
.append('sDate', sDate)
.append('eDate', eDate);
return this.http.get<EmployeeRoster[]>(apiUrl, { params: paramsdsf });
}
This is just an example you can update this method and model as per your requirement.

Related

Iterate through Observable object-array and add additional data from another Observable to every object

I have a Angular service returning an array of "cleaningDuty" objects. Inside a duty object, there is a nested object called "currentCleaner" with an id.
[
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
};
},
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
};
},
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
};
}
]
with the help of the currentCleaner.id I want to fetch the user data of the currentCleaner from a UserService dynamically in the same pipe() method and add the returned user data to the cleaningDuty object. Then the object should look like this:
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
},
cleanerData: {
name: string;
photoUrl: string;
// other user data
}
},
Unfortunately I just cant get this to work even after investing days into it. I tried almost every combination from forkJoin(), mergeMap() and so on. I know a nested subscribe() method inside the target component would get the job done, but I want to write the best code possible quality-wise. This is my current state of the service method (it adds the user observable instead of the value to the cleaningDuty object):
getAllForRoommates(flatId: string, userId: string) {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
mergeMap((duties) => {
let currentCleaners = duties.map((duty) =>
this.userService.getPublicUserById(duty.currentCleaner.id),
);
return forkJoin([currentCleaners]).pipe(
map((users) => {
console.log(users);
duties.forEach((duty, i) => {
console.log(duty);
duty.cleanerInfos = users[i];
});
return duties;
}),
);
}),
);
}
The getPublicUserById() method:
getPublicUserById(id: string) {
return this.firestore.collection('publicUsers').doc(id).valueChanges();
}
Any help would be greatly appreciated!
The forkJoin function will:
Wait for Observables to complete and then combine last values they emitted; complete immediately if an empty array is passed.
So, you have to make sure that all the inner Observables have been completed, then the forkJoin will emit the combined values, and you can achieve that using take(1) operator, like the following:
getAllForRoommates(flatId: string, userId: string) {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
mergeMap((duties) =>
// The `forkJoin` will emit its value only after all inner observables have been completed.
forkJoin(
duties.map((duty) =>
// For each duty, fetch the cleanerData, and append the result to the duty itself:
this.userService.getPublicUserById(duty.currentCleaner.id).pipe(
take(1), // to complete the sub observable after getting the value from it.
map((cleanerData) => ({ ...duty, cleanerData }))
)
)
)
)
);
}
Check if you need switchMap or mergeMap. IMO I'd cancel the existing observable when the valueChanges() emits. See here for the difference b/n them.
Use Array#map with RxJS forkJoin function to trigger the requests in parallel.
Use RxJS map operator with destructuring syntax to add additional properties to existing objects.
Ideally defined types should be used instead of any type.
Try the following
getAllForRoommates(flatId: string, userId: string) {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
switchMap((duties: any) =>
forkJoin(
duties.map((duty: any) => // <-- `Array#map` method
this.userService.getPublicUserById(duty.currentCleaner.id).pipe(
map((data: any) => ({ // <-- RxJS `map` operator
...duty, // <-- the `currentCleaner` property
cleanerData: data // <-- new `cleanerData` property
}))
)
)
)
)
);
}
Firstly I would suggest you to improve the typings on your application, that will help you to see the issue more clear.
For my answer I will assume you have 3 interfaces defined CurrentCleaner, CleanerData and RoomData wich is just:
interface RoomData {
currentCleaner: CurrentCleaner
cleanerData: CleanerData
}
Then you can write a helper function to retrieve the room data from a current cleaner:
getRoomData$(currentCleaner: CurrentCleaner): Observable<RoomData> {
return this.userService.getPublicUserById(currentCleaner.id).pipe(
map(cleanerData => ({ currentCleaner, cleanerData }))
)
}
As you can see, this functions take a current cleaner, gets the cleaner data for it and maps it into a RoomData object.
Having this next step is to define your main function
getAllForRoommates(flatId: string, userId: string): Observable<RoomData[]> {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
mergeMap((duties) => {
// here we map all duties to observables of roomData
let roomsData: Observable<RoomData>[] = duties.map(duty => this.getRoomData$(duty.currentCleaner));
// and use the forkJoin to convert Observable<RoomData>[] to Observable<RoomData[]>
return forkJoin(roomsData)
}),
);
}
I think this way is easy to understand what the code is doing.

What is the replacement for `publishReplay(1).refCount()` in RxJS version 7.4.0

I am learning RxJS and Angular. The below code is a simplified version of rxjs-chat from the book ‘ng-book2-book-angular-11-r77.pdf’.
It’s a simple Angular service class.
This is what I am trying to do:
Call from outside addMessage(1) method
From that method I push value "1" onto newMessages Subject using newMessages.next() method.
newMessages subscribes in turn to updates(Subject)
updates is using pipe member of updates to map passed message into concatenated array of numbers (messages) and returns the concatenated array (the initial value of concatenated array is kept in const initialMessages).
The result is stored into the messages(Observable).
pipe method also calls share operator so the messages Observable can update Observable newNumbers object (at least that what I thought it would do)
That where my problem is - the newNumbers are not updated. I thought that the this.newNumbers = this.messages... will execute because of my share operator is called. (Similar code in the mentioned above book works but they are using deprecated methods publishReplay(1).refCount() instead of my share operator. Could someone point me in the right direction - below is the code:
const initialMessages: number[] = [];
#Injectable({ providedIn: 'root' })
export class MessagesService {
// `newNumbers` is a observable that contains the most up to date list of numbers
newNumbers: Observable<{ [key: string]: number }>;
newMessages: Subject<number> = new Subject<number>();
// `messages` is a stream that emits an array of the most up to date numbers
messages: Observable<number[]>;
updates: Subject<any> = new Subject<any>();
constructor() {
this.messages = this.updates
// watch the updates and accumulate operations on the messages
.pipe(
map((message) => {
let retVal = initialMessages.concat(message);
return retVal;
}),
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
this.newMessages.subscribe(this.updates);
this.newNumbers = this.messages.pipe(
map((messages: number[]) => {
const numbers: { [key: string]: number } = {};
messages.map((n: number) => {
numbers[n] = numbers[n] || n;
console.log(n);
});
return numbers;
})
);
this.messages.subscribe((value) => console.log(value));
}
addMessage(value: number): void {
this.newMessages.next(value);
}
}
Operators publishReplay(1) + refCount() are equals to shareReplay(1)

How to invoke function that return an Observable within a recursive function?

I have the following function that traverse the tree-like object and it working fine so far.
const traverse = async (menuInputs: MenuInput[], parent: Menu = null) => {
for (const input of menuInputs) {
const entity = toEntity(input, parent);
const parentMenu = await this.menuService.create(entity).toPromise();
if (isEmpty(input.children)) {
continue;
}
await traverse(input.children, parentMenu);
}
};
My question is how can i invoke method this.menuService.create(entity) that is actually return an Observable<Menu> without convert it to Promise, are there any RxJS way of doing this ?
I've got something like this for recursive observable calls using the HttpService. You can probably work with something similar.
#Injectable()
export class CharacterReaderApiService implements CharacterReader {
private characters: SwapiCharacter[] = [];
constructor(private readonly http: HttpService) {}
async getCharacters(): Promise<Character[]> {
console.log('Querying the API for character data...');
return this.callUrl('https://swapi.dev/api/people')
.pipe(
map((data) => {
return data.map(this.mapPerson);
}),
tap(async (data) =>
promises.writeFile(join(process.cwd(), characterFile), JSON.stringify(data)),
),
)
.toPromise();
}
private callUrl(url: string): Observable<SwapiCharacter[]> {
return this.http.get<SwapiResponse>(url).pipe(
map((resp) => resp.data),
mergeMap((data) => {
this.characters.push(...data.results);
return iif(() => data.next !== null, this.callUrl(data.next), of(this.characters));
}),
);
}
private mapPerson(data: SwapiCharacter): Character {
return {
name: data.name,
hairColor: data.hair_color,
eyeColor: data.eye_color,
gender: data.gender,
height: data.height,
};
}
}
The important thing is keeping a running array of the values so that they can be referenced later on.
By using mergeMap with iif we're able to recursively call observables and work with the result as necessary. If you don't like the idea of a class variable for it, you could make the running array a part of the callUrl (or similar) method's parameters and pass it on as needed.
I'd strive to change toEntity() and menuService.create() to receive the list of children rather than the parent so I could use forkJoin:
const buildMenu = (input: MenuInput) =>
forkJoin(
input.children.map(childInput => buildMenu(childInput, menu))
).pipe(
flatMap(childMenus => this.menuService.create(toEntity(input, childMenus)))
);

How can I access the auto-created document ID when creating a new Firestore document in Angular?

I am trying to return the document id when I create it. Since Firebase functions are async the return value is not completed until the query is done. How can I prevent this so I can wait to get the value when the query is done?
This function create the document is located in a service:
public makeDoc(title: string, score: number): any{
const fields = {
title: title,
score: number
}
this.db.collection('saved').add(fields)
.then(function(ref) {
console.log(ref.id);
return ref.id;
})
}
I call this from a function which is located in a component:
onCreate() {
const str = this.createService.makeDoc(this.title, this.score);
console.log(str);
}
Try following:
const fields = {
title: title,
score: number
}
var newFieldsRef = this.db.collection('saved').push();
this.db.collection('saved').child(newFieldsRef).set(fields);
var id = newFieldsRef.toString();
You don't want to prevent waiting until the query is done, you should embrace the use of promises here.
First, if you haven't, make sure you import the firestore namespace in the service:
import { firestore } from 'firebase';
Now, for your service:
I had to slightly change your makeDoc method as the fields object wasn't being created in a valid way (e.g. reusing the number type):
public makeDoc(titleParam: string, scoreParam: number): Promise<firestore.DocumentReference> {
const fields = {
title: titleParam,
score: scoreParam
};
return this.db.collection('saved').add(fields);
}
This now returns a Promise<DocumentReference> which, when resolved, the reference will point to the created document.
Now, the call to it in onCreate looks like:
onCreate() {
this.createService.makeDoc('myTitle', 123)
.then((ref) => { console.log(ref.id); })
.catch((err) => { console.log(err); });
}
And this will log the id as soon as it is available.

Flow (InferError): Cannot get 'object[key]' because an index signature declaring the expected key / value type is missing in 'Class'

Here's my code to test equality of some class objects. See my other question if you want to know why I'm not just doing
expect(receivedDeals).toEqual(expectedDeals) and other simpler assertions.
type DealCollection = { [key: number]: Deal }; // imported from another file
it("does the whole saga thing", async () => {
sagaStore.dispatch(startAction);
await sagaStore.waitFor(successAction.type);
const calledActionTypes: string[] = sagaStore
.getCalledActions()
.map(a => a.type);
expect(calledActionTypes).toEqual([startAction.type, successAction.type]);
const receivedDeals: DealCollection = sagaStore.getLatestCalledAction()
.deals;
Object.keys(receivedDeals).forEach((k: string) => {
const id = Number(k);
const deal = receivedDeals[id];
const expected: Deal = expectedDeals[id];
for (let key in expected) {
if (typeof expected[key] === "function") continue;
expect(expected[key]).toEqual(deal[key]);
}
});
});
The test passes fine, but I'm getting a Flow error on expected[key]:
Cannot get 'expected[key]' because an index signature declaring the expected key / value type is missing in 'Deal'
I can paste in code from Deal by request, but I think all you need to know is that I haven't declared an index signature (because I don't know how!).
I've searched around a bit but I can't find this exact case.
Update: I can eliminate the errors by changing deal and expected thusly:
const deal: Object = { ...receivedDeals[id] };
const expected: Object = { ...expectedDeals[id] };
And since I'm comparing properties in the loop this isn't really a problem. But I would think that I should be able to do this with Deals, and I'd like to know how I declare the index signature mentioned in the error.
PS. Bonus question: In some world where a mad scientist crossbred JS with Swift, I imagine you could do something like
const deal: Object = { ...receivedDeals[id] where (typeof receivedDeals[id] !== "function" };
const expected = // same thing
expect(deal).toEqual(expected);
// And then after some recombining of objects:
expect(receivedDeals).toEqual(expectedDeals);
Is this a thing at all?
Edit:
Adding a bit of the definition of Deal class:
Deal.js (summary)
export default class Deal {
obj: { [key: mixed]: mixed };
id: number;
name: string;
slug: string;
permalink: string;
headline: string;
// ...other property definitions
constructor(obj?: Object) {
if (!obj) return;
this.id = obj.id;
this.name = obj.name;
this.headline = obj.headline;
// ...etc
}
static fromApi(obj: Object): Deal {
const deal = new Deal();
deal.id = obj.id;
deal.name = obj.name;
deal.slug = obj.slug;
deal.permalink = obj.permalink;
// ...etc
return deal;
}
descriptionWithTextSize(size: number): string {
return this.descriptionWithStyle(`font-size:${size}`);
}
descriptionWithStyle(style: string): string {
return `<div style="${style}">${this.description}</div>`;
}
distanceFromLocation = (
location: Location,
unit: unitOfDistance = "mi"
): number => {
return distanceBetween(this.location, location);
};
distanceFrom = (otherDeal: Deal, unit: unitOfDistance = "mi"): number => {
return distanceBetween(this.location, otherDeal.location);
};
static toApi(deal: Deal): Object {
return { ...deal };
}
static collectionFromArray(array: Object[]) {
const deals: DealCollection = {};
array.forEach(p => (deals[p.id] = Deal.fromApi(p)));
return deals;
}
}
An index signature (or indexer property) is defined as [keyName: KeyType]: ValueType. DealCollection is a great example: the keyName is key, the KeyType is number and the ValueType is Deal. This means that whenever you access a number property of an object of type DealCollection, it will return a Deal. You will want to add a similar expression to the definition of Deal in order to access arbitrary properties on it. More information can be found at the Objects as maps section in the Flow documentation.

Categories