Advertisement

#21 Finishing The Tab Component By Programmatically Instantiating Vue Tab Entries

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.

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>
root ⟩ src ⟩ components ⟩ tabs ⟩ TabEntry.vue

TabEntry.vue (2:41)

<template lang="pug">
div(v-if="visible")
    slot
</template>
root ⟩ src ⟩ components ⟩ tabs ⟩ TabEntry.vue

index.ts (3:15)

export { default as TabContainer } from "@/components/tabs/TabContainer.vue";
export { default as TabEntry } from "@/components/tabs/TabEntry.vue";
root ⟩ src ⟩ components ⟩ tabs ⟩ index.ts

Securities.vue (3:40)

<script lang="tsx">
...
import {
    TabContainer,
    TabEntry,
} from "@/components/tabs";

@Component({
    components: {
        ...
        TabEntry,
    },
})
export default class Securities extends Vue {
    ...
}
</script>
root ⟩ src ⟩ views ⟩ Securities.vue

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>
root ⟩ src ⟩ views ⟩ Securities.vue

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>
root ⟩ src ⟩ components ⟩ tabs ⟩ TabContainer.vue

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>
root ⟩ src ⟩ components ⟩ tabs ⟩ TabContainer.vue

TabContainer.vue (14:05)

<template lang="pug">
div.tab-container
    ...
    AnimatableItem(v-bind:subject="animationSubject")
        slot
</template>
root ⟩ src ⟩ components ⟩ tabs ⟩ TabContainer.vue

TabContainer.vue (14:58)

<style lang="sass" scoped>
.tab-container
    overflow: hidden
</style>
root ⟩ src ⟩ components ⟩ tabs ⟩ TabContainer.vue

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>
root ⟩ src ⟩ views ⟩ Securities.vue

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>
root ⟩ src ⟩ components ⟩ tabs ⟩ TabContainer.vue

TabContainer.vue (17:14)

<template lang="pug">
div.tab-container
    ...
    AnimatableItem(...)
        slot <-- remove
        div(ref="container")
</template>
root ⟩ src ⟩ component ⟩ tabs ⟩ TabContainer.vue

Exciton Interactive LLC
Advertisement