fix(@vben-core/form-ui)!: group field slot component props (#8215)
* refactor(@vben-core/form-ui)!: group field slot component props * fix(@vben-core/form-ui): stabilize grouped field slot bindings Resolve function-based common component props with field context. Preserve internal model and event bindings when custom field slots spread componentProps. Add regression coverage for array contexts, disabled precedence, and stale binding isolation. * feat(@vben/vite-config): warn about field slot migration Print the temporary field-slot migration warning when the development server starts. Inject the same message into the browser console and keep the plugin serve-only. * docs(@vben/docs): document field slot migration Place the breaking field-slot notice at the start of both form documents. Explain old and new bindings, development warnings, and planned notice removal. * feat(@vben/playground): extend custom form example Add a switchable input and select field to demonstrate schema component updates. Update the composite phone field to emit new tuple values so validation observes changes.
This commit is contained in:
@@ -91,7 +91,7 @@ function updateMenuBadge() {
|
||||
<Card title="徽标更新">
|
||||
<Form>
|
||||
<template #badgeVariants="slotProps">
|
||||
<RadioGroup v-bind="slotProps">
|
||||
<RadioGroup v-bind="slotProps.componentProps">
|
||||
<Radio
|
||||
v-for="color in colors"
|
||||
:key="color.value"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, markRaw } from 'vue';
|
||||
import { h, markRaw, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card, Input, message } from 'antdv-next';
|
||||
import { Button, Card, Input, message, Select } from 'antdv-next';
|
||||
|
||||
import { useVbenForm, z } from '#/adapter/form';
|
||||
|
||||
@@ -14,7 +14,8 @@ interface CustomFormValues extends Record<string, unknown> {
|
||||
field1?: string;
|
||||
field2?: string;
|
||||
field3?: string;
|
||||
field4?: [string | undefined, string];
|
||||
field4?: [string | undefined, string | undefined];
|
||||
field5?: string;
|
||||
}
|
||||
|
||||
function encodeCustomFormValues(values: Readonly<CustomFormValues>) {
|
||||
@@ -28,6 +29,8 @@ function encodeCustomFormValues(values: Readonly<CustomFormValues>) {
|
||||
|
||||
type CustomSubmitValues = ReturnType<typeof encodeCustomFormValues>;
|
||||
|
||||
const dynamicComponentType = ref<'input' | 'select'>('input');
|
||||
|
||||
function decodeCustomFormValues(
|
||||
values: Readonly<CustomSubmitValues>,
|
||||
): CustomFormValues {
|
||||
@@ -38,7 +41,7 @@ function decodeCustomFormValues(
|
||||
};
|
||||
}
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
const [Form, formApi] = useVbenForm({
|
||||
codec: {
|
||||
decode: decodeCustomFormValues,
|
||||
encode: encodeCustomFormValues,
|
||||
@@ -89,7 +92,6 @@ const [Form] = useVbenForm({
|
||||
{
|
||||
component: markRaw(TwoFields),
|
||||
defaultValue: [undefined, ''],
|
||||
changeEventFallback: true,
|
||||
fieldName: 'field4',
|
||||
formItemClass: 'col-span-1',
|
||||
label: '组合字段',
|
||||
@@ -107,11 +109,55 @@ const [Form] = useVbenForm({
|
||||
message: ' 号码格式不正确',
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: markRaw(Input),
|
||||
componentProps: {
|
||||
placeholder: '请输入动态组件值',
|
||||
},
|
||||
fieldName: 'field5',
|
||||
label: '动态组件',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
],
|
||||
// 中屏一行显示2个,小屏一行显示1个
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2',
|
||||
});
|
||||
|
||||
function handleToggleDynamicComponent() {
|
||||
const nextType = dynamicComponentType.value === 'input' ? 'select' : 'input';
|
||||
dynamicComponentType.value = nextType;
|
||||
|
||||
if (nextType === 'select') {
|
||||
formApi.updateSchema([
|
||||
{
|
||||
component: markRaw(Select),
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '选项一', value: 'option-1' },
|
||||
{ label: '选项二', value: 'option-2' },
|
||||
],
|
||||
placeholder: '请选择动态组件值',
|
||||
},
|
||||
fieldName: 'field5',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
component: markRaw(Input),
|
||||
componentProps: {
|
||||
placeholder: '请输入动态组件值',
|
||||
},
|
||||
fieldName: 'field5',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function onSubmit(values: CustomSubmitValues) {
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
@@ -122,9 +168,16 @@ function onSubmit(values: CustomSubmitValues) {
|
||||
<template>
|
||||
<Page description="表单组件自定义示例" title="表单组件">
|
||||
<Card title="基础示例">
|
||||
<template #extra>
|
||||
<Button @click="handleToggleDynamicComponent">
|
||||
{{
|
||||
dynamicComponentType === 'input' ? '切换为下拉框' : '切换为输入框'
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
<Form>
|
||||
<template #field3="slotProps">
|
||||
<Input placeholder="请输入" v-bind="slotProps" />
|
||||
<Input placeholder="请输入" v-bind="slotProps.componentProps" />
|
||||
</template>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SelectValue } from 'antdv-next';
|
||||
|
||||
import { Input, Select } from 'antdv-next';
|
||||
|
||||
const emit = defineEmits(['blur', 'change']);
|
||||
const emit = defineEmits(['blur']);
|
||||
|
||||
const modelValue = defineModel<[string | undefined, string | undefined]>({
|
||||
default: () => [undefined, undefined],
|
||||
});
|
||||
|
||||
function onChange() {
|
||||
emit('change', modelValue.value);
|
||||
function handlePhoneChange(value: string | undefined) {
|
||||
modelValue.value = [modelValue.value[0], value];
|
||||
}
|
||||
|
||||
function handleTypeChange(value: SelectValue) {
|
||||
modelValue.value = [
|
||||
typeof value === 'string' ? value : undefined,
|
||||
modelValue.value[1],
|
||||
];
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex w-full gap-1">
|
||||
<Select
|
||||
v-model:value="modelValue[0]"
|
||||
:value="modelValue[0]"
|
||||
class="w-20"
|
||||
placeholder="类型"
|
||||
allow-clear
|
||||
@@ -25,18 +34,18 @@ function onChange() {
|
||||
{ label: '私密', value: 'private' },
|
||||
]"
|
||||
@blur="emit('blur')"
|
||||
@change="onChange"
|
||||
@update:value="handleTypeChange"
|
||||
/>
|
||||
<Input
|
||||
placeholder="请输入11位手机号码"
|
||||
class="flex-1"
|
||||
allow-clear
|
||||
:class="{ 'valid-success': modelValue[1]?.match(/^1[3-9]\d{9}$/) }"
|
||||
v-model:value="modelValue[1]"
|
||||
:value="modelValue[1]"
|
||||
:maxlength="11"
|
||||
type="tel"
|
||||
@blur="emit('blur')"
|
||||
@change="onChange"
|
||||
@update:value="handlePhoneChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -108,7 +108,7 @@ function getNodeClass(node: Recordable<any>) {
|
||||
bordered
|
||||
:default-expanded-level="2"
|
||||
:get-node-class="getNodeClass"
|
||||
v-bind="slotProps"
|
||||
v-bind="slotProps.componentProps"
|
||||
value-field="id"
|
||||
label-field="meta.title"
|
||||
icon-field="meta.icon"
|
||||
|
||||
@@ -107,7 +107,7 @@ function getNodeClass(node: Recordable<any>) {
|
||||
bordered
|
||||
:default-expanded-level="2"
|
||||
:get-node-class="getNodeClass"
|
||||
v-bind="slotProps"
|
||||
v-bind="slotProps.componentProps"
|
||||
value-field="id"
|
||||
label-field="name"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user