Angular 2 *ngfor doesn't update with sequential http request responses - angularjs

I have an empty array, called rows and I want to display it with *ngFor. On each row of the array I pass the response from an http request. All the http requests are called sequentially. Even though the array is fed correctly, the *ngFor is never updated. I want to display each row sequentially by the time I receive its request response.
<div class="row" >
<my-rows
*ngFor="let row of rows | keys; trackBy: trackByFn; let index = index"
[month]='row.value'
[title]='row.key'>
</my-rows>
</div>
getRerports(url: string, dates: any, id: any){
let params = new URLSearchParams()
params.set('company_id',id)
params.set('date',dates[0])
this.authHttp.get(this._getReportsUrl,{search:params})
.subscribe(
data=>{
let formatedDate = moment(data.json().date,'YYYY-MM-DD').format('MMMM YYYY')
this.rows[formatedDate] = data.json()
dates.splice(0,1)
this.flag = false
if(dates.length>0){
this.getRerports(this._getReportsUrl,dates, id)
}else{
this.flag=true
}
},
err=> console.log(err)
)
}
trackByFn(index:any, item:any) {
return index;
}

Use ChangeDetectorRef if view is not updated.
constructor(private ref: ChangeDetectorRef)
Use this.ref.detectChanges() when you update your array.

I ve tried to change how I provide the object to rows array and instead of
this.rows[formatedDate] = data.json()
I ve used this:
let object = {key:formatedDate, value: data.json()}
this.rows.push(object)
and it seems to work just fine.
So the problem was in the array initialisation.

Related

Mat-select doesnt display the array content in ngfor

I have an array of countries and i want to display in a mat-select list of options. I am receiving the data like an object format in this.lngCountries so i need to convert to array first.
I think that the array is not complete before the ngfor is loaded. How can I wait for my function to finish? Because my problem is that when the page loads the ngfor my array is still empty.
My code:
private preparePaisOpts() {
let array = this.lngCountries;
this.paisOps = Object.keys(array).map(function(index) {
let count = array[index];
return count;
});
}
HTML:
<th2-mat-select class="form-field-dark" required [form]="usecaseForm"
formControlFieldName="pais"
placeholder="País">
<mat-option *ngFor="let country of paisOps" [value]="country">{{country}}</mat-option>
</th2-mat-select>
Thanks!!! <3
One way is ngIf
<th2-mat-select *ngIf="paisOps && paisOps.length > 0" class="form-field-dark" required [form]="usecaseForm"
formControlFieldName="pais"
placeholder="País">
<mat-option *ngFor="let country of paisOps" [value]="country">{{country}}</mat-option>
</th2-mat-select>
Better is to show a loading spinner instead of the select as example for a better user experience.
You can set a custom variable, too:
loading: boolean = true;
...
private preparePaisOpts() {
let array = this.lngCountries;
this.paisOps = Object.keys(array).map(function(index) {
let count = array[index];
return count;
});
this.loading = false;
}
and HTML
<th2-mat-select *ngIf="!loading" class="form-field-dark" .......
But!
Normally ngFor will be refresh the data if it's array changing. You write not specific error so I don't know what your problem is.

Angular/Firestore Collection Document Query to return a single document field from all documents into an array

