Advertisement

#9 Removing Our Dependency On Rxjs From Animatable Item Consumers

In this article we are going to focus on removing the explicit dependency for rxjs from our consumer facing api. This will have two immediate benefits. The first is the fact that we are using rxjs is an implementation detail that we may choose to change in the future and if we did that the way everything sits right now it would break any of the code that is using our animatable item. The second is that it helps with discoverability. What I mean by that is that if you use intellisense to see what properties and methods are available to you on the rxjs subject you will see a whole bunch of stuff of which the only thing we are really expecting to be used is the next method. We will also finish up by adding the ability to be notified when a particular animation has completed which will help simplify the router outlet code.

Wrapping the Rxjs Subject

  • root
    • src
      • components
        • animations
          • animation-subject.ts

The first thing we are going to do is introduce a new class that will be used to handle the interaction with our animatable item (a).

animation-subject.ts

import { Subject } from "rxjs";

import {
    IAnimateOptions,
} from "@/components/animations/types";

export class AnimationSubject {
    private _subject = new Subject<IAnimateOptions>();

    public get subject() { return this._subject; }

    public asObservable = () => {
        return this._subject.asObservable();
    }

    public complete = () => {
        this._subject.complete();
    }

    public next = (options: IAnimateOptions) => {
        this._subject.next(options);
    }
}
(a) Our animation subject will be the object that a consumer uses to interact with our animatable item.

Add it to our index

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

Just to keep making things as simple as possible to use we shouldn't forget to add our new class to our index file for exporting (b).

index.ts

...
export * from "@/components/animations/animation-subject";
...
(b) Add our animation subject to our index file for easy importing.

Updating our animatable item

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

To update our animatable item it is a simple matter of importing our new class and changing the constructor of the animation subject (c).

AnimatableItem.vue

<script lang="ts">
...
import { AnimationSubject } from "@/components/animations/animation-subject";
...
export default class AnimatableItem extends Vue {
    ...
    @Prop() private readonly subject!: AnimationSubject;
    ...
}
</script>
(c) Importing our new animation subject class and updating the constructor of the animation subject field.

Remove rxjs from the splash screen

  • root
    • src
      • components
        • TheSplashScreen.vue

With all of that done we can no remove the importing of rxjs from our splash screen and convert the subject to our new animation subject class (d).

TheSplashScreen.vue

<script lang="ts">
...
import { Subject } from "rxjs"; // <-- remove

import {
    AnimatableItem,
    AnimationSubject,
    AnimationTypes,
} from "@/components/animations";
...
export default class TheSplashScreen extends Vue {
    private readonly animationSubject = new AnimationSubject();
    ...
}
</script>
(d) Removing the rxjs import and changing the animation subject field to our new class.

Remove rxjs from the routing outlet

  • root
    • src
      • components
        • routing
          • TheRouterOutlet.vue

Next we will do the exact same thing with our routing outlet (e).

TheRoutingOutlet.vue

<script lang="ts">
...
import { Subject } from "rxjs"; // <-- remove

import {
    AnimatableItem,
    AnimationSubject,
    AnimationTypes,
} from "@/components/animations";
...
export default class TheRoutingOutlet extends Vue {
    ...
    private readonly animationSubject = new AnimationSubject();
    ...
}
</script>
(e) Removing the rxjs import and changing the animation subject field to our new class.

Separating the consumer options from our internal options

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

Next up we can simplify the invoking of an animation by modifying the method for invoking one. The first thing that I do not like is any time we want to run an animation we have to at the very least specify which animation it is and to do this we need to construct an object with a single field to do it. To change this we will introduce a new interface for the options that can be passed into our next method on our animation subject (f). For this moment it will just be empty but we will add a property to it very shortly.

types.ts

...
export interface IAnimationSubjectOptions {

}

export interface IAnimateOptions extends IAnimationSubjectOptions  {
    ...
}
...
(f) Creating a new interface type that we can pass into our next method.

  • root
    • src
      • components
        • animations
          • animation-subject.ts

Now we can modify our next method by requiring the type to be provided as the first argument and then allowing the user to specify or not any options (g).

animation-subject.ts

...
import {
    AnimationTypes,
    ...,
    IAnimationSubjectOptions,
} from "@/components/animations/types";

export class AnimationSubject {
    ...
    public next = (type: AnimationTypes,  options?: IAnimationSubjectOptions) => {
        if (typeof(options) !== "undefined") {
            this._subject.next({
                type,
                ...options,
            });
            return;
        }
        this._subject.next({ type });
    }
}
(g) Changing the next method to require the type of animation as the first argument and allowing for options to be passed in as well.
Advertisement

Back to updating our splash screen

  • root
    • src
      • components
        • TheSplashScreen.vue

Again we find ourselves updating our consumer code which fortunately as before is pretty simple to do. All we have to do is just pass in the animation type to the next method (h).

TheSplashScreen.vue

<script lang="ts">
...
export default class TheSplashScreen extends Vue {
    ...
    private mounted() {
        setTimeout(() => {
            this.animationSubject.next(AnimationTypes.FadeOut);
        }, 2000);
    }
}
</script>
(h) Updating the call to our next method by just passing in the animation type instead of wrapping it in an object.

