返回

Vuetify数据表格Scoped Slot失效?两种解决方案详解

vue.js

Vuetify 数据表格 Scoped Slot 失效问题

在使用 Vuetify 的 <v-data-table> 组件时,如果需要自定义单元格渲染,scoped slots 是一种常用的方法。 常见的情况是将 <v-data-table> 封装到自定义组件中。 如果封装不当,scoped slots 可能会失效,导致自定义渲染不生效。

问题分析

问题源于对 Vue slots 工作原理的理解偏差,尤其是在涉及动态 scoped slots 传递时。 当我们尝试通过 <slot :name="name" v-bind="slotData">v-data-table 接收到的 scoped slot 重新暴露时,并未直接按预期工作。这是由于组件模板渲染和作用域的问题导致,slot 在父组件作用域中绑定,并在父组件中进行预处理,导致透传过程的参数被忽略了。 具体说,父组件定义的 <template v-slot:item.someProperty="..."> 会绑定其作用域内的值。但是,我们的目标是在子组件 <v-data-table> 中应用作用域内值。

简单的传递 <slot/> 不会将其作为命名 slots 进行处理, 造成预期之外的效果。在 <v-data-table> 中, <template v-slot:item.name="{ item }"> 这类 v-slot 指令会在 <v-data-table> 内部的作用域处理 item 参数,直接透传 <slot> 不会将其作为 v-slot。这就是问题出现的根本原因。

解决方案

以下提供几种可以解决这个问题的方案:

方案一:使用 v-bind="$scopedSlots" 透传 scoped slot

这个方案是将 v-data-table 组件上的所有 scoped slots,利用 Vue 的 $scopedSlots API 获取到,然后作为属性绑定到内部的 <v-data-table> 上, 实现动态传递。 这也是推荐的一种处理方式,因为它足够简洁而且语义清晰。

代码示例:

<template>
  <v-card outlined>
    <v-data-table
      v-bind="$attrs"
      v-on="$listeners"
      :scoped-slots="$scopedSlots"
      dense
    ></v-data-table>
  </v-card>
</template>

操作步骤:

  1. 修改封装的 <DataTable> 组件,将 :scoped-slots="$scopedSlots" 添加到内部的 <v-data-table> 元素上。
  2. 移除<template v-for ...> <slot/> </template> 部分。
  3. 确保在父组件使用 <template v-slot:item.someProperty="{item}"> </template>这种写法定义scoped slot。

原理:

:scoped-slots="$scopedSlots" 将父组件传递过来的所有 scoped slot 作为内部 <v-data-table> 组件的 scopedSlots 属性进行传递。这样 v-data-table 便可以使用正确的作用域渲染 slot, 实现了 slots 的转发。$attrs 用于传递所有非 prop 的 attribute 到内部 v-data-table 组件。 $listeners 用于传递父组件绑定的所有事件监听器。这种做法有助于构建透明、可复用的封装组件。

方案二:显式定义 Render Function 处理 Scope Slot

对于复杂的需求或者需要额外逻辑的情况,可以使用 render function 来显式控制组件渲染过程,包括 scope slot 处理。 此方案的理解和实现相对更复杂一些。

代码示例:

<template>
 <v-card outlined>
     <v-data-table v-bind="$attrs"
                    v-on="$listeners"
                   :loading="loading"
                   :items="items"
                   :headers="headers"
     dense>
         </v-data-table>
     </v-card>
</template>

<script>
import { h } from 'vue';

export default {
    inheritAttrs:false, // 非常重要!
   name:'DataTable',
   props:{
    items:{
      type:Array,
        default: () =>[]
    },
       loading:{
        type:Boolean,
        default:false
       },
    headers:{
        type:Array,
        default:() => []
    }
   },

  render() {

      const scopedSlots = Object.keys(this.$slots).reduce((acc,key) => {

           if( key === "default" ) {
                return acc;
           }
           acc[`${key}`] = (props) => { return  this.$slots[key](props) }

          return acc;
        },{});
      return h("v-card",{ props:{outlined:true}}, [
           h('v-data-table',
             {
                  ...this.$attrs,
               ...{props:{...this.$props, scopedSlots }},
                ...this.$listeners,
             }
           ),

      ])
  },
}
</script>

操作步骤:

  1. 引入 vue h 函数.
  2. export default 内移除<template>, 使用 render function render() 来处理 UI 渲染
  3. 禁用 inheritAttrs:false 防止 attribute 被应用到 root 节点上.
  4. 获取 slots $slots
  5. 迭代 slots,将 item.* slot 转换为 scoped slots
  6. 使用 h 渲染 VDataTabel 组件
  7. 使用 <v-card/>包裹 <v-data-table/>

原理:
通过 render 函数显式控制 DOM 结构, 利用 h 函数构建 Vnode,将父组件 slots 收集转换为 v-data-table 所需要的 scopedSlots。使用 h("v-data-table", {...{ props: { ...this.$props, scopedSlots} }} 手动传递所有的props 和 slots , 其中通过 (props) => this.$slots[key](props)this.$slots[key] 中的 key 做了包装 ,将其转为可以传 props 参数的方法。

安全建议

无论是采用那种方法, 在进行slot的封装透传的时候都要注意以下几点:

  1. 透传 Props: 确保通过 v-bind="$attrs" 或手动传递将 props 透传到 v-data-table,以避免 prop 失效
  2. 事件监听: 使用 v-on="$listeners" 传递所有事件监听器,避免遗漏父组件事件处理。
  3. Scoped Slot: 使用正确的 v-slot 语法。

总结

Vuetify v-data-table 的 scoped slot 传递需要额外关注组件层级以及 Vue slot 的工作机制, 理解问题根源有助于更好使用 scoped slot 并设计更佳的自定义组件方案,希望可以帮助大家在日常开发中更好的利用 slot 实现 UI 复用和自定义。