#6 Animating the Navigation Between Views
Sunday, July 14, 2019
In this article we will convert from specifying our animation css class explicitly to creating them based on a convention. This change will make it much easier to add new animations. Following that change we will create the ability for us to animation elements in and out in the x direction. We use this along with the notion of a parent for our routes to conditionally decide which direction our views should be animated in and out when a user requests a route change.
Parts
- Part 45: Adjusting Shares
- Part 44: Plan Percentages
- Part 43: Home Securities
- Part 42: Updating Plans
- Part 41: Plan Details View
- Part 40: Portfolio Getters
- Part 39: Portfolio Plan
- Part 38: Portfolio Category
- Part 37: Account Securities
- Part 36: Account Transfer
- Part 35: View Account Security
- Part 34: Updating Deposit
- Part 33: View Account Deposit
- Part 32: Display Account Details
- Part 31: Account Getters
- Part 30: Deposits And Securities
- Part 29: Add Accounts Details
- Part 28: Refactoring Accounts
- Part 27: Add Security Models
- Part 26: Edit Security Details
- Part 25: View Security Details
- Part 24: Navigating To Details
- Part 23: Getters Validation
- Part 22: Query Parameters
- Part 21: Tab Entries
- Part 20: Tab Header
- Part 19: List View
- Part 18: Vuex Getters
- Part 17: End Domain Model
- Part 16: Start Domain Model
- Part 15: Pop Routes
- Part 14: Push Routes
- Part 13: Removing Accounts
- Part 12: Vuex (Decorators)
- Part 11: Vuex (Accounts)
- Part 10: The App Bar (Settings)
- Part 9: Remove Consumer Rxjs
- Part 8: The App Bar (Back)
- Part 7: Structuring Our App
- Part 6: Animation Between Views
- Part 5: Navigation Fade
- Part 4: Navigation Requests
- Part 3: Fade Animations (cont.)
- Part 2: Fade Animations
- Part 1: Splash Screen
It was fun while it lasted
- root
- src
- components
- animations
- _animation-classes.scss
- animations
- components
- src
Originally we decided to use a system where we would explicitly specify all of the animation classes in our sass file and then export them for use in our script. This would be fine in principle but it has not taken long for me to decide it is more trouble than it is worth. Instead of specifying them individually we are going to take a more convention based approach. We are going to start by converting what did previously starting with our definitions of the css classes (a).
_animation-classes.scss
@function active($name) {
@return $name + -active;
}
@function after($name) {
@return $name + -after;
}
@function before($name) {
@return $name + -before;
}
$fadeIn : fade-in;
$fadeOut: fade-out;
Applying the convention to the fade animation
- root
- src
- component
- animations
- _animation-fade.sass
- animations
- component
- src
In accordance with our new approach we need to change how the classes are expressed within our fade animation (b). Notice that we are using the helper functions that we defined previously to help us construct the classes.
_animation-fade.sass
...
@mixin animation-classes-fade-in($duration: 200ms, $easing: ease-in-out)
&.#{active($fadeIn)}
@include animation-options($duration, $easing)
animation-name: fadeIn
&.#{after($fadeIn)}
opacity: 1
&.#{before($fadeIn)}
opacity: 0
@mixin animation-classes-fade-out($duration: 200ms, $easing: ease-in-out)
&.#{active($fadeOut)}
@include animation-options($duration, $easing)
animation-name: fadeOut
&.#{after($fadeOut)}
opacity: 0
&.#{before($fadeOut)}
opacity: 1
...
Applying the convention to the animatable item
- root
- src
- components
- animations
- Animatable.vue
- animations
- components
- src
The last thing that we need to update before everything works again is our animatable object.
There are two changes that we need to make one is to update our map and then to change how
we create the string that will become the css classes
(c). Once that is done we can also
remove the getCssClass
helper method as it is no
longer needed.
AnimatableItem.vue
<script lang="ts">
...
export default class AnimatableItem extends Vue {
...
private readonly animationMap = new Map<AnimationTypes, string>([
[AnimationTypes.FadeIn, "fade-in"],
[AnimationTypes.FadeOut, "fade-out"],
]);
...
private animate(options: IAnimateOptions) {
...
this.updateWithPreAndPost(
AnimationStages.BeforeApplyPre,
AnimationStages.BeforeApplyPost,
() => this.beforeCssClass = `${animation}-before`);
requestAnimationFrame(() => {
this.updateWithPreAndPost(
AnimationStages.ActiveApplyPre,
AnimationStages.ActiveApplyPost,
() => this.activeCssClass = `${animation}-active`);
...
});
}
private animationEnd() {
...
this.updateWithPreAndPost(
AnimationStages.AfterApplyPre,
AnimationStages.AfterApplyPost,
() => this.afterCssClass = `${animation}-after`);
...
}
...
private getCssClass(index: string) { // <-- Remove this method
const css = animationClasses[index];
if (typeof(css) === "undefined") {
// tslint:disable-next-line:no-console
console.warn(`CSS animation class for ${index} is undefined.`);
return "";
}
return css;
}
}
</script>
Time for a new animation
- root
- src
- components
- animations
- _animation-classes.scss
- animations
- components
- src
With all of those changes made we can no proceed to create some new animations. We will start by defining the base of the css classes that we will be using (d).
_animation-classes.scss
...
$translateInFromLeft : translate-in-from-left;
$translateInFromRight: translate-in-from-right;
$translateOutToLeft : translate-out-to-left;
$translateOutToRight : translate-out-to-right;
Need some css for our translate animations to work
- root
- src
- components
- animations
- _animation-translate.sass
- animations
- components
- src
Here we are following the pattern that we created when we made the fade animations previously. We start by defining the key frames for translating an element in and out from both the left and right sides. After that we say what css properties should be applied to the element when each of the animation classes are applied to it. Of course we also provide an easy way to import all of the key frames and all of the classes at one time (e).
_animation-translate.sass
@import "animation-classes"
@import "animation-options"
@mixin animation-keyframes-translate-in-from-left
@keyframes translateInFromLeft
from
transform: translateX(-100%)
to
transform: translateX(0)
@mixin animation-keyframes-translate-in-from-right
@keyframes translateInFromRight
from
transform: translateX(100%)
to
transform: translateX(0)
@mixin animation-keyframes-translate-out-to-left
@keyframes translateOutToLeft
from
transform: translateX(0)
to
transform: translateX(-100%)
@mixin animation-keyframes-translate-out-to-right
@keyframes translateOutToRight
from
transform: translateX(0)
to
transform: translateX(100%)
@mixin animation-keyframes-translate
@include animation-keyframes-translate-in-from-left
@include animation-keyframes-translate-in-from-right
@include animation-keyframes-translate-out-to-left
@include animation-keyframes-translate-out-to-right
@mixin animation-classes-translate-in-from-left($duration: 200ms, $easing: ease-in-out)
&.#{active($translateInFromLeft)}
@include animation-options($duration, $easing)
animation-name: translateInFromLeft
&.#{after($translateInFromLeft)}
transform: translateX(0)
&.#{before($translateInFromLeft)}
transform: translateX(-100%)
@mixin animation-classes-translate-in-from-right($duration: 200ms, $easing: ease-in-out)
&.#{active($translateInFromRight)}
@include animation-options($duration, $easing)
animation-name: translateInFromRight
&.#{after($translateInFromRight)}
transform: translateX(0)
&.#{before($translateInFromRight)}
transform: translateX(100%)
@mixin animation-classes-translate-out-to-left($duration: 200ms, $easing: ease-in-out)
&.#{active($translateOutToLeft)}
@include animation-options($duration, $easing)
animation-name: translateOutToLeft
&.#{after($translateOutToLeft)}
transform: translateX(-100%)
&.#{before($translateOutToLeft)}
transform: translateX(0)
@mixin animation-classes-translate-out-to-right($duration: 200ms, $easing: ease-in-out)
&.#{active($translateOutToRight)}
@include animation-options($duration, $easing)
animation-name: translateOutToRight
&.#{after($translateOutToRight)}
transform: translateX(100%)
&.#{before($translateOutToRight)}
transform: translateX(0)
@mixin animation-classes-translate($duration: 200ms, $easing: ease-in-out)
@include animation-classes-translate-in-from-left($duration, $easing)
@include animation-classes-translate-in-from-right($duration, $easing)
@include animation-classes-translate-out-to-left($duration, $easing)
@include animation-classes-translate-out-to-right($duration, $easing)
We want them all
- root
- src
- components
- animations
- _animations.sass
- animations
- components
- src
Now it is just a small change to our animation file for us to make it easy to import all of the necessary components of our translate animations (f).
_animation.sass
...
@import "animation-translate"
@mixin animation-keyframes
...
@include animation-keyframes-translate;
@mixin animation-classes($selector, $duration: 200ms, $easing: ease-in-out)
#{$selector}
...
@include animation-classes-translate($duration, $easing)
...
Back to our scripts
- root
- src
- components
- animations
- types.ts
- animations
- components
- src
In our typescript code we will start by adding four new entries to our animation types enum (g).
types.ts
...
export enum AnimationTypes {
...,
TranslateInFromLeft,
TranslateInFromRight,
TranslateOutToLeft,
TranslateOutToRight,
}
...
Last step for our translate animations
- root
- src
- components
- animations
- AnimatableItem.vue
- animations
- components
- src
The last thing that we have to do before our animations will work is to update our map (h).
AnimatableItem.vue
<script lang="ts">
...
export default class AnimatableItem extends Vue {
...
private readonly animationMap = new Map<AnimationTypes, string>([
...,
[AnimationTypes.TranslateInFromLeft, "translate-in-from-left"],
[AnimationTypes.TranslateInFromRight, "translate-in-from-right"],
[AnimationTypes.TranslateOutToLeft, "translate-out-to-left"],
[AnimationTypes.TranslateOutToRight, "translate-out-to-right"],
]);
...
}
</script>
Translate instead of fade
- root
- src
- components
- routing
- TheRouterOutlet.vue
- routing
- components
- src
In order to test that everything is working correctly we will update our router outlet by changing the animation that we want to run from fade to translate out to the left before a route change is requested and to translate in from the right afterwards (i).
TheRouterOutlet.vue
<script lang="ts">
...
export default class TheRouterOutlet extends Vue {
...
private animate(route: Routes) {
...
this.animationSubject.next({ type: AnimationTypes.TranslateOutToLeft });
}
private animationComplete() {
...
this.animationSubject.next({ type: AnimationTypes.TranslateInFromRight });
}
...
}
</script>
Adding structure to our routes
- root
- src
- components
- routing
- route-entry.ts
- routing
- components
- src
Next up is to add the ability to determine the direction our views should animate in and out from/to. In order to do this we will need to add some structure to our routes. For now we will just introduce the concept of a parent for a route (j). With this addition we will create a tree structure for our routes.
route-entry.ts
...
export interface IRouteEntryConfig {
...
parent: RouteEntry | null;
...
}
export class RouteEntry {
...
private readonly _parent: RouteEntry | null;
...
constructor(config: IRouteEntryConfig) {
...
this._parent = config.parent;
...
}
public isChildOf(entry: RouteEntry) {
return this._parent === entry;
}
}
Updating the routing service
- root
- src
- components
- routing
- routing-service.ts
- routing
- components
- src
Now we need to add a parent entry to the configuration objects when we create our route entries. As you can see we will be treating the home view as the root of our tree (k).
routing-service.ts
...
const home = new RouteEntry({
...,
parent: null,
...,
});
const about = new RouteEntry({
...,
parent: home,
...,
});
...
Pick a direction
- root
- src
- components
- routing
- TheRouterOutlet.vue
- routing
- components
- src
Now that we have a notion of a parent for a route we can use it to decide which direction we should animate our views in and out from/to (l). For now at least if we animate to a view other than a direct parent of child we will just use the fade animation.
TheRouterOutlet.vue
<script lang="ts">
...
export default class TheRouterOutlet extends Vue {
...
private inAnimation = AnimationTypes.None;
...
private animate(route: Routes) {
...
const from = this.routingService.current;
if (from.isChildOf(this.toEntry)) {
this.inAnimation = AnimationTypes.TranslateInFromLeft;
this.animationSubject.next({ type: AnimationTypes.TranslateOutToRight });
} else if (this.toEntry.isChildOf(from)) {
this.inAnimation = AnimationTypes.TranslateInFromRight;
this.animationSubject.next({ type: AnimationTypes.TranslateOutToLeft });
} else {
this.inAnimation = AnimationTypes.FadeIn;
this.animationSubject.next({ type: AnimationTypes.FadeOut });
}
}
private animationComplete() {
...
this.animationSubject.next({ type: this.inAnimation });
this.inAnimation = AnimationTypes.None;
}
...
}
</script>
Overflow in the x direction
- root
- src
- components
- routing
- TheRouterOutlet.vue
- routing
- components
- src
If we run the translate animations using the mobile device emulator of our browser everything will appear to be running without a hitch. The problem comes if you run it without the emulation. Since we are translating the view in and our horizontally we will see that the horizontal scrollbar appears while the animation is playing. We can fix this by adding a flag that will tell us if there is an animation currently playing and by using that flag to apply a css class. First up is some modification of our template (m). We need to wrap our animatable item in another element and apply a new class to it.
TheRouterOutlet.vue
<template lang="pug">
div.router-view(v-bind:class="{ 'is-animating': isAnimating }")
AnimatableItem.router-view-animatable(...)
...
</template>
Next up is modifying our script to include a new field and a computed property. The computed property is just the combination of our previous animating out field and our new animating in field. When an animation is started the animate our animation will complete which will trigger the animating our flag to be set to false. Once that animate in animation is complete the animating in flag will then be set to false as well (n).
TheRouterOutlet.vue
<script lang="ts">
...
export default class TheRouterOutlet extends Vue {
...
private isAnimatingIn = false;
...
private get isAnimating() { return this.isAnimatingIn || this.isAnimatingOut; }
...
private animationComplete() {
if (this.isAnimatingIn) {
this.isAnimatingIn = false;
}
...
this.isAnimatingIn = true;
...
}
...
}
</script>
The final step is to add just a little bit of styling to make sure the horizontal scrollbar is not shown when an animation is running (o).
TheRouterOutlet.vue
<style lang="sass" scoped>
.router-view
height: 100%
&.is-animating
overflow-x: hidden
.router-view-animatable
height: 100%
</style>