Queuing CSS animations in Angular with RXJS
CSS Animations
CSS animations describe what a change in display on a DOM element should look like.
They are triggered when a CSS class is added or removed, or when a pseudo class like :hover is activated.
In Angular animations are triggered in the same way, with further support for how the DOM is manipulated to trigger the animations.
What though, if you need to animate, in order, a series of changes made by multiple users at the same time?
Lets make an app
Let's make an app with poll count that increases as users send through their choices. I like cheese, so I'll pivot this entire blog post around an app where users can vote for their favourite.
We'll track the count of each poll option in app.component.ts
public votes = {
asiago: 0,
cheddar: 0,
brie: 0
};
public vote(cheese: string) {
this.votes[cheese]++;
}
And represent these in app.component.html
<h1>What is your favourite cheese?</h1>
<ul>
<li>Asiago {{votes.asiago}}</li>
<li>Brie {{votes.brie}}</li>
<li>Cheddar {{votes.cheddar}}</li>
</ul>
<div>
<button (click)="vote('asiago')">Asiago</button>
<button (click)="vote('brie')">Brie</button>
<button (click)="vote('cheddar')">Cheddar</button>
</div>
Which gives us the following page:

Ok, time to add an animation
Lets add an animation that is triggered as new votes are received:
app.component.html
<li [@pollChange]="votes.asiago">Asiago {{votes.asiago}}</li>
<li [@pollChange]="votes.brie">Brie {{votes.brie}}</li>
<li [@pollChange]="votes.cheddar">Cheddar {{votes.cheddar}}</li>
app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [
trigger('pollChange', [
transition(':increment', group([
style({ color: 'green', fontSize: '50px' }),
animate('0.8s ease-out', style('*'))
]))
])
]
})
export class AppComponent {

... And one more animation
This is good. Let's add another one. This time we'll add an image for the cheese that was voted for last, and animate it!
app.component.html
<img [@lastcheese]
[src]="'/assets/' + lastCheese + '.jpg'"
*ngif="lastCheese"/>
<ul>
<li [@pollchange]="votes.asiago">Asiago {{votes.asiago}}</li></ul>
app.component.ts
...
animations: [
...
trigger('lastCheese', [
transition(':enter', [
animate('800ms', keyframes([
style({opacity: 0, transform: 'scale(1.0) translate3d(0,0,0)'}),
style({opacity: 0.9, transform: 'scale(1.5)'}),
style({opacity: 1, transform: 'scale(1.25)'}),
style({opacity: 1, transform: 'scale(1.0) translate3d(0,0,0)'}),
]))
])
])
]
})
export class AppComponent {
...
public lastCheese: string;
public vote(cheese: string) {
this.votes[cheese]++;
this.lastCheese = undefined;
setTimeout(() => {
this.lastCheese = cheese;
});
}
...

Concurrent votes
Looking good - but what happens if we receive votes at (or very close to) the same time?
The animations get cut off!

Time to fix this. Enter RXJS
With RXJS we can post new votes to an RXJS subject.
When a new vote is received a flag is set to indicate an animation is being rendered. When the animation has completed the flag is reset.
- When we process each event, if we're already processing an update we'll wait for 2 seconds (enough time for the animation to run).
- When the updates are being processed, we first set the last cheese variable, this will start the last cheese animation.
- After 1 second we set the new count - which means the count won't be updated (and animated) until the last cheese animation has occurred.
- Finally, after 1.5 seconds we reset the processing update flag - allowing any new votes incoming to be processed immediately.
app.component.html is left unchanged, leaving us with:
Our final app.component.ts:
export class AppComponent implements OnInit {
public votes = {
asiago: 0,
cheddar: 0,
brie: 0
};
private updateSubject = new Subject<string>();
private updateObservable = this.updateSubject.asObservable();
private processingUpdate = false;
public lastCheese: string;
public vote(cheese: string) {
this.updateSubject.next(cheese);
}
ngOnInit(): void {
this.updateObservable
.pipe(concatMap(x => of(x)
.pipe(delayWhen(() => this.processingUpdate ? interval(2000) : of(undefined)))))
.subscribe((cheese: string) => {
this.processingUpdate = true;
this.lastCheese = undefined;
setTimeout(() => {
this.lastCheese = cheese;
setTimeout(() => {
this.votes[cheese]++;
}, 1000);
setTimeout(() => {
this.processingUpdate = false;
}, 1500);
});
});
}

So now each vote animation is shown in-turn.
Exactly how we wanted it.
Of course this would not scale to large numbers of concurrent users, for that we'd look to use some more appropriate strategies in showing the votes.
Probably not showing all the votes - if we received over 43,200 votes, at 2 seconds per vote animation, there wouldn't be enough time in 24 hours to render them all!
For a small, low volume applications it works perfectly well.
For further details I've added the code to GitHub.
You can try it out here: https://matthewgerrard.github.io/queuing-angular-animations/.