Sergej
Sergej

Reputation: 2196

Problem updating component data in Angular

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

Answers (4)

Eliseo
Eliseo

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

Moshezauros
Moshezauros

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

Rachid O
Rachid O

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

Gunnar B.
Gunnar B.

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

Related Questions