diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md
index c21c6420..ba3613b0 100644
--- a/docs/src/components/common-ui/vben-form.md
+++ b/docs/src/components/common-ui/vben-form.md
@@ -4,6 +4,24 @@ outline: deep
# Vben Form 表单
+::: warning 字段插槽破坏性变更
+
+字段命名 slot 的控件绑定已统一收拢到 `slotProps.componentProps`。旧写法会把 `field`、`formApi`、`values` 等表单元数据一并传给实际控件,可能产生无效属性和 Vue 运行时警告。
+
+```vue
+
+
+
+
+
+```
+
+请将所有字段 slot 的 `v-bind="slotProps"` 迁移为 `v-bind="slotProps.componentProps"`。根级的 `field`、`componentField`、`modelValue`、`name`、`disabled`、`isInValid`、`values` 和 `formApi` 仍可用于模板逻辑,但不会再自动传入实际控件。
+
+当前版本启动 Vben 应用或 Playground 开发服务器时会在终端输出一次迁移警告,页面加载时浏览器控制台也会提示。该提示不会进入生产构建,并计划在下个版本移除。
+
+:::
+
框架提供的表单组件,可适配 `Element Plus`、`Ant Design Vue`、`Naive UI` 等框架。
> 如果文档内没有参数说明,可以尝试在在线示例内寻找
@@ -385,7 +403,9 @@ async function fillForm() {
```
-字段命名插槽提供 `field`、`componentField`、`modelValue`、`name`、`disabled`、`isInValid`、`values` 和 `formApi`。默认插槽提供 `shapes`、`values` 和 `formApi`;`reset-before`、`submit-before`、`expand-before`、`expand-after` 提供 `values` 和 `formApi`。未声明 `TValues` 时仍兼容任意字段名,但 slot props 会回退为宽泛类型。
+字段命名插槽提供完整控件绑定 `componentProps`,以及 `field`、`componentField`、`modelValue`、`name`、`disabled`、`isInValid`、`values` 和 `formApi`。默认插槽提供 `shapes`、`values` 和 `formApi`;`reset-before`、`submit-before`、`expand-before`、`expand-after` 提供 `values` 和 `formApi`。
+
+建议为表单声明没有字符串索引签名的精确接口,使每个字段插槽都能推导自己的值类型。使用 `Record` 等宽泛类型时,slot props 仍保持完整结构,不再整体退化为 `any`,但字段值只能推导为索引值类型。
### FormApi
@@ -746,6 +766,18 @@ import { z } from '#/adapter/form';
::: tip 字段插槽
-除了以上内置插槽之外,`schema`属性中每个字段的`fieldName`都可以作为插槽名称,这些字段插槽的优先级高于`component`定义的组件。也就是说,当提供了与`fieldName`同名的插槽时,这些插槽的内容将会作为这些字段的组件,此时`component`的值将会被忽略。
+除了以上内置插槽之外,`schema` 属性中每个字段的 `fieldName` 都可以作为插槽名称。这些字段插槽的优先级高于 `component` 定义的组件。
+
+字段 slot 的控件绑定统一收拢在 `componentProps` 中,其中包含模型值、对应的 `update:*` 事件、schema/common/dependencies props 和 disabled 状态:
+
+```vue
+
+```
+
+`field`、`componentField`、`modelValue`、`name`、`disabled`、`isInValid`、`values` 和 `formApi` 保留在 slot 根级,供模板逻辑使用,不会自动传入实际控件。
:::
diff --git a/docs/src/demos/vben-form/custom/index.vue b/docs/src/demos/vben-form/custom/index.vue
index cd6a55cb..99b6040f 100644
--- a/docs/src/demos/vben-form/custom/index.vue
+++ b/docs/src/demos/vben-form/custom/index.vue
@@ -62,7 +62,7 @@ function onSubmit(values: Record) {
diff --git a/docs/src/en/components/common-ui/vben-form.md b/docs/src/en/components/common-ui/vben-form.md
index 61173123..6e09df01 100644
--- a/docs/src/en/components/common-ui/vben-form.md
+++ b/docs/src/en/components/common-ui/vben-form.md
@@ -4,6 +4,24 @@ outline: deep
# Vben Form
+::: warning Field Slot Breaking Change
+
+Named field slot control bindings are now grouped under `slotProps.componentProps`. The old binding forwards form metadata such as `field`, `formApi`, and `values` to the rendered control, which can produce invalid attributes and Vue runtime warnings.
+
+```vue
+
+
+
+
+
+```
+
+Migrate every field slot from `v-bind="slotProps"` to `v-bind="slotProps.componentProps"`. Root metadata remains available for template logic through `field`, `componentField`, `modelValue`, `name`, `disabled`, `isInValid`, `values`, and `formApi`, but it is no longer forwarded automatically to the rendered control.
+
+In this release, starting a Vben application or Playground development server prints this migration warning in the terminal, and loading the page prints the same warning in the browser console. The warning is excluded from production builds and is planned for removal in the next release.
+
+:::
+
`Vben Form` is the shared form abstraction used across different UI-library variants such as `Ant Design Vue`, `Element Plus`, `Naive UI`, and other adapters added inside this repository.
It uses [TanStack Form](https://tanstack.com/form/latest/docs/framework/vue/overview) internally for state and validation lifecycles, with [Zod 4](https://zod.dev/v4) schemas. Application code should continue using `useVbenForm`, `FormApi`, and the adapter layer instead of depending on the raw TanStack instance.
@@ -252,7 +270,23 @@ async function fillForm() {
```
-Named field slots expose `field`, `componentField`, `modelValue`, `name`, `disabled`, `isInValid`, `values`, and `formApi`. The default slot exposes `shapes`, `values`, and `formApi`; action slots expose `values` and `formApi`. Forms without an explicit `TValues` remain compatible with arbitrary slot names and broad props.
+Named field slots expose grouped control bindings through `componentProps`, together with `field`, `componentField`, `modelValue`, `name`, `disabled`, `isInValid`, `values`, and `formApi`. The default slot exposes `shapes`, `values`, and `formApi`; action slots expose `values` and `formApi`.
+
+Use a precise form-value interface without a string index signature to infer each field value. A broad type such as `Record` keeps the complete slot-prop structure instead of degrading the whole scope to `any`, but field values can only use the declared index value type.
+
+## Field Slots
+
+Control bindings are grouped under `componentProps`. It contains the model value, matching `update:*` event, schema/common/dependency props, and disabled state:
+
+```vue
+
+```
+
+Root metadata remains available for template logic through `field`, `componentField`, `modelValue`, `name`, `disabled`, `isInValid`, `values`, and `formApi`; it is not forwarded automatically to the rendered control.
## Form Codec
diff --git a/internal/vite-config/src/plugins/form-field-slot-migration-warning.test.ts b/internal/vite-config/src/plugins/form-field-slot-migration-warning.test.ts
new file mode 100644
index 00000000..6a58cb10
--- /dev/null
+++ b/internal/vite-config/src/plugins/form-field-slot-migration-warning.test.ts
@@ -0,0 +1,43 @@
+import type { ResolvedConfig } from 'vite';
+
+import { describe, expect, it, vi } from 'vitest';
+
+import {
+ FORM_FIELD_SLOT_MIGRATION_WARNING,
+ viteFormFieldSlotMigrationWarningPlugin,
+} from './form-field-slot-migration-warning';
+
+describe('form field slot migration warning plugin', () => {
+ it('warns in serve mode and injects the same browser message', () => {
+ const plugin = viteFormFieldSlotMigrationWarningPlugin();
+ const warning = vi.fn();
+
+ expect(plugin.apply).toBe('serve');
+ expect(plugin.configResolved).toBeTypeOf('function');
+ if (typeof plugin.configResolved !== 'function') return;
+
+ plugin.configResolved({ logger: { warn: warning } } as ResolvedConfig);
+
+ expect(warning).toHaveBeenCalledOnce();
+ expect(warning).toHaveBeenCalledWith(FORM_FIELD_SLOT_MIGRATION_WARNING);
+ expect(plugin.transformIndexHtml).toBeTypeOf('function');
+ if (typeof plugin.transformIndexHtml !== 'function') return;
+
+ const result = plugin.transformIndexHtml('', {} as never);
+
+ expect(result).toEqual([
+ {
+ attrs: {
+ 'data-vben-form-field-slot-migration-warning': '',
+ },
+ children: `console.warn(${JSON.stringify(FORM_FIELD_SLOT_MIGRATION_WARNING)});`,
+ injectTo: 'body',
+ tag: 'script',
+ },
+ ]);
+ expect(FORM_FIELD_SLOT_MIGRATION_WARNING).toContain('`v-bind="slotProps"`');
+ expect(FORM_FIELD_SLOT_MIGRATION_WARNING).toContain(
+ '`v-bind="slotProps.componentProps"`',
+ );
+ });
+});
diff --git a/internal/vite-config/src/plugins/form-field-slot-migration-warning.ts b/internal/vite-config/src/plugins/form-field-slot-migration-warning.ts
new file mode 100644
index 00000000..535ef5d9
--- /dev/null
+++ b/internal/vite-config/src/plugins/form-field-slot-migration-warning.ts
@@ -0,0 +1,31 @@
+import type { Plugin } from 'vite';
+
+const FORM_FIELD_SLOT_MIGRATION_WARNING =
+ '[Vben Form] BREAKING CHANGE: Named field slot control bindings moved to `slotProps.componentProps`. Replace `v-bind="slotProps"` with `v-bind="slotProps.componentProps"`. See https://doc.vben.pro/components/common-ui/vben-form.html';
+
+function viteFormFieldSlotMigrationWarningPlugin(): Plugin {
+ return {
+ apply: 'serve',
+ configResolved(config) {
+ config.logger.warn(FORM_FIELD_SLOT_MIGRATION_WARNING);
+ },
+ name: 'vite:form-field-slot-migration-warning',
+ transformIndexHtml() {
+ return [
+ {
+ attrs: {
+ 'data-vben-form-field-slot-migration-warning': '',
+ },
+ children: `console.warn(${JSON.stringify(FORM_FIELD_SLOT_MIGRATION_WARNING)});`,
+ injectTo: 'body',
+ tag: 'script',
+ },
+ ];
+ },
+ };
+}
+
+export {
+ FORM_FIELD_SLOT_MIGRATION_WARNING,
+ viteFormFieldSlotMigrationWarningPlugin,
+};
diff --git a/internal/vite-config/src/plugins/index.ts b/internal/vite-config/src/plugins/index.ts
index f7bdfd28..d6a14784 100644
--- a/internal/vite-config/src/plugins/index.ts
+++ b/internal/vite-config/src/plugins/index.ts
@@ -20,6 +20,7 @@ import viteVueDevTools from 'vite-plugin-vue-devtools';
import { viteArchiverPlugin } from './archiver';
import { viteDayjsPlugin } from './dayjs';
import { viteExtraAppConfigPlugin } from './extra-app-config';
+import { viteFormFieldSlotMigrationWarningPlugin } from './form-field-slot-migration-warning';
import { viteHtmlPlugin } from './html';
import { viteImportMapPlugin } from './importmap';
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
@@ -144,6 +145,10 @@ async function loadApplicationPlugins(
return [await vitePrintPlugin({ infoMap: printInfoMap })];
},
},
+ {
+ condition: !isBuild,
+ plugins: () => [viteFormFieldSlotMigrationWarningPlugin()],
+ },
{
condition: vxeTableLazyImport,
plugins: async () => {
diff --git a/packages/@core/ui-kit/form-ui/__tests__/form-component-performance.benchmark.ts b/packages/@core/ui-kit/form-ui/__tests__/form-component-performance.benchmark.ts
index 31e2a256..e11fb006 100644
--- a/packages/@core/ui-kit/form-ui/__tests__/form-component-performance.benchmark.ts
+++ b/packages/@core/ui-kit/form-ui/__tests__/form-component-performance.benchmark.ts
@@ -65,7 +65,9 @@ const dependencySchema: FormSchema[] = [
component: TestInput,
defaultValue: `Value ${index}`,
dependencies: {
- resolve: ({ values }) => ({ disabled: values.mode === 'locked' }),
+ resolve: ({ values }: { values: Record }) => ({
+ disabled: values.mode === 'locked',
+ }),
triggerFields: ['mode'],
},
fieldName: `dependent${index}`,
diff --git a/packages/@core/ui-kit/form-ui/__tests__/form-integration.test.ts b/packages/@core/ui-kit/form-ui/__tests__/form-integration.test.ts
index 524e86d3..fba7d18e 100644
--- a/packages/@core/ui-kit/form-ui/__tests__/form-integration.test.ts
+++ b/packages/@core/ui-kit/form-ui/__tests__/form-integration.test.ts
@@ -1,6 +1,6 @@
import type { VueWrapper } from '@vue/test-utils';
-import type { FormSchemaRuleType } from '../src/types';
+import type { FormSchemaRuleType, VbenFormFieldSlotProps } from '../src/types';
import { flushPromises, mount } from '@vue/test-utils';
import { defineComponent, h, nextTick } from 'vue';
@@ -104,18 +104,27 @@ describe('useVbenForm integration', () => {
expect(validateValue).toHaveBeenCalledTimes(initialValidationCount + 1);
});
- it('keeps only the active model protocol and boolean disabled in field slots', async () => {
+ it('keeps only the active model protocol in field slot componentProps', async () => {
interface ModelProtocolValues {
defaultField: string;
valueField: string;
}
+ const staleDefaultBlur = vi.fn();
+ const staleDefaultUpdate = vi.fn();
+ const staleValueUpdate = vi.fn();
let defaultSlotProps: Record | undefined;
let valueSlotProps: Record | undefined;
const [Form, formApi] = useVbenForm({
schema: [
{
component: TestInput,
+ componentProps: {
+ modelValue: 'stale-default-value',
+ name: 'stale-default-name',
+ onBlur: staleDefaultBlur,
+ 'onUpdate:modelValue': staleDefaultUpdate,
+ },
defaultValue: 'default-initial',
fieldName: 'defaultField',
},
@@ -124,6 +133,8 @@ describe('useVbenForm integration', () => {
componentProps: {
eventMode: 'value-and-change',
modelValue: 'stale-model-value',
+ 'onUpdate:value': staleValueUpdate,
+ value: 'stale-value',
},
defaultValue: 'value-initial',
fieldName: 'valueField',
@@ -136,18 +147,15 @@ describe('useVbenForm integration', () => {
defaultField(slotProps: Record) {
defaultSlotProps = slotProps;
return h(TestInput, {
+ ...slotProps.componentProps,
class: 'default-protocol-input',
- modelValue: slotProps.modelValue,
- 'onUpdate:modelValue': slotProps['onUpdate:modelValue'],
});
},
valueField(slotProps: Record) {
valueSlotProps = slotProps;
return h(TestInput, {
+ ...slotProps.componentProps,
class: 'value-protocol-input',
- eventMode: slotProps.eventMode,
- value: slotProps.value,
- 'onUpdate:value': slotProps['onUpdate:value'],
});
},
},
@@ -159,21 +167,31 @@ describe('useVbenForm integration', () => {
expect(valueSlotProps).toBeDefined();
if (!defaultSlotProps || !valueSlotProps) return;
- expect(defaultSlotProps.disabled).toBe(false);
- expect(defaultSlotProps.modelValue).toBe('default-initial');
- expect(defaultSlotProps).toHaveProperty('onUpdate:modelValue');
- expect(defaultSlotProps).not.toHaveProperty('value');
+ expect(defaultSlotProps.componentProps.disabled).toBe(false);
+ expect(defaultSlotProps.componentProps.modelValue).toBe('default-initial');
+ expect(defaultSlotProps.componentProps.name).toBe('defaultField');
+ expect(defaultSlotProps.componentProps).toHaveProperty(
+ 'onUpdate:modelValue',
+ );
+ expect(defaultSlotProps.componentProps).not.toHaveProperty('value');
- expect(valueSlotProps.disabled).toBe(false);
- expect(valueSlotProps.value).toBe('value-initial');
- expect(valueSlotProps).toHaveProperty('onUpdate:value');
- expect(valueSlotProps).not.toHaveProperty('modelValue');
- expect(valueSlotProps).not.toHaveProperty('onUpdate:modelValue');
+ expect(valueSlotProps.componentProps.disabled).toBe(false);
+ expect(valueSlotProps.componentProps.value).toBe('value-initial');
+ expect(valueSlotProps.componentProps).toHaveProperty('onUpdate:value');
+ expect(valueSlotProps.componentProps).not.toHaveProperty('modelValue');
+ expect(valueSlotProps.componentProps).not.toHaveProperty(
+ 'onUpdate:modelValue',
+ );
+ await wrapper.get('.default-protocol-input').trigger('blur');
await wrapper.get('.default-protocol-input').setValue('default-updated');
await wrapper.get('.value-protocol-input').setValue('value-updated');
await flushPromises();
+ expect(defaultSlotProps.field.state.meta.isTouched).toBe(true);
+ expect(staleDefaultBlur).not.toHaveBeenCalled();
+ expect(staleDefaultUpdate).not.toHaveBeenCalled();
+ expect(staleValueUpdate).not.toHaveBeenCalled();
expect(await formApi.getValues()).toEqual({
defaultField: 'default-updated',
valueField: 'value-updated',
@@ -208,6 +226,109 @@ describe('useVbenForm integration', () => {
expect(wrapper.get('.slot-value').text()).toBe('Grace');
});
+ it('groups control bindings in field slot componentProps', async () => {
+ interface SlotFormValues {
+ name: string;
+ }
+
+ let latestSlotProps:
+ | undefined
+ | VbenFormFieldSlotProps;
+ const [Form, formApi] = useVbenForm({
+ schema: [
+ {
+ component: TestInput,
+ defaultValue: 'Ada',
+ fieldName: 'name',
+ },
+ ],
+ });
+ const wrapper = mount(Form, {
+ slots: {
+ name(slotProps: VbenFormFieldSlotProps) {
+ latestSlotProps = slotProps;
+ return h(TestInput, {
+ ...slotProps.componentProps,
+ class: 'slot-component',
+ });
+ },
+ },
+ });
+ wrappers.push(wrapper);
+ await flushPromises();
+
+ expect(latestSlotProps).toBeDefined();
+ if (!latestSlotProps) return;
+ expect(latestSlotProps.formApi).toBe(formApi);
+ expect(latestSlotProps.values).toEqual({ name: 'Ada' });
+ expect(latestSlotProps.modelValue).toBe('Ada');
+ expect(latestSlotProps.componentProps.modelValue).toBe('Ada');
+ expect(latestSlotProps.componentProps.disabled).toBe(false);
+ expect(latestSlotProps.componentProps).toHaveProperty(
+ 'onUpdate:modelValue',
+ );
+ expect(latestSlotProps.componentProps).not.toHaveProperty('formApi');
+ expect(latestSlotProps.componentProps).not.toHaveProperty('values');
+ expect(latestSlotProps).not.toHaveProperty('onUpdate:modelValue');
+
+ await wrapper.get('.slot-component').setValue('Grace');
+ await flushPromises();
+
+ expect(latestSlotProps.modelValue).toBe('Grace');
+ expect(latestSlotProps.values.name).toBe('Grace');
+ });
+
+ it('normalizes disabled state in field slot componentProps', async () => {
+ interface DisabledFormValues {
+ commonDisabled: string;
+ dependencyDisabled: string;
+ }
+
+ const fieldSlotProps: Record> = {};
+ const [Form] = useVbenForm({
+ commonConfig: { disabled: true },
+ schema: [
+ {
+ component: TestInput,
+ componentProps: { disabled: false },
+ fieldName: 'commonDisabled',
+ },
+ {
+ component: TestInput,
+ componentProps: { disabled: false },
+ dependencies: {
+ resolve: () => ({ disabled: true }),
+ triggerFields: [],
+ },
+ fieldName: 'dependencyDisabled',
+ },
+ ],
+ });
+ const wrapper = mount(Form, {
+ slots: {
+ commonDisabled(slotProps: Record) {
+ fieldSlotProps.commonDisabled = slotProps;
+ return h(TestInput, slotProps.componentProps);
+ },
+ dependencyDisabled(slotProps: Record) {
+ fieldSlotProps.dependencyDisabled = slotProps;
+ return h(TestInput, slotProps.componentProps);
+ },
+ },
+ });
+ wrappers.push(wrapper);
+ await flushPromises();
+
+ expect(fieldSlotProps.commonDisabled).toBeDefined();
+ expect(fieldSlotProps.dependencyDisabled).toBeDefined();
+ expect(fieldSlotProps.commonDisabled?.disabled).toBe(true);
+ expect(fieldSlotProps.commonDisabled?.componentProps.disabled).toBe(true);
+ expect(fieldSlotProps.dependencyDisabled?.disabled).toBe(true);
+ expect(fieldSlotProps.dependencyDisabled?.componentProps.disabled).toBe(
+ true,
+ );
+ });
+
it('supports a field-level change event fallback for legacy components', async () => {
const [Form, formApi] = useVbenForm({
schema: [
diff --git a/packages/@core/ui-kit/form-ui/__tests__/form-schema.test.ts b/packages/@core/ui-kit/form-ui/__tests__/form-schema.test.ts
new file mode 100644
index 00000000..b4294ef2
--- /dev/null
+++ b/packages/@core/ui-kit/form-ui/__tests__/form-schema.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import {
+ createArrayChildSchema,
+ createFormFieldSchema,
+} from '../src/form-render/schema';
+
+describe('form schema normalization', () => {
+ it('resolves common component props with the field context', () => {
+ const componentProps = vi.fn(({ fieldName }) => ({
+ placeholder: `Enter ${fieldName}`,
+ }));
+
+ const schema = createFormFieldSchema(
+ { component: 'VbenInput', fieldName: 'name' },
+ { commonConfig: { componentProps } },
+ );
+
+ expect(componentProps).toHaveBeenCalledWith({ fieldName: 'name' });
+ expect(schema.commonComponentProps).toEqual({
+ placeholder: 'Enter name',
+ });
+ });
+
+ it('preserves common component props objects', () => {
+ const schema = createFormFieldSchema(
+ { component: 'VbenInput', fieldName: 'name' },
+ { commonConfig: { componentProps: { placeholder: 'Enter a name' } } },
+ );
+
+ expect(schema.commonComponentProps).toEqual({
+ placeholder: 'Enter a name',
+ });
+ });
+
+ it('resolves global common component props functions', () => {
+ const componentProps = vi.fn(({ fieldName }) => ({
+ title: `Global ${fieldName}`,
+ }));
+
+ const schema = createFormFieldSchema(
+ { component: 'VbenInput', fieldName: 'email' },
+ { globalCommonConfig: { componentProps } },
+ );
+
+ expect(componentProps).toHaveBeenCalledWith({ fieldName: 'email' });
+ expect(schema.commonComponentProps).toEqual({ title: 'Global email' });
+ });
+
+ it('resolves array common props with the row context', () => {
+ const componentProps = vi.fn(() => ({ placeholder: 'Contact name' }));
+
+ const schema = createArrayChildSchema(
+ { component: 'VbenInput', fieldName: 'name' },
+ {
+ arrayField: 'contacts',
+ commonConfig: { componentProps },
+ index: 1,
+ },
+ );
+
+ expect(componentProps).toHaveBeenCalledWith({
+ arrayField: 'contacts',
+ fieldName: 'contacts[1].name',
+ originalFieldName: 'name',
+ rowIndex: 1,
+ rowPath: 'contacts[1]',
+ });
+ expect(schema.commonComponentProps).toEqual({
+ placeholder: 'Contact name',
+ });
+ });
+});
diff --git a/packages/@core/ui-kit/form-ui/__tests__/form-types.test.ts b/packages/@core/ui-kit/form-ui/__tests__/form-types.test.ts
index f44c468a..23a4a49c 100644
--- a/packages/@core/ui-kit/form-ui/__tests__/form-types.test.ts
+++ b/packages/@core/ui-kit/form-ui/__tests__/form-types.test.ts
@@ -141,6 +141,9 @@ describe('form public types', () => {
EmailSlotProps['field']['state']['value']
>().toEqualTypeOf();
expectTypeOf().toEqualTypeOf();
+ expectTypeOf<
+ EmailSlotProps['componentProps']['modelValue']
+ >().toEqualTypeOf();
expectTypeOf().toEqualTypeOf<
ExtendedFormApi
>();
diff --git a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
index 3c4d7c83..64566192 100644
--- a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
+++ b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
@@ -361,9 +361,10 @@ function createComponentProps(slotProps: RuntimeFieldSlotProps) {
);
const binds = {
- ...normalizedSlotProps.componentField,
...computedProps.value,
+ ...normalizedSlotProps.componentField,
...bindEvents,
+ disabled: shouldDisabled.value,
...(Reflect.has(computedProps.value, 'onChange')
? { onChange: computedProps.value.onChange }
: {}),
@@ -379,6 +380,17 @@ function createComponentProps(slotProps: RuntimeFieldSlotProps) {
return binds;
}
+function createFieldSlotScope(slotProps: RuntimeFieldSlotProps) {
+ return {
+ ...createFieldSlotProps(slotProps),
+ componentProps: createComponentProps(slotProps),
+ disabled: shouldDisabled.value,
+ isInValid: isInValid.value,
+ modelValue: fieldValue.value,
+ name: fieldName,
+ };
+}
+
function autofocus() {
if (
fieldComponentRef.value &&
@@ -484,14 +496,7 @@ onUnmounted(() => {
:class="cn('relative flex w-full items-center', wrapperClass)"
>
-
+
{
shouldApplyInvalidStyle,
}"
v-bind="createComponentProps(slotProps)"
- :disabled="shouldDisabled"
>
componentProps(baseContext);
}
+function wrapCommonConfig(
+ commonConfig: FormCommonConfig | undefined,
+ baseContext: FormSchemaContext,
+) {
+ if (!commonConfig || !isFunction(commonConfig.componentProps)) {
+ return commonConfig;
+ }
+
+ return {
+ ...commonConfig,
+ componentProps: wrapComponentProps(
+ commonConfig.componentProps,
+ baseContext,
+ ),
+ };
+}
+
function wrapCustomParamsRender(
render: AnyFormSchema['help'],
baseContext: FormSchemaContext,
@@ -350,6 +367,9 @@ export function createFormFieldSchema(
const normalizedSchema = isFormArraySchema(schema)
? createArrayFieldSchema(schema, options)
: schema;
+ const commonComponentProps = isFunction(componentProps)
+ ? componentProps({ fieldName: normalizedSchema.fieldName })
+ : componentProps;
let resolvedSchemaFormItemClass = normalizedSchema.formItemClass;
if (isFunction(normalizedSchema.formItemClass)) {
@@ -370,7 +390,7 @@ export function createFormFieldSchema(
modelPropName,
wrapperClass,
...normalizedSchema,
- commonComponentProps: componentProps as MaybeComponentProps,
+ commonComponentProps,
componentProps: normalizedSchema.componentProps,
controlClass: [controlClass, normalizedSchema.controlClass]
.filter(Boolean)
@@ -423,10 +443,13 @@ export function createArrayChildSchema(
),
},
{
- commonConfig: options.commonConfig,
+ commonConfig: wrapCommonConfig(options.commonConfig, baseContext),
disabled: options.disabled || schema.disabled,
forceHideLabel: true,
- globalCommonConfig: options.globalCommonConfig,
+ globalCommonConfig: wrapCommonConfig(
+ options.globalCommonConfig,
+ baseContext,
+ ),
},
);
}
diff --git a/packages/@core/ui-kit/form-ui/src/index.ts b/packages/@core/ui-kit/form-ui/src/index.ts
index 40f7da65..db7b7e9d 100644
--- a/packages/@core/ui-kit/form-ui/src/index.ts
+++ b/packages/@core/ui-kit/form-ui/src/index.ts
@@ -19,6 +19,7 @@ export type {
VbenFormFieldArrayProps,
VbenFormFieldSlotProps,
VbenFormProps,
+ VbenFormResolvedComponentProps,
FormSchema as VbenFormSchema,
VbenFormSlots,
} from './types';
diff --git a/packages/@core/ui-kit/form-ui/src/types.ts b/packages/@core/ui-kit/form-ui/src/types.ts
index 93f64475..6272435b 100644
--- a/packages/@core/ui-kit/form-ui/src/types.ts
+++ b/packages/@core/ui-kit/form-ui/src/types.ts
@@ -244,6 +244,10 @@ export interface VbenFormFieldSlotProps<
FormFieldValue,
TFieldName
>;
+ componentProps: VbenFormResolvedComponentProps<
+ FormFieldValue,
+ TFieldName
+ >;
disabled: boolean;
field: FormRuntimeField>;
isInValid: boolean;
@@ -251,6 +255,16 @@ export interface VbenFormFieldSlotProps<
name: TFieldName;
}
+export type VbenFormResolvedComponentProps<
+ TValue = unknown,
+ TFieldName extends string = string,
+> = MaybeComponentProps & {
+ disabled: boolean;
+ modelValue?: TValue;
+ name: TFieldName;
+ 'onUpdate:modelValue'?: (value: TValue) => void;
+};
+
type VbenFormFieldSlots<
TValues extends FormValues,
T extends BaseFormComponentType,
diff --git a/playground/src/views/demos/badge/index.vue b/playground/src/views/demos/badge/index.vue
index a067a62f..30bd78af 100644
--- a/playground/src/views/demos/badge/index.vue
+++ b/playground/src/views/demos/badge/index.vue
@@ -91,7 +91,7 @@ function updateMenuBadge() {