Reputation: 2196
I need help to solve the next problem:
I have a component A
, that subscribes to an API service and gets new data every 5 seconds:
ngOnInit(): void {
const obs = this.dataService.getData();
this.data$ = obs.pipe(repeatWhen(() => interval(5000)));
}
in the template I have the next code:
<div *ngFor="let data of data$ | async">
<app-view-data [data]="data"></app-view-data>
</div>
In the component ViewDataComponent (app-view-data)
I make the preview of the input data
with some additional options, for example, I add an input field, where user can type anything next to the provided data.
example template:
Id: {{ data.id }}<br/>
Name: {{ data.name }}<br/>
Count: {{ data.count }}<br/>
<input placeholder="enter your comment" />
The problem is that, when the Observable from the dataService
emits a new value, the ViewDataComponent
components are rebuild, and the input's value is lost.
The emitted data in most cases contains the same set of data, which can be slightly changed, for example the count
can be updated for the same item.
If the item get's removed - it's ok to lost that input, but if the item did not change, or only count was updated - then the input
must stay. The ViewDataComponent
component should not rebuild.
I know there is an optimization feature in angular - trackBy
, but this doesn't fit me, because the data can be changed (for example the count
can be updated), but the item must stay the same place. - in this situation the ViewDataComponent
should only update the count
without any other visual changes.
Upvotes: 1
Views: 665
Reputation: 57939
it'f a few complex, but you can use this aproach. The idea is not iterate over $data|async, else a FormArrayControls. I'm going to create a FormArray of FormGroups, so defined a function that return a FormGroup giving the index and another to create a new FormGroup
formArray=new FormArray([])
//get a formGroup from rormArray
getGroup(index)
{
return this.formArray.at(index) as FormGroup
}
//this allow create a new FormGroup
createGroup(id:number){
return new FormGroup({
id:new FormControl(id),
other:new FormControl()
})
}
In the observable, using "tap" we are going to check the response and change the formArray adding elements that not exist and removing elements if the response don't include the FormArray
this.data$ = interval(5000).pipe(
switchMap(_=>this.dataService.getData(this.count)), //<--really I want to return this
tap((res:any[])=>{
const idsArrays=this.formArray.value.map(x=>x.id)
const idsres=res.map(x=>x.id)
//search in formArray to add new elements
res.forEach(x=>{
if (idsArrays.indexOf(x.id)<0)
this.formArray.push(this.createGroup(x.id))
})
//check in res to remove elements of FormArray
for (let i=0;i<this.formArray.value.length;i++)
{
if (idsres.indexOf(this.formArray.value[i].id)<0)
{
this.formArray.removeAt(i);
i--;
}
}
this.count=(this.count+1)%5 //<--this is only to "simulate"
})
)
Well, the .html use a "tipical construction" *ngIf="{data:data$|async} as result" to only subscribe one time, see that under the ng-container you can use result.data
<ng-container *ngIf="{data:data$|async} as result">
<div *ngFor="let group of formArray.controls;let i=index">
<hello [data]="result.data" [group]="getGroup(i)"></hello>
</div>
</ng-container>
Yes, to the component pass all the response in data and the formGroup of the formArray at index "i"
Our child-component use a setter in the input to give value to the control and to "index" to
control:FormControl;
index:number=0
id:number=0
data:any
@Input('data') set _data(value)
{
this.data=value;
if (this.control && this.control.value)
this.index=this.data.findIndex(x=>x.id==this.id)
};
@Input() set group(value){
this.id=value.value.id
this.control=value.get('other') as FormControl
this.index=this.data.findIndex(x=>x.id==this.id)
}
And the .html,e.g.
<pre>{{data[index]|json}}</pre>
<input [formControl]="control">
You can see in the stackblitz
NOTE, you can, if you has more that one "input" for element use a FormGroup
Upvotes: 0
Reputation: 2593
You are trying to merge two arrays (original array has some changes, e.g. the inputValue), and the new array, which might add, remove or update existing elements. I think you should utilize rxjs and merge the two arrays into a new one - saving the old values and adding them to the new data that arrived:
ngOnInit(): void {
const obs = this.dataService.getData();
this.data$ = obs.pipe(
repeatWhen(() => interval(5000)),
map((v) => {
// merge the two arrays here
})
);
}
here is a working example - you need to trigger the emit with the button.
you can improve this solution by keeping the old values (making the merge function more complex, because you have to update all property changes), this will net the benefit of not re-rendering objects which didn't change, making the UI faster\smoother
Upvotes: 1
Reputation: 14002
one way to solve this would be to save your comments in the local state of the ViewDataComponent
in order to make them persist between re-renders
exemple: https://stackblitz.com/edit/angular-save-local-input-state?file=src/app/hello.component.ts
Upvotes: 0
Reputation: 2989
trackBy
will generally be helpful to you because it will prevent the whole list from rebuilding. By default, objects are tracked by reference, but with objects that is not really what you want. Instead you want trackBy
to return an aggregate of the relevant information of the object. That can be as simple as an id, but also more complex.
This should fix your problem partially. If there is no change on refresh, the row would not be rebuild. When there is a change, it would be.
An idea to solve that part would be to 'persist' the values that you edit right now by putting the currently being edited data into a service and combine the value with the refreshed value and repopulate the respective input. This might be a bit choppy for the user though (probably needs refocusing etc.).
Upvotes: 1