#21 Finishing The Tab Component By Programmatically Instantiating Vue Tab Entries
Saturday, October 26, 2019
In this article we will finish up dealing with our tab control. We will start by adding in a new tab entry component that we will use to determine what content should be visible based on which tab is currently active. The way that it will work is that we will require the direct children of the tab container to be an instance of a tab entry and if they are not we will show an error and the control will just not work at all. This is fine and it will work but it is not as easy to use as it could be so we will finish up by allowing the children of the tab container to be whatever we want them to be and we will programmatically wrap them in a tab entry. Along the way we will also provide appropriate animations for the switching between tabs.
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
Code Snippets
TabEntry.vue (1:41)
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component
export default class TabEntry extends Vue {
private isVisible = false;
public get visible() { return this.isVisible; }
public set visible(isVisible: boolean) { this.isVisible = isVisible; }
}
</script>
TabEntry.vue (2:41)
<template lang="pug">
div(v-if="visible")
slot
</template>
index.ts (3:15)
export { default as TabContainer } from "@/components/tabs/TabContainer.vue";
export { default as TabEntry } from "@/components/tabs/TabEntry.vue";
Securities.vue (3:40)
<script lang="tsx">
...
import {
TabContainer,
TabEntry,
} from "@/components/tabs";
@Component({
components: {
...
TabEntry,
},
})
export default class Securities extends Vue {
...
}
</script>
Securities.vue (4:03)
<template lang="pug">
TabContainer(v-bind:tabs="tabs")
TabEntry
ListView(
v-bind:items="securities"
v-bind:renderFn="renderFnSecurities")/
TabEntry
ListView(
v-bind:items="categories"
v-bind:renderFn="renderFnCategory")/
TabEntry
ListView(
v-bind:items="markets"
v-bind:renderFn="renderFnDescriptor('markets')")/
TabEntry
ListView(
v-bind:items="segments"
v-bind:renderFn="renderFnDescriptor('segments')")/
TabEntry
ListView(
v-bind:items="territories"
v-bind:renderFn="renderFnDescriptor('territories')")/
TabEntry
ListView(
v-bind:items="types"
v-bind:renderFn="renderFnDescriptor('types')")/
</template>
TabContainer.vue (4:58)
<script lang="ts">...
import TabEntry from "@/components/tabs/TabEntry.vue";
...
export default class TabContainer extends Vue {
...
private readonly children: TabEntry[] = [];
private activeIndex = 0;
private mounted() {
if (typeof (this.$slots) === "undefined" || typeof (this.$slots.default) === "undefined") {
return;
}
this.$slots.default.forEach((x, i) => {
if (typeof(x.componentOptions) === "undefined" || typeof(x.componentInstance) === "undefined") {
return;
}
if (x.componentOptions.tag !== "TabEntry") {
console.error("The direct children of a TabContainer must be an instance of TabEntry.");
return;
}
const entry = x.componentInstance as TabEntry;
entry.visible = i === this.activeIndex;
this.children.push(entry);
});
}
private setActive(index: number) {
this.children.forEach((x, i) => {
x.visible = i === index;
});
}
}
</script>
Advertisement
TabContainer.vue (9:39)
<script lang="ts">
...
import {
AnimatableItem,
AnimationSubject,
IAnimationSubjectOptions,
AnimationTypes,
} from "@/components/animations";
...
@Component({
components: {
AnimatableItem,
...
},
})
export default class TabContainer extends Vue {
...
private readonly animationOptionsOut: IAnimationSubjectOptions = {
complete: this.animationCompleteOut,
};
private readonly animationSubject = new AnimationSubject();
...
private inAnimation = AnimationTypes.None;
private animationCompleteOut() {
this.children.forEach((x, i) => {
x.visible = this.activeIndex === i;
});
requestAnimationFrame(() => {
this.animationSubject.next(this.inAnimation);
});
}
...
private setActive(index: number) {
let outAnimation = AnimationTypes.None;
if (index > this.activeIndex) {
outAnimation = AnimationTypes.TranslateOutToLeft;
this.inAnimation = AnimationTypes.TranslateInFromRight;
} else if (index < this.activeIndex) {
outAnimation = AnimationTypes.TranslateOutToRight;
this.inAnimation = AnimationTypes.TranslateInFromLeft;
} else {
return;
}
this.activeIndex = index;
this.animationSubject.next(outAnimation, this.animationOptionsOut);
}
}
</script>
TabContainer.vue (14:05)
<template lang="pug">
div.tab-container
...
AnimatableItem(v-bind:subject="animationSubject")
slot
</template>
TabContainer.vue (14:58)
<style lang="sass" scoped>
.tab-container
overflow: hidden
</style>
Securities.vue (15:45)
<template lang="pug">
TabContainer(v-bind:tabs="tabs")
ListView(
v-bind:items="securities"
v-bind:onClick="onClickSecurity"
v-bind:renderFn="renderFnSecurity")/
ListView(
v-bind:items="categories"
v-bind:onClick="onClickCategory"
v-bind:renderFn="renderFnCategory")/
ListView(
v-bind:items="markets"
v-bind:onClick="onClickDescriptorFactory(descriptorMarkets)"
v-bind:renderFn="renderFnDescriptor")/
ListView(
v-bind:items="segments"
v-bind:onClick="onClickDescriptorFactory(descriptorSegments)"
v-bind:renderFn="renderFnDescriptor")/
ListView(
v-bind:items="territories"
v-bind:onClick="onClickDescriptorFactory(descriptorTerritories)"
v-bind:renderFn="renderFnDescriptor")/
ListView(
v-bind:items="types"
v-bind:onClick="onClickDescriptorFactory(descriptorTypes)"
v-bind:renderFn="renderFnDescriptor")/
</template>
TabContainer.vue (16:25)
<script lang="ts">
import { Refs } from "@/types";
...
export default class TabContainer extends Vue {
public $refs!: Refs<{
container: HTMLElement,
}>
...
private mounted() {
if (typeof (this.$slots) === "undefined" || typeof (this.$slots.default) === "undefined") {
return;
}
this.$slots.default.forEach((x, i) => {
const entry = new TabEntry();
entry.visible = i === this.activeIndex;
entry.$slots.default = [ x ];
entry.$mount();
this.$refs.container.appendChild(entry.$el);
this.children.push(entry);
});
}
...
}
</script>
TabContainer.vue (17:14)
<template lang="pug">
div.tab-container
...
AnimatableItem(...)
slot <-- remove
div(ref="container")
</template>
Exciton Interactive LLC