BizzyBob
BizzyBob

Reputation: 14750

How to know when item has been rendered in DOM using *ngFor?

I'm displaying a list of messages using *ngFor and wish to apply a css class to items added to the collection after the list was originally rendered.

i.e. I don't want to apply the class when the view initially loads, but only when the messages collection gets a new item.

The source of this collection is an observable from a service which can change at any time.

<div *ngFor="let message of thread.messages">
    <div [class.fade-in-text]="threadWasLoaded">
        {{ message.text }}
    </div>
</div>

I thought I would just set a variable after I know the thread got loaded, but this has not worked. I tried subscribing to the observable in basically every Angluar lifecycle hook.

The results are either that the css class is always applied to all list items OR I get the following error:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.


this.thread$ = 
    this.storeQuery.getThreadWithMessages(this.threadId)
        .pipe(
            map(t => this.thread = new Thread(t)),
            tap(t => this.threadWasLoaded = true)
        );

It's possible I'm asking the wrong question entirely. Please let me know if my overall approach is off base. :-)

Edit: I've found a solution to my problem from This Article which approaches this in a different way by suppressing child animations on page load.

Upvotes: 2

Views: 1231

Answers (4)

bryan60
bryan60

Reputation: 29335

I know this was solved in another manner, but for future finders, this is a perfect usecase for the pairwise() operator to perform a delta operation:

  public  thread$ = this.dataService.thread$.pipe(
    startWith(null), // pairwise requires 2 emissions so we start it with a null
    pairwise(), // emit the last value and the current value in pairs
    map(([oThread, nThread]) => {
      if (oThread) { // only do this if there was a prior emission
        nThread.messages = nThread.messages.map(message => { // map the messages
          let isNew = false;
          if (!oThread.messages.find(m => m.id === message.id)) {
            // if they're not in the old array, they're new
            isNew = true;
          };
          return Object.assign({},message, {isNew});
        });
      }
      return nThread.messages;
    })
  );

and a blitz: https://stackblitz.com/edit/angular-problem-preventing-animation-on-page-load-aybwz4?file=src/app/app.component.ts

Upvotes: 1

Lars R&#248;dal
Lars R&#248;dal

Reputation: 876

EDIT 29/7-19: https://stackblitz.com/edit/angular-ptnvro Updated Stackblitz based on comments. :)

I created a working Stackblitz-example for you based on my comment: https://stackblitz.com/edit/angular-nn5w2u

I omitted the function for generating name here, but it is in the Stackblitz. objects = [];

  ngAfterViewInit() {
    this.fetchData();
    interval(5000).subscribe(() => this.addItem());

  }

  fetchData() {
    // creates mock data with a delay, simulating backend
    timer(2000).subscribe(() => this.objects = [{name: 'John Doe', initial: true}, {name: 'Marilyn Monroe', initial: true}])
  }

  addItem() {
    this.objects.push({name: this.generateName()});
  }

-

<ng-container *ngIf="objects.length">
<p class *ngFor="let obj of objects" [class.newItem]="!obj.initial">
  {{obj.name}}
</p>
</ng-container>

Upvotes: 1

Reactgular
Reactgular

Reputation: 54801

Create an observable with a timeout.

<div *ngFor="let message of thread.messages">
    <div [class.fade-in-text]="delayTrue() | async">
        {{ message.text }}
    </div>
</div>

function delayTrue() {
   return new Observable(s => setTimeout(() => (s.next(true),s.complete()));
}

Remove everything you did with threadWasLoaded and the error should go away.

Upvotes: 0

Kevin Doyon
Kevin Doyon

Reputation: 3578

Is the fade-in-text class an animation? If the old items keep their fade-in-text class, would it cause a problem? Could you work around that?

If that's the case, just keep the fade-in-text class on all items. Only new items will get the animation. But Angular might not know if an item is new or not without a trackBy in your ngFor, so it might remove/add back the class to old items, restarting their fade-in animation.

Upvotes: 0

Related Questions