Slots(插槽)

296 阅读3分钟

Slots

关于插槽的基本内容可以查看vue官网

Slots出现的原因

组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。

大部分情况下我们只需要向插槽提供数据,至于如何渲染模板,在定义插槽时是不需要在意的。但是总归有特殊情况的嘛

如何对传入的Slots进行更改

代码结构的更改

以下的例子是基于Vue3 和 antd vue的

image.png 在后端管理系统中这种搜索条件的展示很常见的,确实不难完成,但是作为一个开发者,肯定还是会考虑到组件化,提高代码复用

首先分析下

  • 需要一个<Form/>组件为我们提供表单的功能
  • 表单项是需要通过Slots传递的
  • 会提供默认的查询重置 的功能

代码实现


<template>
	<a-form
		ref="formRef"
		:model="formState"
		:layout="layout"
		label-wrap
		:wrapper-col="{ span: 16 }"
		:label-col="{ span: 8 }"
		@finish="handleFinish"
	>
        
        	
		<template v-if="$slots.default">
                	<!-- 这个组件是对传入的表单项进行响应式布局的-->
			<SearchRowHelp
				ref="formHelpRef"
				v-bind="$attrs"
			>
                        <!-- 这里就是表单项需要展示的插槽位置 🎈🎈🎈🎈🎈-->
				<slot name="default"></slot>
                                
				<template #searchTools>
					<a-form-item>
                                        <!-- 这里就是提供功能按钮的区域 🎈🎈🎈🎈🎈-->
                                        <!-- :form-ref="formRef" 也就是作用域插槽,方便拓展按钮功能 🎈🎈🎈🎈🎈-->
                                        <!-- 这里是把定义的SearchTools插槽传递到了<a-form-item>组件的default插槽中 🎈🎈得理解下🎈🎈🎈-->
						<slot
							name="searchTools"
							:form-ref="formRef"
						></slot>
                                                
                                                <!-- 这里是默认的查询和重置按钮 🎈🎈🎈🎈🎈-->
						<a-button
							type="primary"
							html-type="submit"
						>
							查询
						</a-button>
						<a-button @click="resetForm">重置</a-button>
						<a-button
							type="link"
							@click="handleTriggerIsContract"
						>
							{{ formHelpRef?.isContract ? '展开' : '折叠' }}</a-button
						>
					</a-form-item>
				</template>
			</SearchRowHelp>
		</template>
	</a-form>
</template>


