#9 Removing Our Dependency On Rxjs From Animatable Item Consumers
Saturday, August 3, 2019
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.
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
Wrapping the Rxjs Subject
- root
- src
- components
- animations
- animation-subject.ts
- animations
- components
- src
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);
}
}
Add it to our index
- root
- src
- components
- animations
- index.ts
- animations
- components
- src
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";
...
Updating our animatable item
- root
- src
- components
- animations
- AnimatableItem.vue
- animations
- components
- src
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>
Remove rxjs from the splash screen
- root
- src
- components
- TheSplashScreen.vue
- components
- src
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>
Remove rxjs from the routing outlet
- root
- src
- components
- routing
- TheRouterOutlet.vue
- routing
- components
- src
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>
Separating the consumer options from our internal options
- root
- src
- components
- animations
- types.ts
- animations
- components
- src
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 {
...
}
...
- root
- src
- components
- animations
- animation-subject.ts
- animations
- components
- src
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 });
}
}
Back to updating our splash screen
- root
- src
- components
- TheSplashScreen.vue
- components
- src
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>
Back to updating our router outlet
- root
- src
- components
- TheRouterOutlet.vue
- components
- src
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>
Preparing our animatable item to use the options
- root
- src
- components
- animations
- AnimatableItem.vue
- animations
- components
- src
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>
Notification of completion for an individual animation
- root
- src
- components
- animations
- types.ts
- animations
- components
- src
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;
}
Once again notifying that the animation is complete
- root
- src
- components
- animations
- AnimatableItem.vue
- animations
- components
- src
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>
Remove the complete binding form our template
- root
- src
- components
- TheRouterOutlet.vue
- components
- src
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>
Goodbye state machine
- root
- src
- components
- TheRouterOutlet.vue
- components
- src
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>