Slots
关于插槽的基本内容可以查看vue官网
Slots出现的原因
组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
大部分情况下我们只需要向插槽提供数据,至于如何渲染模板,在定义插槽时是不需要在意的。但是总归有特殊情况的嘛
如何对传入的Slots进行更改
代码结构的更改
以下的例子是基于Vue3 和 antd vue的
在后端管理系统中这种搜索条件的展示很常见的,确实不难完成,但是作为一个开发者,肯定还是会考虑到组件化,提高代码复用
首先分析下
- 需要一个
<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>
其实推荐第二种,维护性更高,也充分的发挥了数据流的作用,尤其在嵌套层次深,并且数据来源多的情况下可维护性更好