#3 Performing the Fade Animation Without Magic Strings
Saturday, June 22, 2019
In this article we will start by defining our css animation classes names for our fade animations in a separate file that we will import into our animation mixins. The purpose for doing this is that we will also export those names out so that we will be able to use them within the script tags in our vue files and within our typescript files. This will mean that we will be able to write them down in one place and then use them where ever we need them. Once that is done we will focus on removing our reliance on using magic strings to communicate which animation we want to run and what class names to apply when. We will finish up by adding the ability to notify a consumer as to what stage the animation is currently on.
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
Say no to strings (as much as possible)
- root
- src
- animations
- _animation-classes.scss
- animations
- src
I have mentioned it many times before that I really do dislike using strings for things and because of this we are going to do our best to minimize them now. To start we are going to create a scss file so that we can pass in our class names to the animation files that need them (a). We will see shortly why we are using a scss extension instead of sass.
_animation-classes.scss
$fade-in-active : fade-in-active;
$fade-in-after : fade-in-after;
$fade-in-before : fade-in-before;
$fade-out-active: fade-out-active;
$fade-out-after : fade-out-after;
$fade-out-before: fade-out-before;
Replacing our css class names
- root
- src
- animations
- _animation-fade.sass
- animations
- src
With our classes names defined we just need to modify our fade animation classes mixins to make use of them (b)
_animation-fade.sass
@import "animation-classes"
...
@mixin animation-classes-fade-in($duration: 200ms)
&.#{$fade-in-active}
@include animation-options($duration)
animation-name: fadeIn
&.#{$fade-in-after}
opacity: 1
&.#{$fade-in-before}
opacity: 0
@mixin animation-classes-fade-out($duration: 200ms)
&.#{$fade-out-active}
@include animation-options($duration)
animation-name: fadeOut
&.#{$fade-out-after}
opacity: 0
&.#{$fade-out-before}
opacity: 1
...
Exporting scss variables to javascript
- root
- src
- animations
- _animation-classes.scss
- animations
- src
The reason that we defined our css class names in a scss file instead of sass file is if it is possible I have not yet figured out how to export them into javascript yet. With that being said we will now export our variables plus a couple of extra that we will need shortly (c).
_animation-classes.scss
...
:export {
fadeIn : fadeIn;
fadeInActive : $fade-in-active;
fadeInAfter : $fade-in-after;
fadeInBefore : $fade-in-before;
fadeOut : fadeOut;
fadeOutActive: $fade-out-active;
fadeOutAfter : $fade-out-after;
fadeOutBefore: $fade-out-before;
}
Modifying our shim
- root
- src
- shims-vue.d.ts
- src
Normally I would prefer to add a new file and not modify one that was created by the framework but I need to spend some time figuring out how to register another shim so for now we will just modify the one already provided (d). This modification will allow us to import the variables from our scss files into our vue and ts files without getting errors about missing modules.
shims-vue.d.ts
...
declare module "*.scss" {
const content: {[className: string]: string};
export default content;
}
Time to give it a try
- root
- src
- animations
- AnimatableItem.vue
- animations
- src
With our shim file modified we can return to our animatable item and modify the way we are assigning the names of our css classes (e).
AnimatableItem.vue
...
import animationClasses from "@/components/animations/_animation-classes.scss";
...
export default class AnimatableItem extends Vue {
...
private animate(animation: string) {
const temp = animationClasses["fadeOut"];
this.afterCssClass = "";
this.beforeCssClass = this.getCssClass(`${temp}Before`);
requestAnimationFrame(() => {
this.activeCssClass = this.getCssClass(`${temp}Active`);
requestAnimationFrame(() => {
this.beforeCssClass = "";
});
});
}
private animationEnd() {
const temp = animationClasses["fadeOut"];
this.afterCssClass = this.getCssClass(`${temp}After`);
requestAnimationFrame(() => {
this.activeCssClass = "";
});
}
...
private getCssClass(index: string) {
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;
}
}
We do what we want tslint
- root
- tslint.json
I do tend to access properties in objects through string keys a lot so we will take a small detour to modify our lint configuration to allow this (f).
tslint.json
{
...
"rules": {
...,
"no-string-literal": false
}
}
Adding some new types
- root
- src
- components
- animations
- types.ts
- animations
- components
- src
We have changed a few things related to the internal implementation of our animatable item but we are still expecting a consumer to pass us a string which we are going to fix now (g). We are fixing this by creating an enum for which animation we want to run and using a map to connect it to a string from the scss variables.
types.ts
export enum AnimationTypes {
None,
FadeIn,
FadeOut,
}
export interface IAnimateOptions {
type: AnimationTypes;
}
Get out of here string
- root
- src
- components
- animations
- AnimatableItem.vue
- animations
- components
- src
With all of that done we are now in a position to modify how a consumer calls our animation method by sending us an instance of animation options instead of a string (h).
AnimatableItem.vue
<script lang="ts">
...
import { AnimationTypes, IAnimateOptions } from "@/components/animations/types";
...
export default class AnimatableItem extends Vue {
@Prop() private readonly subject!: Subject<IAnimateOptions>;
...
private readonly animationMap = new Map<AnimationTypes, string>([
[AnimationTypes.FadeIn, animationClasses["fadeIn"]],
[AnimationTypes.FadeOut, animationClasses["fadeOut"]],
]);
...
private type = AnimationTypes.None;
...
private animate(options: IAnimateOptions) {
this.type = options.type;
const animation = this.animationMap.get(this.type);
this.afterCssClass = "";
this.beforeCssClass = this.getCssClass(`${animation}Before`);
requestAnimationFrame(() => {
this.activeCssClass = this.getCssClass(`${animation}Active`);
requestAnimationFrame(() => {
this.beforeCssClass = "";
});
});
}
private animationEnd() {
const animation = this.animationMap.get(this.type);
this.afterCssClass = this.getCssClass(`${animation}After`);
requestAnimationFrame(() => {
this.activeCssClass = "";
});
this.type = AnimationTypes.None;
}
...
}
</script>
Make things easier to use
- root
- src
- components
- animations
- index.ts
- animations
- components
- src
Since we have a few different moving parts that need to be used we can make them easier to import by adding in and index file and exporting them (i).
index.ts
export { default as AnimatableItem } from "@/components/animations/AnimatableItem.vue";
export * from "@/components/animations/types";
Update the splash screen
- root
- src
- components
- TheSplashScreen.vue
- components
- src
With those changes made we can return to our splash screen and update our import statement as well as how we are notifying our animatable object which animation it should run (j).
TheSplashScreen.vue
<script lang="ts">
...
import { AnimatableItem, AnimationTypes, IAnimateOptions } from "@/components/animations";
...
export default class TheSplashScreen extends Vue {
private readonly animationSubject = new Subject<IAnimateOptions>();
...
private mounted() {
setTimeout(() => {
this.animationSubject.next({ type: AnimationTypes.FadeOut });
}, 2000);
}
}
</script>
Almost there
- root
- src
- components
- animations
- types.ts
- animations
- components
- src
If we refresh the page we will see that our splash screen does in fact fade out as we expect and we have even removed all of the string references that we have been using. But we still have a problem. If you attempt to interact with any of the content of the app, for instance click on a link, you will find that you are unable to do so. The problem is although the splash screen is not visible it is still there and is intercepting the user input. There are a few ways we could handle this. The way that we will do it is to add the ability to be notified about what stage the animation is at and run some additional code selectively. First up then is to add a new enum to represent the stages (k).
types.ts
...
export enum AnimationStages {
ActiveApplyPost,
ActiveApplyPre,
ActiveRemovePost,
ActiveRemovePre,
AfterApplyPost,
AfterApplyPre,
BeforeApplyPost,
BeforeApplyPre,
BeforeRemovePost,
BeforeRemovePre,
}
...
Notification of the stage
- root
- src
- components
- animations
- AnimatableItem.vue
- animations
- components
- src
Now back to our animatable item. The first thing we will do is add a prop to our component
so that a consumer can specify a function for us to call in order to update them as to what
stage the animation is on. I tried a few different things to get the Prop
decorator to work in the case where the prop is a function and I want to specify a default value. I
was unable to get it working so we will just resort to adding it to the component decorator manually
(l). Now that we have a gauranteed not
undefined value we can just simply call the function anytime we update the stage.
AnimatableItem.vue
<script lang="ts">
...
import {
...
AnimationStages,
...
} from "@/components/animations/types";
...
@Component({
props: {
update: {
type: Function,
default: () => {},
}
},
})
export default class AnimatableItem extends Vue {
public $props!: Vue["$props"] & { update: (x: AnimationStages) => void }
...
private animate(options: IAnimateOptions) {
this.type = options.type;
const animation = animationMap.get(this.type);
this.afterCssClass = "";
this.updateWithPreAndPost(
AnimationStages.BeforeApplyPre,
AnimationStages.BeforeApplyPost,
() => this.beforeCssClass = this.getCssClass(`${animation}Before`));
requestAnimationFrame(() => {
this.updateWithPreAndPost(
AnimationStages.ActiveApplyPre,
AnimationStages.ActiveApplyPost,
() => this.activeCssClass = this.getCssClass(`${animation}Active`));
requestAnimationFrame(() => {
this.updateWithPreAndPost(
AnimationStages.BeforeRemovePre,
AnimationStages.BeforeRemovePost,
() => this.beforeCssClass = "");
});
});
}
private animationEnd() {
const animation = animationMap.get(this.type);
this.updateWithPreAndPost(
AnimationStages.AfterApplyPre,
AnimationStages.AfterApplyPost,
() => this.afterCssClass = this.getCssClass(`${animation}After`));
requestAnimationFrame(() => {
this.updateWithPreAndPost(
AnimationStages.ActiveRemovePre,
AnimationStages.ActiveRemovePost,
() => this.activeCssClass = "");
});
this.type = AnimationTypes.None;
}
private updateWithPreAndPost(pre: AnimationStages, post: AnimationStages, apply: () => void) {
this.$props.update(pre);
apply();
this.$props.update(post);
}
}
</script>
The vanishing splash screen
- root
- src
- components
- TheSplashScreen.vue
- components
- src
Now that we have the ability to be notified about the animation stage we can modify our
splash screen so that it can be removed from the DOM after it is animated out. First up
is to modify our template (m) to use the
v-if
directive.
TheSplashScreen.vue
<template lang="pug">
AnimatableItem.splash-screen(
...
v-bind:update="updateAnimationStage"
v-if="!isAnimatedOut")
...
</template>
It sounds as good a time as any to remove the splash screen after the application of the after css class (n).
TheSplashScreen.vue
<script lang="ts">
...
import AnimatableItem, {
AnimationStages,
...
} from "@/components/animations";
...
export default class TheSplashScreen extends Vue {
...
private isAnimatedOut = false;
...
private updateAnimationStage(stage: AnimationStages) {
switch(stage) {
case AnimationStages.AfterApplyPost:
this.isAnimatedOut = true;
break;
}
}
}
</script>
Tidying up
- root
- src
- types.ts
- src
If you paid careful attention to the code in (l)
you saw that we added some information to the script to type the $props
property. There is a good chance we will need to do this again so we might as well make it easier to do
(o).
types.ts
import Vue from "vue";
export type Props<T extends object> = Vue["$props"] & T;
Typing our props
- root
- src
- animations
- AnimatableItem.vue
- animations
- src
With the addition we just made we can now type our props much more easily as shown in (p).
AnimatableItem.vue
<script lang="ts">
...
import { Props } from "@/types";
...
export default class AnimatableItem extends Vue {
...
public $props!: Props<{ update: (x: AnimationStages) => void }>;
...
}
</script>