Reactive & Conditional

View source
Dynamically show or hide fields based on form state to create smart reactive forms.

Overview

All meta fields in AutoForm can accept reactive functions (ctx) => value, where ctx is the AutoFormFieldContext (containing state, path, value). The function re-evaluates automatically as the form state changes, enabling conditional rendering and field dependencies.

There are two ways to conditionally render, with different behaviors:
  • if - Display condition; when false the field is not created (not mounted, not included in validation).
  • hidden - Whether to hide; when true the field remains in the DOM and form data, only visually hidden.

Examples

Conditional Rendering

if and hidden accept functions returning a boolean: when switching account type, business fields (if) are mounted/unmounted on demand; after checking the terms, the subscription field (hidden) switches from hidden to visible.

Use if when you want to completely exclude a field (no validation, not submitted); use hidden when you want to temporarily hide it but retain its value.

切换后观察 company/tax 字段的挂载与卸载

勾选后下方订阅字段显示

<script lang="ts" setup>
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'
import type { AutoFormFieldContext } from '@movk/nuxt'

const { afz } = useAutoForm()
const toast = useToast()

const schema = afz.object({
  accountType: afz
    .enum(['personal', 'business'])
    .default('personal')
    .meta({ label: '账户类型', description: '切换后观察 company/tax 字段的挂载与卸载' }),

  username: afz
    .string({ controlProps: { placeholder: '请输入用户名' } })
    .min(3, '用户名至少需要 3 个字符')
    .meta({ label: '用户名' }),

  companyName: afz
    .string({ controlProps: { placeholder: '请输入公司名称' } })
    .meta({
      label: '公司名称(if)',
      hint: 'if:仅企业账户挂载,个人账户下字段不创建、不参与校验',
      if: ({ state }: AutoFormFieldContext) => state?.accountType === 'business'
    }),

  taxId: afz
    .string({ controlProps: { placeholder: '请输入税号' } })
    .meta({
      label: '税号(if)',
      hint: 'if:与公司名称同条件挂载',
      if: ({ state }: AutoFormFieldContext) => state?.accountType === 'business'
    }),

  agreeToTerms: afz
    .boolean()
    .default(false)
    .meta({ label: '同意服务条款', description: '勾选后下方订阅字段显示' }),

  newsletter: afz
    .boolean()
    .default(true)
    .meta({
      label: '订阅邮件通知(hidden)',
      description: 'hidden:未同意条款时隐藏,但字段始终保留在 DOM 与表单数据中',
      hidden: ({ state }: AutoFormFieldContext) => !state?.agreeToTerms
    })
})

type Schema = z.output<typeof schema>

const form = ref<Partial<Schema>>({})

async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({
    title: '提交成功',
    color: 'success',
    description: JSON.stringify(event.data, null, 2)
  })
}
</script>

<template>
  <MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>

Dependent Fields

Functional controlProps re-evaluates as other fields change. In the example below, city options depend on the country field, and a watch clears the stale value when the country changes.

Field dependencies only re-evaluate control attributes and will not automatically clear old values. Use watch to manually reset stale options when needed.
选项随国家变化,切换国家后会清空已选城市
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const countryOptions = ['CN', 'US', 'JP'] as const
type CountryCode = typeof countryOptions[number]

const cityItemsByCountry: Record<CountryCode, string[]> = {
  CN: ['北京', '上海', '广州'],
  US: ['New York', 'San Francisco', 'Seattle'],
  JP: ['Tokyo', 'Osaka', 'Kyoto']
}

const schema = afz.object({
  country: afz.enum(countryOptions).default('CN').meta({ label: '国家' }),
  city: afz.enum([], {
    type: 'enum',
    controlProps: ({ state }) => ({
      items: cityItemsByCountry[(state.country as CountryCode | undefined) ?? 'CN']
    })
  }).meta({ label: '城市', hint: '选项随国家变化,切换国家后会清空已选城市' })
})

const state = reactive<Partial<z.output<typeof schema>>>({})

watch(() => state.country, () => {
  state.city = undefined
})
</script>

<template>
  <MAutoForm :schema="schema" :state="state" />
</template>
When items comes from async or external reactive data (e.g. a computed / ref based on useApiFetch or useAsyncData), always use functionalcontrolProps: () => ({ items: source.value }), or pass the reactive source directly as items: source — never write items: source.value.The schema is only constructed once during setup, so source.value is a one-time snapshot at that moment. This may appear to work during SSR hydration when data is ready, but will freeze to an empty array when the component re-mounts during client-side navigation before async data arrives, and subsequent option updates will no longer take effect. Functional controlProps evaluates at render time and avoids this problem.

API

meta

NameTypeDescription
ifReactiveValue<boolean, AutoFormFieldContext>Display condition; field is not created when false
hiddenReactiveValue<boolean, AutoFormFieldContext>Whether to hide; retains DOM but visually hidden when true
In addition to the above fields, label, hint, description, controlProps, class and other meta fields also support the (ctx) => value function form, dynamically evaluated based on form state.
Copyright © 2025 - 2026 YiXuan - MIT License