<script lang="ts">
export default {
	inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import { ref } from 'vue'
import { FormInstance, FormProps } from 'ant-design-vue'
import SearchRowHelp from '@/components/Table/src/components/SearchRowHelp.vue'

type FormState = Record<string, any>
interface IFormProps extends Omit<FormProps, 'model' | 'layout'> {
	tableRef: TableActionType | null
	layout?: 'horizontal'
	formState: FormState
}

const props = withDefaults(defineProps<IFormProps>(), {
	layout: 'horizontal',
})

/**
 * 得到form实例
 */
const formRef = ref<FormInstance>()

/**
 *得到SearchRowHelp的实例
 */
const formHelpRef = ref<{ handleTriggerIsContract: () => void; isContract: boolean }>()

/**
 * 重置表单
 */
const resetForm = () => {
	formRef?.value?.resetFields()
	console.log(formRef.value?.getFieldsValue())
}

/**
 *校验通过后表单提交
 * @param values
 */
const handleFinish = (values: FormState) => {
	props.tableRef?.fetch()
}
/**
 * 点击展示/折叠
 */
const handleTriggerIsContract = () => {
	formHelpRef.value?.handleTriggerIsContract()
}

defineExpose({
	formRef,
})
</script>

Form组件已经写好了,现在就是对传入默认插槽的表单项进行响应式布局

  • 因为更改的内容涉及到传入默认插槽的结构,模板语法,我没找到什么合适的方法,就使用了jsx
<script lang="tsx" setup>
import { ref, useSlots, withDefaults } from 'vue'

// 常量
const DEFAULT_MIN_ITEMS = 1
const DEFAULT_COLUMN_SPAN = {
	xxxl: 4,
	xxl: 4,
	xl: 6,
	lg: 8,
	md: 8,
	xs: 24,
	sm: 12,
}
const DISPLAY_BLOCK = 'block'
const DISPLAY_NONE = 'none'
const slot = useSlots()
const props = withDefaults(
	defineProps<{
		min?: number | (() => number)
	}>(),
	{
		min: 1,
	}
)

// 是否折叠
const isContract = ref(true)

// 得到传入的表单项
const getFormItems = (arr: any[] = []) => {
	return arr.map(ele => {
		return ele?.children
	})
}

// 得到页面的jsx结构
const renderFormItem = (arr, key) => {
	if (key === 'items') {
        // 这里就是对表单项格式化的代码🎈🎈🎈🎈
		return arr?.map((item, index) => {
                // 设立的style 写在compouter中可以好看点,阅读困难会下降🎈我就这样了不改了🎈哈哈🎈🎈🎈
			return (
				<a-col
					style={{ display: !isContract.value ? DISPLAY_BLOCK : index < props.min ? DISPLAY_BLOCK : DISPLAY_NONE }}
					{...DEFAULT_COLUMN_SPAN}
					key={index}
				>
					{item}
				</a-col>
			)
		})
	} else {
		return (
                // 这里是对功能区域 按钮做了格式化
			<a-col>
				<a-form-item>
					<a-space>{arr}</a-space>
				</a-form-item>
			</a-col>
		)
	}
}

/**
    这里关于 handleTriggerIsContract,isContract,
    的设计是不太合理的,因为我们在这个组件中应该只关心元素结构的渲染🎈🎈🎈🎈
    所以应该提到上一父级,如果需要使用到使用props传递就可以了

*/
const handleTriggerIsContract = () => {
	isContract.value = !isContract.value
}
defineExpose({
	handleTriggerIsContract,
	isContract,
})
// 这就是渲染函数了
function render() {
        // 得到默认插槽
	const formListArray = getFormItems(slot.default?.()).flat() || []
        //slot.searchTools?.()?.[0].children 得到的是<a-form-item>
        //.default?.()才可以得到传入的功能按钮
	const formDefaultToolsArray = slot.searchTools?.()?.[0].children?.default?.()
	const obj = {
		items: formListArray,
		tools: [...formDefaultToolsArray],
	}
	const dom = (
		<a-row gutter={10}>
			{renderFormItem(obj.items, 'items')}
			{renderFormItem(obj.tools, 'tools')}
		</a-row>
	)
	return <>{dom}</>
}
</script>
<template>
	<component :is="render()"></component>
</template>

这样关于需求的组件化开发就完成了看一看使用

<SearchForm
    ref="searchFormRef"
    :form-state="formState"
>
    <a-form-item
            label="姓名"
            name="pass"
    >
            <a-input
                    v-model:value="formState.pass"
                    autocomplete="off"
                    placeholder="请输入姓名"
            />
    </a-form-item>
    <a-form-item
            label="Confirm"
            name="checkPass"
    >
            <a-input
                    v-model:value="formState.checkPass"
                    type="password"
                    autocomplete="off"
                    placeholder="请确认密码"
            />
    </a-form-item>
</SearchForm>

可以看到,需求已经可以完成了,其实还是存在可优化的点的

  • 表单项这样传递显得比较麻烦(可以保留)
  • 可以支持为传递props的方式去描述表单项,然后再内部转换
  • 实现的话可以通过默认插槽的形式不传递Slots时直接通过props去得到

传递数据的更改

其实数据的传递使用作用域插槽就够用了 如果传递的内容比较统一或者很多,并且组件的层级不深还是使用jsx可以做一下处理的

涉及到深层次的组件还是通过其他方法,后边也会提到一种解决方法

  <a-form-item
            label="姓名"
            name="pass"
    >
            <a-input
                    v-model:value="formState.pass"
                    autocomplete="off"
                    placeholder="请输入姓名"
            />
    </a-form-item>

看到没 在<a-form-item>中有个属性label,在<a-input>中也有个placeholder两者是由关联的,一两个还好,多了也就烦了哈哈

可以改一下

<script setup lang="tsx">
import { FormItemProps } from 'ant-design-vue'
import { cloneVNode, useAttrs, useSlots } from 'vue'

const slot = useSlots()
const attrs = useAttrs()

// 这里就为每一个类似于`<a-input>`的组件加上了一个默认的`placeholder`通过`label`计算得到
//,当然也是可以重新覆盖的
const renderItem = (arr: any[] = []) => {
	return arr.map(v => {
		const { name, __name } = v.type
		if (/^.*[S|s]elect.*$/g.test(name ?? __name)) {
                🎈🎈🎈就在这里了🎈🎈🎈
			return cloneVNode(v, { placeholder: `请选择${attrs.label}`, ...v.props })
		} else if (/^.*[I|i]nput.*|.*[T|t]extarea.*$/g.test(name ?? __name)) {
			return cloneVNode(v, { placeholder: `请输入${attrs.label}`, ...v.props })
		} else {
			return cloneVNode(v)
		}
	})
}

function render() {
	return (
		<a-col
                    style={{ padding: '0 25px' }}
		>
                    <a-form-item {...attrs}>{renderItem(slot.default?.())}</a-form-item>
		</a-col>
	)
}
</script>

<template>
	<component :is="render()"></component>
</template>
  <a-form-item
            label="姓名"
            name="pass"
    >
            <a-input
                    v-model:value="formState.pass"
                    autocomplete="off"
            />
            <!-- placeholder="请输入姓名" 删掉也可以-->
    </a-form-item>

这些都用到了Vue为我们提供的渲染函数和jsx的功能

如果层级太多,都写成jsx那还不如用react的是吧

可以使用vue的一种通信方式:provide和hook的形式 还是刚刚的那个例子

    import { provide, inject} from 'vue'
    const key = Symbol('basic-table')
    const createFormItemContext (context) {
        provide(key, instance)
    }
    
    const useFormItemContext() {
        inject(key)
    }

<Form-Item/>

<script setup lang="tsx">
import { FormItemProps } from 'ant-design-vue'
const props = withDefaults(defineProps<FormItemProps>(), {
	label:""
})
createFormItemContext()
</script>

<template>
	<slot></slot>
</template>

<Input/>

<script lang="ts" setup>
import type { InputProps } from 'ant-design-vue'
import { useAttrs } from 'vue'

interface RjInputProps extends InputProps {}

defineProps<RjInputProps>()

const emit = defineEmits(['change', 'clear'])

const { label } = useFormItemContext()

const change = (e) => {
	emit('change', e)
	if (e.type == 'click') {
		emit('clear', e)
	}
}

const attrs = useAttrs()
</script>

<template>
    <a-input :placeholder="请输入`${label}`" v-bind="$attrs" @change="change"> </a-input>
</template>

  <a-form-item
            label="姓名"
            name="pass"
    >
            <MyInput
                    v-model:value="formState.pass"
                    autocomplete="off"
            />
    </a-form-item>

其实推荐第二种,维护性更高,也充分的发挥了数据流的作用,尤其在嵌套层次深,并且数据来源多的情况下可维护性更好