pid: 109362778
引言
当我们在实现某个组件需要获取到外部传入的slot的引用、或者希望slot是单个元素时,通常会包裹一层元素(比如div)通过给外层元素打上ref标记来间接获取,例如:
vue
<template>
<divider ref="container">
<slot></slot>
</divider>
</template>
缺点很明显,加了一层元素破坏了原本的html结构。
假设slot本身就是单个元素,比如在使用Popconfirm 气泡确认框 | Element Plus 时,就会默认使得reference slot是单个元素.
但是当我们传入多个元素时, ELPopconfirm也能在不添加外层div的情况下,实现效果:
xml
<el-popconfirm title="Are you sure to delete this?">
<template #reference>
<el-button>Delete</el-button>
<el-button>Delete</el-button>
</template>
</el-popconfirm>

可以看到我们传入的reference元素时两个按钮,但是实际只渲染了第一个元素,也就是从传入的slot中挑选出了第一个元素作为reference
为此,我们可以尝试实现一个抽象组件partial来获取第一个元素。
实现思路
一个基本的抽象组件如下:
vue
export default defineComponent({
setup(props, context) {
const slots = context.slots;
//返回的是一个render函数用来代替template,
//h函数用来创建VNode
return () => {
return h('div',{}, [...slots.default()])
}
},
})
上面的代码将传入的slot套上一层div后返回.
slot.defalut是一个函数,他的返回值可以简单理解为根据你传入的模板而生成的VNode[]
, 因此我们需要做文章的地方也就是这个返回值了.
-
通过slot.default获取到VNode[],并获取它的第一个VNode
tslet dft = slots.default && slots.default()[0];
-
判断dft的type是否是SYmbol, 一些特殊的VNode节点(template, slot, txt ,comment)的type是symbol , 我们要做的就是排除template,slot,comment节点
ini// 排除template slot comment 节点 while (typeof dft!.type === 'symbol') { // 文本节点 if (dft!.type === Symbol.for('v-txt')){ dft = h('span',{},[dft]); break; } // 不允许第一个元素是注释节点 if (dft!.type === Symbol.for('v-cmt')){ dft = h('div',{reason:'not allowed', result:'force replacement with div'}); warn('The first child node of SPartial is a text (annotation) node, which is not allowed'); break; } //如果是template slot节点的话,获取它的第一个孩子节点 dft = (dft as any)!.children[0]; }
-
最后获取到第一个元素节点后返回即可
tsreturn () => { return h(dft as any, {...attr}) }
完整逻辑
xml
<script lang="ts">
import {defineComponent, h, useAttrs, warn} from "vue";
/**
* @description 取default slot中的第一个非(template slot comment)的组件节点
*/
export default defineComponent({
setup(__, context) {
const slots = context.slots;
const attr = useAttrs();
let dft = slots.default && slots.default()[0];
// 没传
if (!dft) {
warn('SPartial has empty slot');
}
// 排除template slot comment 节点
while (typeof dft!.type === 'symbol') {
// 文本节点
if (dft!.type === Symbol.for('v-txt')){
dft = h('span',{},[dft]);
break;
}
// 不允许第一个元素是注释节点
if (dft!.type === Symbol.for('v-cmt')){
dft = h('div',{reason:'not allowed', result:'force replacement with div'});
warn('The first child node of SPartial is a text (annotation) node, which is not allowed');
break;
}
dft = (dft as any)!.children[0];
}
return () => {
return h(dft as any, {...attr})
}
},
name: 'SPartial',
})
</script>
最终效果
xml
<SPartial>
hello
<s-button>hello world!</s-button>
</SPartial>

xml
<SPartial>
<template #default>
<s-button>hello world!</s-button>
<s-button>hello world!</s-button>
</template>
</SPartial>

xml
<SPartial>
<!-- 1111-->
1
<s-button>hello world!</s-button>
<s-button>hello world!</s-button>
</SPartial>

写在最后
这里只是提供前言所述问题的一个解决方案, 代码可能不严谨和全面, 欢迎指出,一起掉头发(
这个项目的地址是:lastertd/sss-ui-plus: 适用于vue3的组件库 (github.com)在这里求一个star✨
感谢看到最后💟💟💟