Jared Youtsey | ng-conf | Oct 2019
Add style to your application by animating your route transitions!
For this article I’m going to assume you already understand the basics of Angular routing and components. I won’t bore you with building an entire application. We’ll get right to adding animations so you can see immediate results!
The finished code for this example can be found here.
Add BrowserAnimationsModule
In your app.module.ts
add BrowserAnimationsModule
to the module imports
.
...
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
imports: [
...,
BrowserAnimationsModule
],
...
})
export class AppModule {}
A Note On Unit Testing
For unit testing, import NoopAnimationsModule
instead. This fulfills the contracts while isolating unit tests from having to deal with the transitions.
Animation Affects User Experience
Have you ever seen a PowerPoint presentation that had a thousand different transitions, fonts, and colors? Yuck. Take a lesson and keep your transitions simple and consistent to avoid confusing or overwhelming your users.
The Premise
For this example, I’ll present a simple set of animations that make sense in the context of navigating forward and backward. Views are animated left or right based on the direction the router is navigating. We’ll have three components named OneComponent
, TwoComponent
, and ThreeComponent
, for simplicity’s sake. When navigating from One to Two, One will slide out to the left while Two will slide in from the right. Two to Three will do the same. When navigating from Three to Two the animations will be reversed. In addition, the opacity of the views will be animated as they leave and enter the page.
States, Transitions, and Triggers, Oh My!
State is a static style definition. A transition defines how a property in the style will change. A trigger defines what action will cause one state to transition to another state.
- State = What
- Transition = How
- Trigger = When
- “animation” = Triggered transition(s) from one state to another.
Router Configuration
To connect animations to the router we must add a data
property to the route configuration. Here are our modified routes:
const routes: Routes = [
{
path: '',
children: [
{
path: 'one',
component: OneComponent,
data: { animationState: 'One' }
},
{
path: 'two',
component: TwoComponent,
data: { animationState: 'Two' }
},
{
path: 'three',
component: ThreeComponent,
data: { animationState: 'Three' }
},
{
path: '**',
redirectTo: 'one'
}
]
},
{
path: '**',
redirectTo: 'one'
}
];
The name animationState
is arbitrary. But, you’ll need to keep track of what you use. I’ve used this name because we’re defining what animation state this route represents. State = What.
AppComponent Configuration
Start by configuring the AppComponent
to set up the animations for the route changes. In app.component.ts
add a method:
prepareRoute(outlet: RouterOutlet) {
return outlet &&
outlet.activatedRouteData &&
outlet.activatedRouteData['animationState'];
}
Notice the check for a route with data for the state specified property, animationState
.
Now, hook up the template. First, let’s add a template variable so that we can get a reference to the <router-outlet>
.
<router-outlet #outlet="outlet"></router-outlet>
Next, add a synthetic property to the container element of the <router-outlet>
. It’s critical that it be on a container div, not on the <router-outlet>
itself. This synthetic property’s name is arbitrary, but it’s good to understand that it will correspond to an animation trigger’s name. For the sake of this example, let’s call it triggerName
.
<div [@triggerName]="prepareRoute(outlet)">
<router-outlet #outlet="outlet"></router-outlet>
</div>
We pass the method prepareRoute
with the argument of the template variable outlet
to the synthetic property @triggerName
.
At this point, if you run the application, you’ll find that there is an error in the console:
ERROR Error: Found the synthetic property @triggerName. Please
include either "BrowserAnimationsModule" or "NoopAnimationsModule"
in your application.
But, wait, we did that already?! Angular is confused because we haven’t actually defined the trigger yet! So, let’s do that now.
Define the Animation
Remember, an animation is caused by a trigger that causes a transition from one state to another state. When we define an animation we start with the trigger and work inward on that definition.
Create a new file named route-transition-animations.ts
next to app.component.ts
. This will contain the trigger definition, triggerName
, and the transitions from and to the states we wish to animate.
import { trigger } from '@angular/animations';
export const routeTransitionAnimations = trigger('triggerName', []);
Here we finally define the trigger triggerName
! The array argument is where we will define the transitions.
Before we define the transitions, let’s hook the app.component.ts
to the trigger definition:
...
import { routeTransitionAnimations } from './route-transition-animations';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [routeTransitionAnimations]
})
export class AppComponent {...}
Now, let’s go back and flesh out the trigger’s transitions in the route-transition-animations.ts
.
Angular uses simple arrow syntax to define the transition from one state to another. For example, if we want to handle the navigation from One to Two we use One => Two
. If we want to handle both directions, we can use a bi-directional arrow, One <=> Two
, and then the transition will be applied going from One to Two and from Two to One.
Angular has some powerful pre-defined concepts in addition to the named states.
-
void
= an element is entering or leaving the view. -
*
= any state -
:enter
and:leave
are aliases for thevoid => *
and* => void
transitions.
Let’s review the animations we wanted at the beginning of the article. One => Two
and Two => Three
should slide the previous view off to the left and bring the new view in from the right. Since they both have the same transition, both state changes can be defined in a single transition using comma separated values:
import { trigger, transition } from '@angular/animations';
export const routeTransitionAnimations = trigger('triggerName', [
transition('One => Two, Two => Three', [])
]);
Now, for the actual transformation! First, notice what the official Angular documentation has to say:
During a transition, a new view is inserted directly after the old one and both elements appear on screen at the same time. To prevent this, apply additional styling to the host view, and to the removed and inserted child views. The host view must use relative positioning, and the child views must use absolute positioning. Adding styling to the views animates the containers in place, without the DOM moving things around.
Apply this to the style definition by adding the following:
import { trigger, transition, style, query } from '@angular/animations';
export const routeTransitionAnimations = trigger('triggerName', [
transition('One => Two, Two => Three', [
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
right: 0,
width: '100%'
})
])
])
]);
First, style({ position: ‘relative’ })
sets the style on the element that is the target of the trigger to be position: relative
. The target element is the one with the synthetic property @triggerName
, which is the div that contains the <router-outlet>
. Now, the “host view” is using relative positioning per the official docs.
Next, query(':enter, :leave', [...])
. This means “query for child elements that are entering or leaving the view.” Then it applies the following style definition to those elements. I won’t dive too much into the CSS solution for the positions, but the real key is that we are setting the child elements to absolute positioning, per the official docs. Your CSS will almost certainly differ at this point based on your chosen animation style and application DOM layout.
Now, we need to define the individual transitions, in order. These will follow the first query
in the transition
's array arguments.
This query defines what the start state is for the view that is entering, positioning it off screen to the far right:
query(':enter', [style({ right: '-100%', opacity: 0 })]),
The next query ensures that any child component animations that need to happen on the leaving component happen before the leaving view animates off screen:
query(':leave', animateChild()),
Next, we group the leave and enter together so that these transitions happen in unison (otherwise, the old would leave, leaving a blank space, and then the new would enter). We animate
, meaning “transition existing styles to the specified styles over a period of time with an easing function.” The leaving view animates its right
value to be 100% (the far left of the screen) and the entering animate’s its right
value to be 0% (the far right of the screen):
group([
query(':leave', [animate('1s ease-out', style({ right: '100%', opacity: 0 }))]),
query(':enter', [animate('1s ease-out', style({ right: '0%', opacity: 1 }))])
]),
At this point, the old view has left, the new one has entered, and we want to trigger any child animations on the new view:
query(':enter', animateChild())
And here is what that looks like:
Now, add the transition for the reverse direction, Three => Two
, and Two => One
, after the first transition, and change the right
’s to left
's:
transition('Three => Two, Two => One', [
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
]),
query(':enter', [style({ left: '-100%', opacity: 0 })]),
query(':leave', animateChild()),
group([
query(':leave', [animate('1s ease-out', style({ left: '100%', opacity: 0 }))]),
query(':enter', [animate('1s ease-out', style({ left: '0%', opacity: 1 }))])
]),
query(':enter', animateChild())
])
Here is the result:
Looking good! We’re just missing two transition definitions, One => Three
, and Three => One
. Rather than defining something different, we will add these to the existing ones. Add One => Three
to the right definition, and the Three => One
to the left
. The transitions now look like this:
transition('One => Two, Two => Three, One => Three', [...]),
transition('Three => Two, Two => One, Three => One', [...])
And the results:
Voila! Successful Angular route transition animations!
Here is the whole trigger/transition definition.
This just scratches the surface of what can be done with Angular animations. Check out my other article on Animating Angular’s *ngIf and *ngFor to have more fun with Angular animations!
ng-conf: Join us for the Reliable Web Summit
Come learn from community members and leaders the best ways to build reliable web applications, write quality code, choose scalable architectures, and create effective automated tests. Powered by ng-conf, join us for the Reliable Web Summit this August 26th & 27th, 2021.
https://reliablewebsummit.com/
Top comments (0)