如何在 Vue 3 中访问 $children 以创建 Tabs 组件?

How to access $children in Vue 3 for creating a Tabs component?

我正在尝试在 Vue 3 中创建一个类似于 .

Tabs 组件
<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>

不幸的是,当 Tab 内部是动态的,即如果 Tab 上有 v-if 或 Tab 被渲染时,建议的解决方案不起作用使用 v-for 循环 - 它失败了。

我在这里为它创建了一个 Codesandbox,因为它包含 .vue 个文件:

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

我试过像 onBeforeMount 一样使用 onBeforeUpdate,但这也不起作用。实际上,它确实插入了新的标签,但是标签的顺序改变了。

最大的障碍似乎是在 Vue 3 中似乎无法 get/set child 来自父级的数据。(如 $children 在 Vue 2.x)。有人建议使用 this.$.subtree.children 但后来强烈建议不要使用(无论如何我试过都没有帮助)。

谁能告诉我如何使 Tabs 中的 Tab 响应并更新 v-if 等?

这看起来像是使用项目索引作为 v-for 循环的 key 的问题。

第一个问题是您在子元素上应用了 v-forkey,而本应在父元素上(在本例中为 <li>)。

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

此外,如果 v-for 后备数组可以重新排列其项目(或删除中间项目),请不要将项目索引用作 key,因为索引不会提供始终如一的独特价值。例如,如果从列表中删除了第 3 个项目中的第 2 个项目,则第三个项目将向上移动到索引 1 中,采用之前被删除的项目使用的 key。由于列表中没有 key 发生变化,Vue 重用现有的虚拟 DOM 节点作为优化,不会发生重新渲染。

在您的情况下,一个好的 key 到 select 是选项卡的 title 值,因为在您的示例中每个选项卡始终是唯一的。这是你的新 Tab.vue,其中 index 替换为 title 属性:

// 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 }
  },
}

然后,更新您的 Tabs.vue 模板以使用选项卡的 title 而不是 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

这个解决方案是 posted by @anteriovieira 在 Vuejs 论坛中,看起来是正确的方法。缺失的拼图 getCurrentInstancesetup

期间可用

完整的工作代码可以在这里找到:

https://codesandbox.io/s/vue-3-tabs-ob1it

我将其添加到此处以供来自 Google 的任何人参考。

由于模板中的 $slots 可以访问插槽(请参阅 Vue documentation),您还可以执行以下操作:

// Tabs component

<template>
  <div v-if="$slots && $slots.default && $slots.default()[0]" class="tabs-container">
    <button
      v-for="(tab, index) in getTabs($slots.default()[0].children)"
      :key="index"
      :class="{ active: modelValue === index }"
      @click="$emit('update:model-value', index)"
    >
      <span>
        {{ tab.props.title }}
      </span>
    </button>
  </div>
  <slot></slot>
</template>

<script setup>
  defineProps({ modelValue: Number })

  defineEmits(['update:model-value'])

  const getTabs = tabs => {
    if (Array.isArray(tabs)) {
      return tabs.filter(tab => tab.type.name === 'Tab')
    } else {
      return []
    }
</script>

<style>
...
</style>

Tab 组件可能是这样的:

// Tab component

<template>
  <div v-show="active">
    <slot></slot>
  </div>
</template>

<script>
  export default { name: 'Tab' }
</script>

<script setup>
  defineProps({
    active: Boolean,
    title: String
  })
</script>

实施应类似于以下内容(考虑一组对象,每个部分一个,具有 titlecomponent):

...
<tabs v-model="active">
  <tab
    v-for="(section, index) in sections"
    :key="index"
    :title="section.title"
    :active="index === active"
  >
    <component
      :is="section.component"
    ></component>
  </app-tab>
</app-tabs>
...
<script setup>
import { ref } from 'vue'

const active = ref(0)
</script>