Back to updating our router outlet

  • root
    • src
      • components
        • TheRouterOutlet.vue

Of course same as with the splash screen we need to update our router outlet by modifying all of the calls to our next method (i).

TheRouterOutlet.vue

<script lang="ts">
...
export default class TheRouterOutlet extends Vue {
    ...
    private animate(route: Routes) {
        ...
        if (from.isChildOf(this.toEntry)) {
            ...
            this.animationSubject.next(AnimationTypes.TranslateOutToRight);
        } else if (this.toEntry.isChildOf(from)) {
            ...
            this.animationSubject.next(AnimationTypes.TranslateOutToLeft);
        } else {
            ...
            this.animationSubject.next(AnimationTypes.FadeOut);
        }
    }

    private animationComplete() {
        ...
        this.animationSubject.next(this.inAnimation);
        ...
    }
}
</script>
(i) Updating all the calls to the next method by just passing in the animation type instead of wrapping them in object.

Preparing our animatable item to use the options

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

In order to make use of the options that may be passed in we are going to need to keep track of them so instead of just remembering the animation type that is passed in we will just save the options themselves and make the necessary changes (j).

AnimatableItem.vue

<script lang="ts">
...
export default class AnimatableItem extends Vue {
    ...
    private options: IAnimateOptions = { type: AnimationTypes.None };
    private type = AnimationTypes.None; // <-- remove
    ...
    private animate(options: IAnimateOptions) {
        this.options = options;
        ...
        const animation = this.animationMap.get(this.options.type);
        ...
    }

    private animationEnd() {
        const animation = this.animationMap.get(this.options.type);
        ...
        requestAnimationFrame(() => {
            ...
            this.options.type = AnimationTypes.None;
        });
    }
}
</script>
(j) Instead of saving the animation type that is passed in we will keep track of the options object itself.

Notification of completion for an individual animation

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

As it stands now any consumer code gets notifications of the state an animation is in without any reference as to which animation is being referred to. This requires us to create a very rudimentary state machine to keep track of things which makes things a bit more complicated that it needs to be especially for what is probably the most used case which is being notified when an animation has completed. To address this we will start by adding a property to our options interface (k).

types.ts

...
export interface IAnimationSubjectOptions {
    complete?: () => void;
}
(k) Adding an optional complete method signature to our options.

Once again notifying that the animation is complete

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

Since we have already made the change to keep track of our options we just call the comlete method if is has been defined (l).

AnimatableItem.vue

<script lang="ts">
...
export default class AnimatableItem extends Vue {
    ...
    private animationEnd() {
        ...
        requestAnimationFrame(() => {
            ...
            if (typeof(this.options.complete) !== "undefined") {
                this.options.complete();
            }
        });
    }
}
</script>
(l) Calling the complete method if it is defined when the animation has completed.

Remove the complete binding form our template

  • root
    • src
      • components
        • TheRouterOutlet.vue

Now that we can have a method called when an animation is complete per animation we no longer need to bind a method to the complete prop (m).

TheRouterOutlet.vue

<template lang="pug">
div.router-view(v-bind:class="{ 'is-animating': isAnimating }")
    AnimatableItem.router-view-animatable(
        v-bind:complete="animationComplete" <-- remove
        v-bind:subject="animationSubject")
        ...
</template>
(m) Remove the binding for the complete prop.

Goodbye state machine

  • root
    • src
      • components
        • TheRouterOutlet.vue

Although everything works fine having to keep track of which animation is running just to know what to do when one of the completes is a little more complicated than it needs to be. To fix this we can just call a different complete method depending on whether the animation is incoming or outgoing (n).

TheRouterOutlet.vue

<script lang="ts">
...
import {
    ...,
    IAnimationSubjectOptions,
} from "@/components/animations";
...
export default class TheRouterOutlet extends Vue {
    ...
    private readonly animationOptionsIn: IAnimationSubjectOptions = {
        complete: this.animationCompleteIn,
    };
    private readonly animationOptionsOut: IAnimationSubjectOptions = {
        complete: this.animationCompleteOut,
    };
    ...
    private animate(route: Routes) {
        ...
        if (from.isChildOf(this.toEntry)) {
            ...
            this.animationSubject.next(AnimationTypes.TranslateOutToRight, this.animationOptionsOut);
        } else if (this.toEntry.isChildOf(from)) {
            ...
            this.animationSubject.next(AnimationTypes.TranslateOutToLeft, this.animationOptionsOut);
        } else {
            ...
            this.animationSubject.next(AnimationTypes.FadeOut, this.animationOptionsOut);
        }
    }

    private animationCompleteOut() {
        this.isAnimatingOut = false;
        this.isAnimatingIn = true;
        this.$router.push(this.toEntry.path);
        this.animationSubject.next(this.inAnimation, this.animationOptionsIn);
    }

    private animationCompleteIn() {
        this.isAnimatingIn = false;
        this.inAnimation = AnimationTypes.None;
    }
    ...
    private animationComplete() { // <-- remove
        ...
    }
}
</script>
(n) Executing a different method depeding on whether we are animating in our animating out.
Exciton Interactive LLC
Advertisement