vue3 过滤插槽里的组件
获取到插槽,也就是VNode,根据type的名称把不符合名称的组件过滤出去
想实现类似ElementUI的select效果,在el-sleect
组件中放文本div或是别的组件都不显示只能显示el-option
组件。ElementU源码里没找不到是怎么实现的。按道理应该是通过插槽的某些条件做的过滤。
用useSlots().default()打印出来的VNode里面有一个type 里面的__name是它的文件名称,注意:如果<script setup></style>
标签里没有写js代码是不会有__name的。因为是直接判断文件名称的没办法区分两个同名组件
新建一个父组件 parent.vue引入use-filterSlots.ts
<slot />
标签是不能用了,要做的是把处理好的节点用动态组件component循环出来
<template>
<div class="parent">
<template v-for="(item, index) in selectItem" :key="index">
<component :is="item" />
</template>
</div>
</template>
<script setup lang="ts">
import filterSlots from './select/use-filterSlots'
import { toRef } from 'vue'
const props = defineProps<{ componentName: string }>()
const name = toRef(props, 'componentName') //引用props传进来的名称 用来动态控制过滤的组件名称
const { selectItem } = filterSlots(name)
</script>
<style>
.parent {
cursor: pointer;
min-width: 300px;
padding: 5px;
border: 3px solid #ababab;
border-radius: 8px;
}
</style>
use-filterSlots.ts用来处理VNode数组
import type { ComputedRef, Ref, RendererElement, RendererNode, VNode } from 'vue';
import { computed, ref, unref, useSlots, watchEffect } from 'vue';
export type VNodeItem = VNode<RendererNode, RendererElement, { [key: string]: any; }>
export type Callback = (vnode: VNodeItem) => boolean
export type FilterComponents = (
option: string | Callback | Ref<string | Callback> | ComputedRef<string | Callback>,
...componentName: string[]
) => { selectItem: Ref<VNodeItem[]>, slotsList: ComputedRef<VNodeItem[]> }
/**
* 过滤插槽组件保留指定组件
* @param { * } item 要保留组件名称或者传入一个回调函数
* @param { * } componentName 多个组件名称
* @returns { { selectItem, slotsList} }
*/
const filterSlots: FilterComponents = (item, ...componentName) => {
const slots = useSlots()
const slotsList = computed(() => slots.default ? slots.default() : [])//默认插槽内容 过滤前
const selectItem = ref<VNodeItem[]>([])//存放过滤后
// 根据type.__name判断 把对应相同名称的组件添加到selectItem
watchEffect(() => {
const option = unref(item)//转换Ref
const vnodeList = [option, ...componentName].filter(String)//要保留的组件名称
let optionsItem: VNodeItem[] = []//临时存放过滤后的VNode
// 判断名称
const condition = (vNode: VNodeItem): boolean => {
let type = vNode.type as string | { [key: string]: string }
// 传入参数是function就直接使用function返回的判断结果
if (typeof option === 'function') {
return !!option(vNode)
}
// type是object为vue组件 判断type.__name是否存在组件名称列表中
if (typeof type === 'object') {
return vnodeList.includes(type.__name)
}
// 原生标签
return vnodeList.includes(type)
}
slotsList.value.forEach((a) => {
// 判断是否有template包裹 有就需要把template里面的拿出来
if (Array.isArray(a.children) && typeof a.type === 'symbol') {
let children = a.children as VNodeItem[]
optionsItem.push(...children.filter(condition))
return
}
// 判断名称对应就添加到optionsItem
condition(a) && optionsItem.push(a)
})
// 把过滤后的Vnode更新到 selectItem
selectItem.value = optionsItem
})
return {
slotsList,//过滤前
selectItem,//过滤后
}
}
export default filterSlots
parent.vue组件引入到App.vue中
<script setup lang="ts">
import { ref } from 'vue'
import optionView from './optionView.vue'
import Parentl from './parentl.vue'
import ikun from './ikun.vue'
const componentName = ref('ikun') //控制显示的组件
</script>
<template>
<button @click="componentName = 'optionView'">optionView</button>
<button @click="componentName = 'div'">div</button>
<button @click="componentName = 'span'">span</button>
<button @click="componentName = 'ikun'">ikun</button>
<Parentl :componentName="componentName">
3211312113
<optionView>optionView</optionView>
<optionView>optionView</optionView>
<optionView>optionView</optionView>
<ikun>ikun</ikun>
<ikun>ikun</ikun>
<ikun>ikun</ikun>
<ikun>ikun</ikun>
<optionView v-for="_ in 10">optionView{{ _ }}</optionView>
<div v-for="_ in 10">div{{ _ }}</div>
<span v-for="_ in 5">span{{ _ }}</span>
<template v-for="_ in 10">
<optionView>optionView</optionView>
<div v-for="_ in 10">div</div>
<span v-for="_ in 5">span</span>
</template>
</Parentl>
</template>