Systemizing Router-Based Animations in Angular
Tags: angular, animations, router
Published: 2021-01-31 07:50:12

---
title: Systemizing Router-Based Animations in Angular
published: true
description: Building a proper system for router animations
tags: angular, animations, router
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/5cd36xr9kfnnl8ard9yq.png
---

# What We'll Be Building

In this guide, we'll be building an 'animation system' for an Angular application, where our top-level routes will slide to the right or the left, based on an `order` configuration property.

The main idea in terms of user experience is to use animations to queue "spatial familiarity" with our app, by which I mean:
+ Routing from page 1 to page 2 in our app: page 1 slides to the left as page 2 slides in from the left
+ Routing from page 2 to page 1 in our app: page 2 slides to the right as page 1 slides in from the right

This is a little subliminal nudge that reinforces in our user the idea: "page 2 comes after page 1", and helps (if only just a little) their brain to build a map of our app.

![Flipping through cards](https://media1.tenor.com/images/4b2042b23c1e26e0d61553f60b9a9bda/tenor.gif?itemid=4476101)

As a slightly related aside: I learned recently that the best card-counters/"memory athletes" will typically associate each card of a 52-card deck with a person they know. They also come up with 52 spots in a house or physical location that they're familiar with. When they're given a deck to memorize, as they survey the order of cards, they'll "walk" though their imagined house, placing each person in one of these spots. When called upon to recall the order then, they'll mentally walk through this house again, naming the cards associated with each person as they go!

This is a 'brain hack' in that we are evolved to have special recognition and memory around human faces, and our brains tend to best associate the concept of 'sequence' with traversing a physical place.

In creating this type of animation system for our application, we're seeking to leverage these same principles, but in reverse.

This is hardly a necessary feature for all apps, but I find that it provides a nice experience for most users, and it's a little hint of craftsmanship, that I think helps a website/app stand out to users that are so used to the soul-less desert of our modern web where every random site you visit immediately asks for notification or location permissions before you even get started.

Here's a real-life example of this system in action, which I've actually implemented in my [work-in-progress] portfolio site: [zackderose.dev](https://zackderose.dev)!

![Our Example from zackderose.dev](https://dev-to-uploads.s3.amazonaws.com/i/koql1kkhn5pvhzfbixux.gif)

# Top-Level Architecture

From a top-level, here's how we'll break down this problem:

1. Create a `slideLeft` and `slideRight` animation to describe the sliding animations that components should perform as they enter/leave the DOM.
2. Enumerate the 'states' of our app (the top-level routes).
3. Create configuration that will map each of these states to their typical route config, plus an order property, that will determine whether to slide left/right as it enters/leaves.
4. Create an array of all possible state transitions, as sourced by our state array and config data, to determine whether to slide left or right.
5. Use our configuration data to create the route array that we'll pass to `RouterModule` import.
6. Hook everything up with the rest of the Application!

# Step 1: Creating Animations

Let's start by defining our `@angular/animation` objects. As stated, we'll create a `slideLeft` and a `slideRight` animation for our 2 scenarios, and we'll add them in a file we'll call `animations.ts`. Here's the `slideLeft` animation:

```ts
import { animate, group, query, style } from "@angular/animations";

const ANIMATION_SPEED = "500ms";

export const slideLeft = [
  query(":enter, :leave", style({ position: "fixed", width: "100%" })),
  group([
    query(":enter", [
      style({ transform: "translateX(150%)" }),
      animate(
        `${ANIMATION_SPEED} ease-in-out`,
        style({ transform: "translateX(0)" })
      )
    ]),
    query(":leave", [
      style({ transform: "translateX(0%)" }),
      animate(
        `${ANIMATION_SPEED} ease-in-out`,
        style({ transform: "translateX(-150%)" })
      )
    ])
  ])
];
```

Walking through this quickly, the first item in our array will add a `fixed` position (so we can properly translate the elements) and `100%` width (so that both elements take up the whole width of screen) to both the entering (`:enter`) and exiting (`:leave`) element.

The second item in this array is a nested `group()` that contains its own array. This `group()` will cause both of the inner animations to occur simultaneously, which we'll want as we want both the entering and exiting pages to be sliding at the same time to give us this 'spatial familiarity' effect.

For the entering element and sliding left, we'll want to start it out `transform`ed by being translated 150% to the right of its destination. Then we'll animate it for 500ms (and using a `ease-in-out` sequence) to back to its normal position (the `translateX:(0)` in the above code).

For the exiting element, we'll do something very similar. The only difference is since the element on its way out is already starting out in the correct place, we can skip the initial `transform` and have a single `animate()` here that will transform 150% to the left over the same duration and with the same sequence.

`slideRight` is essentially this same thing in reverse. We'll export both of these animations now so we can use these down the road!

# Step 2: Enumerating App States

Now let's think about our states. For this example, we'll have 3 states: 'home', 'page-1', and 'page-2':

```ts
export const STATES = ["home", "page-1", "page-2"] as const;
```

For Typescript magic we'll take more advantage of later, we'll create a `Type` that uses the above read-only array as it's source of truth:

```ts
export type ExampleAppState = typeof STATES[number];
```

This will behave the same as a 'union type' of all the states from our `STATES` array, while still having the benefit of a single source of truth for both the array of states (which we'll use later to help define our state transitions and routes) and the typing, which we'll leverage to prevent typos and to ensure all states are present as keys to our upcoming configuration map.

![Proper Intellisence](https://dev-to-uploads.s3.amazonaws.com/i/3b6ftzqrsb5ifxv4xfjk.png)

# Step 3: Configuration

Next, we'll define a configuration interface that will store all configuration our app will use for a given state:

```ts
interface StateConfiguration {
  path: string;
  component: any;
  order: number;
  linkText: string;
}
```

`path` and `component` will be used the same as you would with a standard `Route` from the Angular router. `order` will be used to order our links, as well as to determine the proper animation direction for a given state transition, and `linkText` will be used for putting correct text into our template.

Since we'll want a single configuration object that should contain a property for every state, we'll reach for[`Record<ExampleAppState, StateConfiguration>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeystype) - which will give us exactly that!

```ts
export const stateConfiguration: Record<ExampleAppState, StateConfiguration> = {
  home: {
    path: "",
    component: HomeComponent,
    order: 0,
    linkText: "Home"
  },
  // ....
};
```

Note too that when we add a new state to our `STATES` array, Typescript will now warn us with an appropriate clarifying message, that the new state is missing from the `stateConfiguration` object!

![Attempting to add a new route](https://dev-to-uploads.s3.amazonaws.com/i/5w1toq9zp8st1dd45yx3.gif)

# Step 4: Creating a Router Transition `trigger()`

Next, we'll create an `@angular/animations` `trigger()` that will list all of our state-to-state transitions, and define the entering and leaving animation associated with each state.

Normally, this would look something like this:

```ts
const routerTransition = trigger(
  'routerTransition',
  [
    transition('home => page-1', shiftLeft),
    transition('page-1 => home', shiftRight)
    // ... more for every possible transition
    // for these 3 states: 2 * 3 = 6
    // if 4 states: 3 * 4 = 12
    // if 5 states: 4 * 5 = 20
    // ...etc.
  ]
);
``` 

![Lots of Typing](https://media4.giphy.com/media/fQZX2aoRC1Tqw/giphy.gif)

But that's a whole bunch of typing - and more typing give us more chance for a typo in our names (which are not type-safe... though with using TS 4.1 template types, we could get there: 
```ts
type RouterTransition = `${ExampleAppState} => ${ExampleAppState}`;
```
There's also a good chance of accidentally using a wrong animation for maybe just one or two of the transitions (which is immeasurable worse than them all being wrong - since the bug wouldn't be immediately apparent while using the app!).

So instead, we'll look to programmatically build out this information from the `STATES` array and the `stateConfiguration` object.

Let's start by creating an array of every possible valid tuple of states. This looks like:

```ts
const allStateCombinations: [
  ExampleAppState,
  ExampleAppState
][] = STATES.reduce(
  (acc, state, index) =>
    acc.concat(
      STATES
        .filter((_, i) => i !== index)
        .map(target => [state, target])
    ),
  []
);
```

There's a bit of fancy `reduce()`ing happening here, but essentially what this code is doing is saying:
  1. Start with an empty array
  2. For each state, append to that array all possible tuples where the given state is the first state of the tuple. For example, for the first state, `home`, those tuples will look like this:

```ts
[
  ['home', 'page-1'],
  ['home', 'page-2']
]
```

With this array of all tuples built, we can now map each tuple to the correct `transition()` object in our `trigger()`, based on `stateConfiguration` data:

```ts
export const routerTransition = trigger(
  "routerTransition",
  allStateCombinations.map(([entering, leaving]) =>
    transition(
      `${entering} => ${leaving}`,
      stateConfiguration[entering].order < stateConfiguration[leaving].order
        ? slideLeft
        : slideRight
    )
  )
);
```

Note that we're [destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) our tuples here in our `map()` to an `entering` and `leaving` state. The string defining the transition is now: `${entering} => ${leaving}`, and depending on the `order` property of the `entering` and `leaving`'s state configuration, the animation associated with that state transition will either be `slideRight` or `slideLeft`.

Perfect! Now we're export this `routerTransition` `trigger()` so we can use it in our application!! 

# Step 5: Creating Routes

Similarly to how we programmatically built our `@angular/animations` `trigger()` from the `STATES` and `stateConfigurations` sources of truth, we'll look to do the same here! (But this should be an order of magnitude easier)

To create our `routes` array, we'll map out the `STATES` array and enrich it with the relevant data from the `stateConfiguration` object:

```ts
export const routes = STATES.map(state => ({
  path: stateConfiguration[state].path,
  component: stateConfiguration[state].component,
  data: { state } // <== note that here we are associating
                  // a `state` with the route data, which
                  // we'll use later in our template
}));
```

# Step 6: Hooking This Up to the Rest of our App

Now that we've got all the lego blocks we'll need properly exported from our `routes.ts` file, let's go ahead and hook these up to the rest of our app.

First, the `AppModule`:

```ts
import { routes } from "./routes";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(routes), // <== routes go here!
    BrowserAnimationsModule // <== don't forget to import this!!
  ],
  declarations: [AppComponent, HomeComponent, Page1Component, Page2Component],
  bootstrap: [AppComponent]
})
export class AppModule {}
```

Next up, our `AppComponent`. We'll start with the `@Component()` decorator:

```ts
import { routerTransition } from "./routes";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
  animations: [routerTransition] // <== add the trigger here
})
export class AppComponent {
  // ...
}
```

And inside the component class, we'll create a `links` property based off our `STATES` and `stateConfiguration`:

```ts
import {
  stateConfiguration,
  STATES
} from "./routes";

@Component({
  // ...
})
export class AppComponent {
  // ...

  links: { linkText: string; path: string }[] = [...STATES]
    .sort((a, b) =>
      stateConfiguration[a].order > stateConfiguration[b].order ? 1 : -1
    )
    .map(state => stateConfiguration[state]);
}
```

Note that I think there's a decent argument for wrapping this up into a factory function that lives in the `routes.ts` file, but I think works just fine as well.

Finally, the `AppComponent` Template:

```html
<h1>Example App</h1>

<nav>
  <ol>
    <a *ngFor="let link of links" [routerLink]="link.path">
      <li>
        {{ link.linkText }}
      </li>
    </a>
  </ol>
</nav>

<main [@routerTransition]="(o.activatedRoute.data | async).state">
  <router-outlet #o="outlet"></router-outlet>
</main>
```

Note that the `[@routerTransition]="(o.activatedRoute.data | async).state"` directive is named after the `routerTransition` `trigger` that we wrote. The value passed to this directive should be a statement that evaluates to the current state. Since we've created a reference `#o` to the `<router-outlet>`'s `outlet`, we can listen to emissions from that `outlet`'s `activedRoute.data` Observable with the async pipe. Since in our `routes` data, we add the `state` property to each route's data (see the note in the code comment in Part 5 above), we can get this `state` property out of that `data` object emitted.

# The SYSTEMZ

Together this all works well, and creates a fairly solid architecture, in my opinion. Our `routes.ts` serves as a system that fairly easily plugins in with the rest of our app. Further, the simple `STATES` array and `stateConfiguration` object operate as sources of truth, from which all other data/logic/behavior of our routing and animations is derived! This is further solidified by proper Typescript typing - to prevent typos or mis-configurations - and further serves to enforce the system we've built going forward.

![Solid Architecture](https://media1.tenor.com/images/47243fe0e25a368e9de5594864a0e830/tenor.gif?itemid=15718531)

I would venture to say that, even without comments, another engineer would be able to add another route to this app, and without direct communication with us, they'd be able to deduce how to add another route. I base this claim on the  fact that this system is so ubiquitous to our implementation, that it would be much more difficult to rip it out than continue to follow it. And the way that it's built makes the patterns straight-forward to follow and append to.

# Source Code

For a working demo with full source-code, be sure to check out:

{% stackblitz router-animations-example %}

# Where We Could Take This Next

Let's do something fun with this one: I'll commit to writing some more blogs building up on the topics presented here based on the number/kind of up-votes this post gets:

20 💙s: Adding swiping for routing with `hammerjs`
35 🦄s: Explanation and demo of list animations (like the ones in the gif at the beginning and at [zackderose.dev](https://zackderose.dev/blogs)
50 📕s: Adding a vertical dimension to our "spacial familiarity" concept - and using it for children of the existing top-level routes.

That's it! Looking forward to seeing how this turns out!!

# Credit Where It's Due!

Much credit to this article by Gerard Sans for [this article](https://medium.com/google-developer-experts/angular-supercharge-your-router-transitions-using-new-animation-features-v4-3-3eb341ede6c8) where I originally learned about router animations, and (Matias Niemela)[https://www.yearofmoo.com/2017/06/new-wave-of-animation-features.html] for great in-depth explanations of many of the features of `@angular/animations`!! (I believe Matias is also responsible for writing the original code for `@angular/animations`!)

# More Content By Zack

[Blogs](https://zackderose.dev/blogs)
[YouTube](https://www.youtube.com/channel/UCIEY8ETWdW3EX-3nWE-T0dQ)
[Twitch](https://www.twitch.tv/zackderose)
[Twitter](https://twitter.com/zackderose)
[All Video Content Combined](https://zackderose.dev/videos)