diff --git a/.changeset/bright-zebras-default.md b/.changeset/bright-zebras-default.md new file mode 100644 index 000000000..1131e24cd --- /dev/null +++ b/.changeset/bright-zebras-default.md @@ -0,0 +1,5 @@ +--- +'@vben-core/form-ui': patch +--- + +fix(@vben-core/form-ui): initialize native Zod string formats with empty values 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 55920e081..34f7658d1 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 @@ -78,6 +78,63 @@ afterEach(() => { }); describe('useVbenForm integration', () => { + it('provides empty defaults for native zod string formats', async () => { + const [Form, formApi] = useVbenForm({ + schema: [ + { + component: TestInput, + fieldName: 'email', + rules: z.email(), + }, + { + component: TestInput, + fieldName: 'address', + rules: z.ipv4(), + }, + ], + showDefaultActions: false, + }); + const wrapper = mount(Form); + wrappers.push(wrapper); + await flushPromises(); + + expect(await formApi.getValues()).toEqual({ + address: '', + email: '', + }); + }); + + it('filters native zod formats from nested default extraction', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const [Form, formApi] = useVbenForm({ + schema: [ + { + component: TestInput, + fieldName: 'profile', + rules: z.object({ + email: z.email(), + address: z.object({ + ip: z.ipv4(), + backup: z.email().default('backup@example.com'), + }), + }), + }, + ], + showDefaultActions: false, + }); + const wrapper = mount(Form); + wrappers.push(wrapper); + await flushPromises(); + + expect(warn).not.toHaveBeenCalled(); + expect(await formApi.getValues()).toEqual({ + profile: { + address: { backup: 'backup@example.com', ip: '' }, + email: '', + }, + }); + }); + it('uses model updates as the primary channel and preserves empty strings', async () => { const validateValue = vi.fn(); const [Form, formApi] = useVbenForm({ diff --git a/packages/@core/ui-kit/form-ui/src/use-form-context.ts b/packages/@core/ui-kit/form-ui/src/use-form-context.ts index ae3d69f90..a52c54a72 100644 --- a/packages/@core/ui-kit/form-ui/src/use-form-context.ts +++ b/packages/@core/ui-kit/form-ui/src/use-form-context.ts @@ -9,7 +9,15 @@ import { computed, toRaw, unref, useSlots } from 'vue'; import { createContext } from '@vben-core/shadcn-ui'; import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils'; -import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod'; +import { + object, + ZodIntersection, + ZodNumber, + ZodObject, + ZodString, + string as zodString, + ZodStringFormat, +} from 'zod'; import { getDefaultsForSchema } from 'zod-defaults'; import { useFormRuntime } from './form-runtime'; @@ -26,6 +34,46 @@ export const [injectFormProps, provideFormProps] = export const [injectComponentRefMap, provideComponentRefMap] = createContext>('ComponentRefMap'); +/** + * Rebuild the schema used by zod-defaults, replacing native format nodes that + * zod-defaults does not recognise while retaining supported wrappers. + */ +function normalizeSchemaForDefaults(rule: ZodType): ZodType { + const rawRule = toRaw(rule) as any; + + if (rawRule instanceof ZodStringFormat) { + return zodString(); + } + + if (rawRule instanceof ZodObject) { + const shape = Object.fromEntries( + Object.entries(rawRule.shape).map(([key, value]) => [ + key, + normalizeSchemaForDefaults(value as ZodType), + ]), + ); + return object(shape); + } + + if (rawRule instanceof ZodIntersection) { + const { left, right } = (rawRule as any).def; + return normalizeSchemaForDefaults(left as ZodType).and( + normalizeSchemaForDefaults(right as ZodType), + ); + } + + if (rawRule.constructor.name === 'ZodDefault') { + const inner = normalizeSchemaForDefaults(rawRule.unwrap()); + return inner.default(rawRule.def.defaultValue); + } + + if (rawRule.constructor.name === 'ZodPipe') { + return normalizeSchemaForDefaults(rawRule.in).pipe(rawRule.out); + } + + return rawRule; +} + export function useFormInitial( props: ComputedRef | VbenFormProps, ) { @@ -56,7 +104,7 @@ export function useFormInitial( // 检查规则是否适合提取默认值 const rawRules = toRaw(item.rules); const customDefaultValue = getCustomDefaultValue(rawRules); - zodObject[item.fieldName] = rawRules; + zodObject[item.fieldName] = normalizeSchemaForDefaults(rawRules); if (customDefaultValue !== undefined) { initialValues[item.fieldName] = customDefaultValue; } @@ -74,8 +122,8 @@ export function useFormInitial( // 自定义默认值提取逻辑 function getCustomDefaultValue(rule: any): any { rule = toRaw(rule); - if (rule instanceof ZodString) { - return ''; // 默认为空字符串 + if (rule instanceof ZodString || rule instanceof ZodStringFormat) { + return ''; // 字符串及其格式校验默认为空字符串 } else if (rule instanceof ZodNumber) { return null; // 默认为 null(避免显示 0) } else if (rule instanceof ZodObject) { @@ -86,7 +134,7 @@ export function useFormInitial( } return defaultValues; } else if (rule instanceof ZodIntersection) { - return getDefaultsForSchema(rule); + return getDefaultsForSchema(normalizeSchemaForDefaults(rule) as any); } else { return undefined; // 其他类型不提供默认值 }