Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to access $children for creating Tabs component?

I'm trying to create a Tabs component in Vue 3 similar to this question here.

<tabs>
   <tab title="one">content</tab>
   <tab title="two" v-if="show">content</tab> <!-- this fails -->
   <tab :title="t" v-for="t in ['three', 'four']">{{t}}</tab> <!-- also fails -->
   <tab title="five">content</tab>
</tabs>

Unfortunately the proposed solution does not work when the Tabs inside are dynamic, i.e. if there is a v-if on the Tab or when the Tabs are rendered using a v-for loop - it fails.

I've created a Codesandbox for it here because it contains .vue files:

https://codesandbox.io/s/sleepy-mountain-wg0bi?file=%2Fsrc%2FApp.vue

enter image description here

I've tried using onBeforeUpdate like onBeforeMount, but that does not work either. Actually, it does insert new tabs, but the order of tabs is changed.

The biggest hurdle seems to be that there seems to be no way to get/set child data from parent in Vue 3. (like $children in Vue 2.x). Someone suggested to use this.$.subtree.children but then it was strongly advised against (and didn't help me anyway I tried).

Can anyone tell me how to make the Tab inside Tabs reactive and update on v-if, etc?

like image 243
supersan Avatar asked Dec 06 '25 11:12

supersan


2 Answers

This looks like a problem with using the item index as the v-for loop's key.

The first issue is you've applied v-for's key on a child element when it should be on the parent (on the <li> in this case).

<li v-for="(tab, i) in tabs">
  <a :key="i"> ❌
  </a>
</li>

Also, if the v-for backing array can have its items rearranged (or middle items removed), don't use the item index as the key because the index wouldn't provide a consistently unique value. For instance, if item 2 of 3 were removed from the list, the third item would be shifted up into index 1, taking on the key that was previously used by the removed item. Since no keys in the list have changed, Vue reuses the existing virtual DOM nodes as an optimization, and no rerendering occurs.

A good key to select in your case is the tab's title value, as that is always unique per tab in your example. Here's your new Tab.vue with the index replaced with a title prop:

// Tab.vue
export default {
  props: ["title"], 👈
  setup(props) {
    const isActive = ref(false)
    const tabs = inject("TabsProvider")

    watch(
      () => tabs.selectedIndex,
      () => {
        isActive.value = props.title === tabs.selectedIndex
      }                        👆
    )

    onBeforeMount(() => {
      isActive.value = props.title === tabs.selectedIndex
    })                       👆

    return { isActive }
  },
}

Then, update your Tabs.vue template to use the tab's title instead of i:

<li class="nav-item" v-for="tab in tabs" :key="tab.props.title">
  <a                                                     👆
    @click.prevent="selectedIndex = tab.props.title"
    class="nav-link"                          👆
    :class="tab.props.title === selectedIndex && 'active'"
    href="#"          👆
  >
    {{ tab.props.title }}
  </a>
</li>

demo

like image 126
tony19 Avatar answered Dec 08 '25 23:12

tony19


I discovered a simple solution that works by having the tab registering itself with the tabs component using provide + inject. It identifies the tabs using a unique title, you could go by index instead if you wanted to.

Tabs Component HTML:

<ul class="tabs">
  <li
    v-for="tab in tabs"
    :key="tab.props.title"
    :class="{ active: tab.props.title === activeTabTitle }"
  >
    <a
      href="#"
      @click="activeTabTitle = tab.props.title"
      >{{ tab.props.title }}</a
    >
  </li>
</ul>

<div class="tab-content">
  <slot></slot>
</div>

Tabs Component setup script:

const activeTabTitle = defineModel();
const tabs = ref([]);

provide("tabsState", {
  tabs,
  activeTabTitle,
});

Tab Component HTML:

<div v-if="title === activeTabTitle" class="tab-content">
  <slot></slot>
</div>

Tab Component setup script:

const { title } = defineProps({
  title: String,
});

const { tabs, activeTabTitle } = inject("tabsState");

const tab = getCurrentInstance();
onBeforeMount(() => tabs.value.push(tab));
like image 36
Mark Lagendijk Avatar answered Dec 09 '25 01:12

Mark Lagendijk



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!