I am performing a query on my collection documents and trying to return just all phone numbers into an array. I just want to set the phone numbers into array for use by another function. Firebase docs only show a console log for (doc.id) and (doc.data) and no practical use for any other objects in your documents. My console log for info.phoneNumbers returns all the phoneNumbers.
async getPhone() {
await this.afs.collection('members', ref => ref.where('phoneNumber', '>=', 0))
.get().toPromise()
.then(snapshot => {
if (snapshot.empty) {
console.log('No Matches');
return;
}
this.getInfo(snapshot.docs);
});
}
getInfo(data) {
data.forEach(doc => {
let info = doc.data();
console.log(info.phoneNumber, 'Phonenumbers');
// let myArray = [];
// myArray.push(doc.doc.data());
// const phoneNumber = info.phoneNumber as [];
// console.log(myArray, 'ARRAY');
return info.phoneNumber;
})
}```
Firestore is a "document store database". You fetch and store entire DOCUMENTS (think "JSON objects") at a time. One of the "anti-patterns" when using document store databases is thinking of them in SQL/relational DB terms. In SQL/relational DB, you "normalize" data. But in a document store database (a "NoSQL" database) we explicitly denormalize data -- that is, we duplicate data -- across documents on write operations. This way, when you fetch a document, it has all the data you need for its use cases. You typically want to avoid "JOINs" and limit the number of references/keys in your data model.
What you are showing in the code above is valid in terms of fetching documents, and extracting the phoneNumber field from each. However, use of .forEach() is likely not what you want. forEach() iterates over the given array and runs a function, but the return value of forEach() is undefined. So the return info.phoneNumber in your code is not actually doing anything.
You might instead use .map() where the return value of the map() function is a new array, containing one entry for each entry of the original array, and the value of that new array is the return value from map()'s callback parameter.
Also, mixing await and .then()/.catch() is usually not a good idea. It typically leads to unexpected outcomes. I try to use await and try/catch, and avoid .then()/.catch() as much as possible.
So I would go with something like:
try {
let querySnap = await this.afs.collection('members', ref =>
ref.where('phoneNumber', '>=', 0)).get();
let phoneNumbers = await this.getInfo(querySnap.docs[i].data());
} catch(ex) {
console.error(`EXCEPTION: ${ex.message}`);
}
getInfo(querySnapDocs) {
let arrayPhoneNumbers = querySnapDocs.map(docSnap => {
let info = doc.data();
let thePhoneNum = info.phoneNumber
console.log(`thePhoneNum is: ${thePhoneNum}`);
return thePhoneNum;
});
return arrayPhoneNumbers;
});
I solved this with help and I hope this may be helpful to others in Getting access to 1 particular field in your documents. In my service:
async getPhone() {
return await this.afs.collection('members', ref => ref.where('phoneNumber', '>=', 0))
.get().toPromise()
.then(snapshot => {
if (snapshot.empty) {
console.log('No Matches');
return;
}
return this.getInfoNum(snapshot.docs);
});
}
getInfoNum(data) {
return data.map(doc => {
let info = doc.data();
return info.phoneNumber
});
}
In my Component using typescript
phoneNumbers: string[] = [];
getPhone() {
this.dbService.getPhone().then(phoneNumbers => {
this.phoneNumbers = phoneNumbers;
this.smsGroupForm.controls.number.setValue(phoneNumbers.join(',')) //sets array seperated by commas
console.log(phoneNumbers);
});
}
This returns all the phone numbers in a comma separated array.
In my template I pull the numbers into an input for another function to send multiple text. Code in the template is not polished yet for the form, I am just getting it there for now.
<ion-list>
<ion-item *ngFor="let phoneNumber of phoneNumbers">
<ion-label position="floating">Phone Number</ion-label>
<ion-input inputmode="number"
placeholder="Phone Number"
formControlName="number"
type="number">{{ phoneNumber }}
</ion-input>
</ion-item>
</ion-list>

Loop array of IDs and fetch data by Observable

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[]>
}

Why can't I update its element value correctly within Array.forEach() loop in Angular 7?

I'm testing Material Table(mat-table) on Angular 7, here's a weird issue I ran into.
Send a request to jsonplaceholder for fake data in users.service
export class UsersService {
API_BASE = 'https://jsonplaceholder.typicode.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<object> {
const url = this.API_BASE;
return this.http.get(url);
}
}
Because jsonplaceholder only returns 10 rows of data, so I concatenate the data for a larger array, say, 30 rows for testing pagination feature with ease. Meanwhile, update the 'id' field with iterate index so the 'id's looks like 1,2,3...30, instead of 1,2,3...10,1,2,3...10,1,2,3...10, which is a result of concatenation, that's it, nothing special.
users.component:
ngOnInit(): void {
this.userService.getUsers().subscribe((users: UserData[]) => {
users = users.concat(users, users);
users.forEach((user, index) => (user.id = index +1));
console.log(users);
this.dataSource.data = users;
});
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
Although the table shows up beautifully, but the 'id's column looks weird, they are not 1,2,3...30 sequentially, instead, they are 21,22,23...30,21,22,23...30,21,22,23...30
I tried to print out the user.id inside the forEach loop, it's all good.
users.forEach((user, index) => {
user.id = index + 1;
console.log(user.id);
});
Where did I go wrong with this? Any clue? Thanks.
P.S, API used in the code: https://jsonplaceholder.typicode.com/users
even though you have 30 array elements after concatenating the array twice, you still only have 10 unique objects. the Object behind users[20] is the same as users[0], so you override the id of the already processed objects from index 10 to 29
you can fix this by creating a copy of each object. There are many ways too do this. a very simple way is serializing and deserializing using JSON.stringify and JSON.parse:
users.forEach(user => users.push(JSON.parse(JSON.stringify(user))));

React, dynamically add text to ref span

I'm trying to render a message to a span tag specific to an item in a list. I've read a lot about React 'refs', but can't figure out how to populate the span with the message after it's been referenced.
So there's a list of items and each item row has their own button which triggers an API with the id associated with that item. Depending on the API response, i want to update the span tag with the response message, but only for that item
When the list is created the items are looped thru and each item includes this
<span ref={'msg' + data.id}></span><Button onClick={() => this.handleResend(data.id)}>Resend Email</Button>
After the API call, I want to reference the specific span and render the correct message inside of it. But I can't figure out how to render to the span at this point of the code. I know this doesn't work, but it's essentially what I am trying to do. Any ideas?
if (response.status === 200) {
this.refs['msg' + id] = "Email sent";
I recommand using state. because string refs legacy (https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs)
const msgs = [
{ id:1, send:false },
{ id:2, send:false },
{ id:3, send:false },
];
this.state = {
msgs
};
return this.state.msgs.map((msg, index) => {
const status = msg.send ? "Email Sent" : "";
<span>{ status }</span><Button onClick={() => this.handleResend(index)}>Resend Email</Button>
});
async handleResend (index) {
const response = await callAPI(...);
if(reponse.status !== 200) return;
const newMsgs = _.cloneDeep(this.state.msgs);
newMsgs[index].send = true;
this.setState({
msgs: newMsgs
})
}
The workaround is set innerText
this.refs['msg' + id].innerText = "Email sent";
But rather than using ref try to use state to update elements inside render.
i was facing with this issue right now and i figured it out this way:
// currentQuestion is a dynamic Object that comes from somewhere and type is a value
const _target = `${currentQuestion.type}_01`
const _val = this[`${_target}`].current.clientHeight // here is the magic
please note that we don't use . after this to call the ref and not using refs to achieve what we want.
i just guessed that this should be an Object that would hold inner variables of the current object. then since ref is inside of that object then we should be able to call it using dynamic values like above...
i can say that it worked automagically!

Resources