dmcblue
dmcblue

Reputation: 363

Accessing arbitrary slots in Vue

Say I want to create a multi layered Component for reuse like a 'Tab' Ui.

So the using developer could write:

<tabs>
  <tab label="My First Tab">
    Content for first tab which could contain components, html, etc.
  </tab>
  <tab label="My Second Tab">
    More Content
  </tab>
</tabs>

Basically, they would use the Tabs and Tab components in such a way that I have no idea how many Tab components the dev will use in the Tabs. How do I access the Tab components to, for example, show and hide them per tab UI functionality?

I've tried using this.$children and this.slots.default but was not actually able to access the Tab data to show and hide it. To be fair, I'm writing in Typescript and the issue may be more difficult because of it.

Something like:

<template>
    <div class="tabs">
        <slot /> <!-- Location of the Tab's -->
    </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

@Component({
    ...
})
export default class Tabs extends Vue {
    public tabs: any[] = [];
        
    public created() {
        this.tabs = this.$slots.default;
        // this.tabs is a set of VNode's, 
        //    I can't figure out how to access the components
        //    I see a prop 'componentInstance' but when I try to
        //    access it, it says undefined
        // this.select(0);
        let tab = this.tabs.find((obj: any, index: number) => {
            return index === 0;
        }); // tab is undefined :(
    }

    public select(index: number) {
        (this.$slots.default[index] as any).show = true;
        // Accessing Vnode instead of Component :(
        // this.$children[0]["show"] = true; // Read only :(
        // this.tabs[index].show = true; // blerrggg :(

        // Vue.nextTick();
    }
}
</script>

I've looked at some GitHub libs for Tabs in Vue and the logic seems very complicated. I'm assuming from the existence of slots/children there is a more straight-forward approach to accessing child components. Maybe that's overly hopeful on my part.

Anyone know how to access, pass or change data in a child slot if you don't know how many children will be there (ie you didn't explicitly write them in the parent Component)?

Upvotes: 3

Views: 3862

Answers (3)

Tomhah
Tomhah

Reputation: 386

If the issue is that you have to wait until the tab components are mounted, the mounted lifecycle hook exists for this sort of thing. There is a potential problem with using the $children property though, as outlined in the Vue docs:

Note there’s no order guarantee for $children, and it is not reactive. If you find yourself trying to use $children for data binding, consider using an Array and v-for to generate child components, and use the Array as the source of truth.

Since the order is not guaranteed, this.$children[index] is not guaranteed to give you the tab you expect.

If you were to go with the approach suggested, using v-for and an array, it might look something like this:

<template>
    <div class="tabs">
        <tab v-for="(tabName, index) in tabs" :key="tabName" :label="tabName" :ref="`tab-${index}`">
            <slot :name="tabName"></slot>
        </tab>
    </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

@Component({
    ...
})
export default class Tabs extends Vue {
    @Prop(Array) tabs: string[];

    public mounted() {
        this.select(0);
    }

    public select(index: number) {
        this.$refs[`tab-${index}`].show = true;
    }
}
</script>

I put the slot inside a tab so that you know all of the child components are going to be wrapped in a tab component, meaning all tabs children are guaranteed to be tab components, and your $refs will all have the expected tab properties. You would use this like so:

<tabs :tabs="['My First Tab', 'My Second Tab']">
    <template slot="My First Tab">
        Content for first tab which could contain components, html, etc.
    </template>
    <template slot="My Second Tab">
        More content
    </template>
</tabs>

Upvotes: 5

briones-gabriel
briones-gabriel

Reputation: 45

I was able to solve it by using the updated life-cycle hook followed by this.$nextTick(). All together they allow you to fire something every time the component updates (which for me was waiting for an axios post on the children elements).

To sum it up, it ended up being something like the following:

mounted() {
  // this makes reference to the children inside the (current) component slots
  const tabs: Tab[] = this.$vnode.componentInstance.$children as Tab[];
  setTabs(tabs);
}

updated() {
  this.$nextTick(function () {
    const updatedTabs: Tab[] = this.$vnode.componentInstance.$children as Tab[];

    if (updatedTabs.length > this.tabs.length) setTabs(updatedTabs);
  });
}

setTabs(updatedTabs: Tab[]) {
  this.tabs = updatedTabs;
  // Do some more stuff here if you want (like filtering or ordering)
}

Upvotes: 0

dmcblue
dmcblue

Reputation: 363

Part of my problem (it seems) is that the created() function for Tabs fires before the Tab components are mounted so they may or may not exist. By calling the initial select with a Timeout, I was able to use

(this.$children[index] as any)["show"] = true;

Does anyone know how to check when (immediate) child components are mounted? I did some searching and could not find a proper answer.

Upvotes: 1

Related Questions