This site uses cookies. Continue to use the site as normal if you are happy with this, or read more about cookies and how to manage them.

×

This site uses cookies. Continue to use the site as normal if you are happy with this, or read more about cookies and how to manage them.

×

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:

The initial voting page receiving votes
The initial voting 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 {
The app page now showing an animation that happens when a vote is received
Animating the cheese name voted for

... 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;
   });
 }
...
The app page showing an animation of the last cheese voted for
Behold - the last 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!

The app page showing animations being cut off when votes are received too quickly
The animations do not complete if votes happen too quickly

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);
     });
   });
 }
The app now running through the vote animations in-turn
Rendering each animation in-turn

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