2

I am still rather new to angular and I just could not find an example for the following issue:

Assume you have a components which displays just a list of items. Each item gets passed from the parent ItemsComponent to a child ItemComponent component. Classic example.

However, let's also assume that we wish to update the list of items whenever there is a change. So we do a nasty poll each 5 seconds (suggestions for better solutions are welcome). This will update items and destroy each ItemComponent child for new ones to be created.

@Component({
  selector: 'app-items',
  templateUrl: './items.component.html',
  styleUrls: ['./items.component.css'],
  template:`
    <div *ngFor="let item of items">
      <app-item [item]="item"></app-item>
    <div>
  `
})
export class ItemsComponent implements OnInit {

  private polling;

  public items: Array<ItemModel>;

  constructor(private itemsService: ItemsService) {}

  ngOnInit() {
    this.polling = interval(5000).pipe(
       startWith(0),
       map(() => {
         this.itemsService.getItems().subscribe(
           (items) => {this.items = items;});
        })
      ).subscribe((data) => this.processData(data));
    }
  }

  ngOnDestroy() {
    this.polling.unsubscribe();
  }
}

Now there are a few problems:

  • The user will be scrolled back (rather snapped back) up to the top
  • Any complex state e.g. a child being in fullscreen mode is lost too

All this would have to be remembered and dealt with in the parent component.

So is there an "angular" war to do this or do I have to implement the update logic myself in the parent component?

My biggest headache is the *ngFor directive. I know I can get a reference to those children using @ViewChildren but I could not manage to achieve what I am seeking here.

So how can we update items in a list view with angular?

Stefan Falk
  • 23,898
  • 50
  • 191
  • 378
  • Using `@ViewChildren` you will get a reference of the item components. If you don't want to update the list data, you can update list items individually. It's trivial, but I'm not getting what is the exact goal. Wouldn't it be a better design to just spawn an "update" button and make it to highlight so that the user is aware that he can update the data? Besides, can you please provide the `ItemModel` class? – briosheje Sep 12 '18 at 09:46
  • @briosheje The problem I had with `@ViewChildren` is that I don't know how to handle new items or items which got removed. If `items` is now missing one item, how is this being reflected in `@ViewChildren`? Well, an update button is an option but imho a user should not be botherd with updating. An `item` can have likes and if the number of likes updates, I don't want the user to bother with this information - it should just update and that's where I am kind of stuck ^^ – Stefan Falk Sep 12 '18 at 09:51
  • I would start with this hint: if I'm not wrong, the *ngFor actually redraws if the **reference** of the array changes. If you **push** an item to the collection, it will just add it. So, you may handle these cases: 1) After you fetch the list, is the list already drawn? if not, assign the array. 2) Otherwise, loop the list you got: does the item exist in the ViewChildren? true: update the data (through ViewChildren) false: push the element to the `items` property. This should handle all cases. If you want to make it even better, assert an element is visible before updating. – briosheje Sep 12 '18 at 09:57
  • Another (easier) solution is to loop the existing items and update the existing ones **without** replacing the entire array. This would make the trick as well, but you won't be able to work with performances. If the list is getting extremely big, you may encounter performance issue as zone.js may take a while to update the view. If you can assert, in some way, that an element is visible, you can trigger the update of visible items only. – briosheje Sep 12 '18 at 09:59

1 Answers1

1

You can use trackBy for rerender only updated items.

And all angular is about observable try to use asyncPipe for items array

@Component({
  selector: 'app-items',
  templateUrl: './items.component.html',
  styleUrls: ['./items.component.css'],
  template: `
    <ng-container *ngIf="items$|async as items">
      <div *ngFor="let item of items; trackBy:item?.uniqueProp">
        <app-item [item]="item"></app-item>
      <div>
    </ng-container>
  `
})
export class ItemsComponent implements OnInit {
  public items$: Observable<Array<ItemModel>>;

  constructor(private itemsService: ItemsService) {}

  ngOnInit() {
    this.items$ = interval(5000).pipe(
      startWith(0),
      map(() => this.itemsService.getItems())
    );
  }
}
Stefan Falk
  • 23,898
  • 50
  • 191
  • 378
Kliment Ru
  • 2,057
  • 13
  • 16
  • If I try `foo = Observable.from(1);` and further use this in `*ngIf="foo|async"` I get an exception `ERROR TypeError: this._subscribe is not a function`. No idea why this is not working. – Stefan Falk Sep 12 '18 at 11:56
  • Look example here https://toddmotto.com/angular-ngif-async-pipe Can you create simple app with you problem on https://stackblitz.com/ ? – Kliment Ru Sep 12 '18 at 12:22
  • Hmm.. well I think I'm using `Obversable` wrong. I made a [stackblitz](https://stackblitz.com/edit/angular-pwylse). – Stefan Falk Sep 12 '18 at 12:32
  • You have problems with rxjs imports. My fork with async pype https://stackblitz.com/edit/angular-pncpqx?file=src/app/app.component.ts – Kliment Ru Sep 12 '18 at 12:53
  • Here example with trackBy https://stackblitz.com/edit/angular-a14qdu?file=src%2Fapp%2Fapp.component.html if you remove trackBy you can see reinitiation items in console after 2 sec – Kliment Ru Sep 12 '18 at 13:10
  • Hmm.. idk why but it's just not getting in my head here. [I'm trying](https://stackblitz.com/edit/angular-itzrbo?file=src/app/app.component.ts) to create a version with an add button s.t. `data` gets a new item added. The `interval` is supposed to update the observable `items$` but well .. it's just not working. I really don't get observables .. – Stefan Falk Sep 12 '18 at 13:18
  • It works fine https://stackblitz.com/edit/angular-zlx2cf?file=src%2Fapp%2Fapp.component.ts – Kliment Ru Sep 12 '18 at 13:56
  • Ok I think I see the problem now .. `getItems()` actually returns an `Observable<>` itself. So what I get in the end is apparently an observable of an observable so I guess this won't work like this.. – Stefan Falk Sep 12 '18 at 14:22
  • Well.. it tooke me quite long to get here but it's more or less working now. The problem now is that this still makes a "hard" replacement of the elements in my list. I think this is what `trackBy` is supposed to prevent? In my code, if I have `trackBy: item.id` I get an exception saying `Cannot read property 'id' of undefined`. This is weird because all items are getting populated so `item` is definitely not undefined.. – Stefan Falk Sep 12 '18 at 14:38
  • trackBy example https://stackblitz.com/edit/angular-a14qdu?file=src%2Fapp%2Fapp.component.html – Kliment Ru Sep 12 '18 at 14:52
  • ha .. finally works :D thank you for your help. I really should eat something I guess ^^ – Stefan Falk Sep 12 '18 at 15:28
  • Hmm. Just one more question if I may: Is it possible to *prevent* a child from being removed too early? E.g. if the user clicked an `item` in the list and is currently reviewing it. It should not be removed in that case. – Stefan Falk Sep 12 '18 at 18:08
  • This is a complex case. Maybe need to user NgRx or NgSx for mage you app state? – Kliment Ru Sep 13 '18 at 10:46
  • Will look into this. Thanks for the hint! :) – Stefan Falk Sep 13 '18 at 10:53
  • update Angular to 6.1. And try to use new feature Scroll Position Restoration (https://medium.com/lacolaco-blog/introduce-router-scroller-in-angular-v6-1-ef34278461e9) – Kliment Ru Sep 14 '18 at 08:22