1

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.

2
  • 1
    And by placing your input outside from app-view-data directly into the div ? Commented Jan 22, 2021 at 7:58
  • 1
    Every input is closely bound to each item in the data object. It is not bound to the entire data object. In real example there is more complex implementation. Input is just an example. Commented Jan 22, 2021 at 8:00

4 Answers 4

1

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.).

Sign up to request clarification or add additional context in comments.

1 Comment

Yep, I did it like you suggested. I put the data into a service. Now it works. But I don't think that this is a good solution, but it works at least ;
1

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

3 Comments

Thanks for suggestion. I'll keep it mind.
@Moshezauros, if you iterate over an array that change using *ngFor, you loose the "focus" when change (a *ngFor, each time change the data "clean" the old elements -remove the template- and redraw them). Your code works because you refresh the data manually (using a button)
thats true, but if you have merge the new values into the array (e.g. only update the count values, delete removed nodes and add new nodes), then you won't lose focus
0

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

1 Comment

I tried to make a local state, but there was a problem that the component was fully recreated/reinitialized
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.