Advertisement

#3 Performing the Fade Animation Without Magic Strings

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.

Say no to strings (as much as possible)

  • root
    • src
      • animations
        • _animation-classes.scss

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;
(a) Defining the names of our css classes so that we can import them into our animation files.

Replacing our css class names

  • root
    • src
      • animations
        • _animation-fade.sass

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
...
(b) We can now use those constants and string interpolation to define our css class names.

Exporting scss variables to javascript

  • root
    • src
      • animations
        • _animation-classes.scss

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;
}
(c) Using a scss file allows us to export the class names to any javascript file that needs them.

Modifying our shim

  • root
    • src
      • shims-vue.d.ts

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;
}
(d) Updating our shim so that we do not get errors when importing variables from our scss files.

Time to give it a try

  • root
    • src
      • animations
        • AnimatableItem.vue

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;
    }
}
(e) Assigning our css classes using the variables passed to our vue file from our scss file.

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
  }
}
(f) Updating our lint configuration so that it will not complain about us accessing properties of object through a string key.

Adding some new types

  • root
    • src
      • components
        • animations
          • types.ts

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;
}
(g) Adding a mapping between our animation type enum and the appropriate variable from our scss file.
Advertisement

Get out of here string

  • root
    • src
      • components
        • animations
          • AnimatableItem.vue

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>
(h) Updating our animate method to use IAnimateOptions instead of a string to notify our animatable object as to which animation it should use.

Make things easier to use

  • root
    • src
      • components
        • animations
          • index.ts

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";
(i) We can make things easier for a consumer by exporting everything from an index file.

Update the splash screen

  • root
    • src
      • components
        • TheSplashScreen.vue

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>
(j) Adjusting our import statement and our call to the animatable object.

Almost there

  • root
    • src
      • components
        • animations
          • types.ts

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,
}
...
(k) Adding a stages enum that we will use to notify a consumer as to what stage the animation is currently on.

Notification of the stage

  • root
    • src
      • components
        • animations
          • AnimatableItem.vue

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>
(l) Adding the ability to notify a consumer as to what stage the animation is on.

The vanishing splash screen

  • root
    • src
      • components
        • TheSplashScreen.vue

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>
(m) Adding the v-if directive so that we can remove the splash screen from the DOM when the animation is done playing.

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>
(n) Setting the removal of the splash screen to occur post the application of the after css class.

Tidying up

  • root
    • src
      • types.ts

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;
(o) Adding a new Props type so that we can more easily type our components props property.

Typing our props

  • root
    • src
      • animations
        • AnimatableItem.vue

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>
(p) Updating the way that we type our props property.
Exciton Interactive LLC
Advertisement