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

Posted

技术标签:

【中文标题】如何在 Vue 3 中访问 $children 以创建选项卡组件?【英文标题】:How to access $children in Vue 3 for creating a Tabs component? 【发布时间】:2021-04-09 16:20:43 【问题描述】:

我正在尝试在 Vue 3 中创建一个类似于 question here 的 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>

不幸的是,当内部的Tabs 是动态的,即如果Tab 上有一个v-if 或者当Tabs 使用v-for 循环呈现时,建议的解决方案不起作用 - 它失败。

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

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

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

最大的障碍似乎是在 Vue 3 中似乎无法从父级获取/设置 child 数据。(如 Vue 2.x 中的 $children)。有人建议使用this.$.subtree.children,但随后强烈建议不要使用(无论如何我尝试过并没有帮助我)。

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

【问题讨论】:

【参考方案1】:

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

第一个问题是您将v-forkey 应用到了应该在父元素上的子元素(在本例中为&lt;li&gt;)。

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

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

在您的情况下选择一个好的key 是选项卡的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

【讨论】:

感谢您的帮助。我现在更好地理解了键控。但是在您的演示中,它仍然没有呈现 v-for 选项卡:请参阅 App.vue 中的 &lt;tab :title="t.title" v-for="t in more" :key="t"&gt;Dynamic t &lt;/tab&gt;。有什么想法吗? 还有一条评论。在 Vue 2 中,不需要使用 title 或其他任何内容“键入”选项卡,因为 $children 提供了对插槽子项的直接访问。所以&lt;Tabs&gt;&lt;Tab&gt;1&lt;/Tab&gt;&lt;Tab&gt;2&lt;/Tab&gt;&lt;/Tabs&gt; 完全没问题。现在看来我们需要在每个&lt;Tab&gt; 上指定一个title 或某种键。你能想出一种方法来识别 selectedTab 而不需要 tab.title 或任何其他键吗? @supersan 我解决了有关缺少动态选项卡的问题(请参阅更新的演示),但这个解决方案(以及您链接到的原始解决方案)不是理想的 IMO,因为它需要进入 VNode 内部解析选项卡的标题只是为了显示选项卡标题。我认为更好的解决方案是将数据作为 Tabs 上的道具传递和/或使用作用域插槽。 另外,考虑使用现有的选项卡组件,或提供该组件的库(例如,Vuetify、BootstrapVue、Buefy 等)。 再次感谢您的帮助。你是对的,这个 Vnode 的东西很烂。在 Vue 2.x 中事情要简单得多,因为我们可以直接访问插槽的 $children,而且它也是反应式的,所以即使标签被拖动或排序,它也会自动更新。我确实研究了 Bootstrapvue、Vuetify 等,但那些还没有迁移到 Vue 3(谁知道他们面临同样的问题哈哈)。无论如何,所以我现在坚持使用 Vue 2.x。无论如何,这对我的需求来说已经绰绰有余了。我只是因为vite 而想升级,这看起来可以节省大量时间。【参考方案2】:

这个解决方案是 Vuejs 论坛中的posted by @anteriovieira,看起来是正确的方法。缺少的一块拼图是getCurrentInstancesetup 期间可用

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

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

我将其添加到此处,以供从 Google 来这里寻找相同内容的任何人参考。

【讨论】:

【参考方案3】:

由于模板中的$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>

【讨论】:

以上是关于如何在 Vue 3 中访问 $children 以创建选项卡组件?的主要内容,如果未能解决你的问题,请参考以下文章

Vue中如何存储多个children的值并在父组件中执行一个函数

在 vue.js 组件中反应 this.props.children

在 vue.js 组件中渲染子组件

按组件名称的Vue.js $ children

vue组件 $children,$refs,$parent的使用详解

Vue组件间通信--$parent/$children/4