Reputation: 14750
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
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
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
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
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