无限灰

关于vuedraggable拖拽组件传递插槽,子组件无法拖拽的问题

问题的产生

在开发elysia低代码表单设计器时遇到一个难受点: 每次开发一个容器组件时,都要传递一些固定的属性,如list、group、ghostClass、animation、item-key、component-data等属性。

示例:

<template>
  <static-wrapper :widget="widget">
    <a-card :title="widget.options.title">
      <!-- 主要看以下代码 -->
      <draggable
        :list="widget.childrenList"
        handle=".move-area"
        :move="checkMove"
        :clone="clone"
        group="dragGroup"
        ghostClass="ghost"
        :animation="300"
        :component-data="{ index: widget.path, type: widget.name }"
        item-key="id"
        :disabled="isViewStatus"
        @end="onEndMove"
      >
        <template #item="{ element }">
          <component
            :is="'elysia-' + element.name"
            :widget="element"
            :key="element.id"
          ></component>
        </template>
      </draggable>
    </a-card>
  </static-wrapper>
</template>
<script setup>
// 省略一些模块导入
defineProps({
  widget: {
    type: Object,
    required: true
  }
})

const onEndMove = () => {
  // 省略
}
</script>

每次开发一个新的容器组件都得这样写,虽说可以使用vscode的用户代码片段来快速生成,但还是尝试封装了一下组件。

最开始是这样写的:通过v-for把vuedraggable的所有插槽通过动态插槽分发下去

<!--container-draggable.vue-->
<template>
  <draggable
    v-bind="$attrs"
    :list="widget.childrenList"
    :handle="handle"
    :move="checkMove"
    :clone="clone"
    :group="group"
    :ghostClass="ghostClass"
    :animation="animation"
    :component-data="{ index: widget.path, type: widget.name }"
    :item-key="itemKey"
    :disabled="isViewStatus"
    @end="onEndMove"
  >
    <!-- 循环插槽并分发下去 -->
    <template v-for="slotKey in Object.keys($slots)" v-slot:[slotKey]="scope">
      <slot v-bind="scope"></slot>
    </template>
  </draggable>
</template>
<script setup>
import draggable from 'vuedraggable'
// 省略一些导入的模块

// 除了`widget`必须传递,其他可选均有默认值
const props = defineProps({
  group: {
    type: [String, Object],
    default: () => ({
      name: 'dragGroup',
      pull: true,
      put: true
    })
  },
  ghostClass: {
    type: String,
    default: 'ghost'
  },
  animation: {
    type: Number,
    default: 300
  },
  widget: {
    type: Object,
    required: true
  },
  itemKey: {
    type: String,
    default: 'id'
  },
  // 可以进行拖拽操作的区域
  handle: {
    type: String,
    default: '.move-area'
  }
})

const onEndMove = evt => {
  // 省略
}
</script>

使用只需要传递一个widget对象即可:

<template>
  <static-wrapper :widget="widget">
    <container-draggable :widget="widget"></container-draggable>
  </static-wrapper>
</template>
<script setup>
// 省略一些模块导入
import containerDraggable from 'path/container-draggable.vue'
defineProps({
  widget: {
    type: Object,
    required: true
  }
})
</script>

本以为就这样结束了,运行也能把组件拖拽进去,但却发现无法拖拽出来,这就尴尬了。

问题的溯源

在寻找问题的根源时,发现了实际拖拽的是其父组件

这时候我在想是不是事件冒泡的原因,也给各种事件加上了eventName.stop防止,但还是没有效果。

就这样又去看了vuedraggable的源码,又在浏览器进行debugger断点调试,发现咋整实际拖拽都是父组件(也就是图中的卡片组件)

在换回vuedraggable直接使用也没问题啊,可以正常拖拽进去、出来,就这样又怀疑到是不是动态插槽分发导致的;

就这样我尝试只把item插槽分发下去,反正其他header和footer插槽用不上

<template #item="scope">
  <slot v-bind="scope"></slot>
</template>

但可惜并没有效果,有了打消使用包装vuedraggable组件的念头。

问题的解决

本着已经怀疑到插槽身上,就尝试打了个断点。

然后就发现 node.props.key 有点不太对劲,这是把插槽默认插槽 当做拖拽对象了,也可以看到 node.ctx 指向的就是卡片组件,这就是问题所在了,那么该咋解决呢?

在插槽外面加个div就行了,这样他的指向就没问题了,也可以从卡片正常拖拽出去了,问题解决。

<template #item="scope">
  <div>
    <slot v-bind="scope"></slot>
  </div>
</template>

debugger调试,可以看到node.props不再单单是一个插槽的key了