Vue3树形列表组件如何为每个项实现插槽,让使用者自定义内容?
解决Vue递归树形列表组件的插槽自定义问题
我开发了一个树形结构的列表组件,接收对象数组作为数据源,目前固定渲染项的text属性。当前使用方式:
<list-base :items="[{id: 'a', text: 'a'}, {id: 'b', text: 'b', parentId: 'a'}]" />
希望升级组件,让使用者通过Vue插槽自由自定义列表项内容,期望使用方式:
<list-base :items="[{id: 'a', text: 'a'}, {id: 'b', text: 'b', parentId: 'a', meta: new Date()}]"> <template v-slot:item="{item}"> {{ item.text }} <fancy-component v-if="item.meta">{{ item.meta.toISOString()}}</fancy-component> </template> </list-base>
由于组件是递归树形结构,常规插槽方案无法直接生效,以下是针对三个组件的修改方案:
1. 修改list-base.vue
定义插槽并透传给递归子组件:
<script setup> import ListTree from "./list-tree.vue"; defineProps({ items: { type: Array, default: () => [], }, draggable: { type: Boolean, default: false, }, editable: { type: Boolean, default: false, }, }); defineEmits(["action", "drag", "edit"]); </script> <template> <div> <template v-for="item in items" :key="item.value"> <list-tree :root="item" :draggable="draggable" :editable="editable" @action="$emit('action', $event)" @drag="$emit('drag', $event)" @edit="$emit('edit', $event)" <!-- 透传插槽给递归组件 --> v-slots="{ item: $slots.item }" /> </template> </div> </template>
2. 修改list-tree.vue
接收插槽并传递给列表项,递归调用时继续透传插槽:
<script setup> import { toRef } from "vue"; import { hasElements, ActionsMenu } from "@lib"; import { COLLAPSE_STATUSES, useCollapse } from "./collapse"; import CollapseSwitch from "./collapse-switch.vue"; import ListItem from "./list-item.vue"; const props = defineProps({ root: { type: Object, default: () => ({}), }, draggable: { type: Boolean, default: false, }, editable: { type: Boolean, default: false, }, // 新增:接收插槽属性 slots: { type: Object, default: () => ({}) } }); defineEmits(["action", "drag", "edit"]); const { collapse, handleStart, handleEnd } = useCollapse(toRef(props, "root")); </script> <template> <div class="list-tree"> <div class="list-root-wrapper"> <collapse-switch v-model="collapse" /> <list-item :item="root" :draggable="draggable" :editable="editable" @drag-start="handleStart" @drag-end="handleEnd" @action="$emit('action', $event)" @drag="$emit('drag', $event)" @edit="$emit('edit', $event)" > <!-- 将插槽传递给list-item --> <template v-if="slots.item" v-slot:item="{ item }"> <component :is="slots.item" :item="item" /> </template> </list-item> <template v-if="hasElements(root.actions)"> <actions-menu :actions="root.actions" @action="$emit('action', { item: root.value, action: $event })" /> </template> </div> <template v-if="collapse === COLLAPSE_STATUSES.OPEN"> <div class="list-tree-children"> <template v-for="child in root.children" :key="child.value"> <list-tree :root="child" :draggable="draggable" :editable="editable" @action="$emit('action', $event)" @drag="$emit('drag', $event)" @edit="$emit('edit', $event)" <!-- 递归时继续透传插槽 --> :slots="slots" /> </template> </div> </template> </div> </template> <style scoped> .list-root-wrapper { display: flex; align-items: center; } .list-tree-children { padding-inline-start: var(--size-20); } </style>
3. 修改list-item.vue
添加插槽支持,保留默认文本作为降级方案:
<script setup> import { computed } from "vue"; import { TooltipBase } from "../tooltip-base"; import { useDrag } from "./use-drag"; const props = defineProps({ item: { type: Object, default: () => ({}), }, draggable: { type: Boolean, default: false, }, editable: { type: Boolean, default: false, }, }); const emit = defineEmits(["drag", "drag-start", "drag-end", "edit"]); const { handlers, dragClasses } = useDrag({ value: props.item.value, emit }); const classes = computed(() => ({ ...dragClasses, inactive: props.item.inactive, })); const handleBlur = (e) => { const text = e.target.textContent; if (text === props.item.text) return; emit("edit", { item: props.item.value, text }); }; const handleEnter = (e) => e.target.blur(); </script> <template> <div v-bind="$attrs" :id="item.value" class="list-item" :class="classes" :draggable="draggable" :contenteditable="editable ? 'plaintext-only' : 'false'" @dragstart="handlers.start" @dragenter="handlers.enter" @dragleave="handlers.leave" @dragover="handlers.over" @drop="handlers.drop" @dragend="handlers.end" @blur="handleBlur" @keydown.enter.prevent="handleEnter" > <!-- 优先渲染自定义插槽,无插槽时显示默认文本 --> <slot name="item" :item="item"> {{ item.text }} </slot> </div> <tooltip-base v-if="item.tooltip" :anchor="item.value" :text="item.tooltip" /> </template> <style scoped> .list-item { --list-item-drag-border-color: var(--color-neutral-40); } .list-item { display: flex; flex-grow: 1; padding-block: var(--size-15); padding-inline-start: var(--size-10); border: var(--border-size-20) solid transparent; } .list-item.inactive { color: var(--color-neutral-40); text-decoration: line-through; } .list-item:hover, .list-item:focus-within, .list-item.over { background-color: var(--background-color-highlight); } .list-item.over.top { border-top-color: var(--list-item-drag-border-color); } .list-item.over.middle { border-color: var(--list-item-drag-border-color); } .list-item.over.bottom { border-bottom-color: var(--list-item-drag-border-color); } </style>
验证使用方式
修改完成后,即可按照预期方式自定义列表项内容:
<list-base :items="[{id: 'a', text: 'a'}, {id: 'b', text: 'b', parentId: 'a', meta: new Date()}]"> <template v-slot:item="{item}"> {{ item.text }} <fancy-component v-if="item.meta">{{ item.meta.toISOString()}}</fancy-component> </template> </list-base>
核心原理
递归组件无法直接访问根组件插槽,需通过属性透传插槽的方式,从list-base逐层传递到每一层list-tree和list-item;同时在list-item中设置默认内容,保证组件在未使用自定义插槽时仍能正常工作。
内容的提问来源于stack exchange,提问作者João Melo
相关产品推荐
相关产品推荐

