Preview Updated 2026-05-10

Form

Form wrapper — three layouts, rule-based validation, async validators, imperative validate / reset / submit, auto-scroll to first error.

Basic usage

<CfForm> provides layout context. <CfFormField> wraps each field and renders the label, required asterisk, hint, and error message uniformly. The minimal form is just a layout wrapper.

背景

用于登录

<script setup lang="ts">
import { ref } from 'vue';
import { CfForm, CfFormField, CfInput, CfButton } from '@chufix-design/vue';

const name = ref('');
const email = ref('');
</script>

<template>
  <CfForm layout="vertical">
    <CfFormField label="姓名">
      <CfInput v-model="name" placeholder="张三" />
    </CfFormField>
    <CfFormField label="邮箱" hint="用于登录">
      <CfInput v-model="email" type="email" placeholder="[email protected]" />
    </CfFormField>
    <CfButton>提交</CfButton>
  </CfForm>
</template>

Three layouts

layout controls how the label and control are arranged:

  • vertical (default) — label above the control, the most common form style
  • horizontal — label and control on the same row, paired with labelWidth for alignment
  • inline — all fields packed on one line, useful for search bars / toolbars
背景
layout = vertical(默认)
layout = horizontal
layout = inline
<script setup lang="ts">
import { ref } from 'vue';
import { CfForm, CfFormField, CfInput, CfButton } from '@chufix-design/vue';

const a = ref('');
const b = ref('');
const c = ref('');
const d = ref('');
const e = ref('');
const f = ref('');
</script>

<template>
  <div class="demo-stack">
    <div>
      <div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">layout = vertical(默认)</div>
      <CfForm layout="vertical">
        <CfFormField label="姓名"><CfInput v-model="a" /></CfFormField>
        <CfFormField label="邮箱"><CfInput v-model="b" /></CfFormField>
      </CfForm>
    </div>
    <div>
      <div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">layout = horizontal</div>
      <CfForm layout="horizontal" :label-width="80">
        <CfFormField label="姓名"><CfInput v-model="c" /></CfFormField>
        <CfFormField label="邮箱"><CfInput v-model="d" /></CfFormField>
      </CfForm>
    </div>
    <div>
      <div style="font-size: 12px; color: var(--fg-3); margin-bottom: 4px;">layout = inline</div>
      <CfForm layout="inline">
        <CfFormField label="姓名"><CfInput v-model="e" /></CfFormField>
        <CfFormField label="邮箱"><CfInput v-model="f" /></CfFormField>
        <CfButton>搜索</CfButton>
      </CfForm>
    </div>
  </div>
</template>

Manual error mode

The most direct usage: the parent component writes its own validation logic and pushes error messages into each field’s error prop. This mode bypasses Form’s built-in validator and works well if you already use libraries like zod / valibot / yup.

背景

用于登录与接收通知

<script setup lang="ts">
import { ref } from 'vue';
import { CfForm, CfFormField, CfInput, CfButton } from '@chufix-design/vue';

const name = ref('');
const email = ref('');
const errors = ref<Record<string, string>>({});

function submit() {
  errors.value = {};
  if (!name.value.trim()) errors.value.name = '姓名不能为空';
  if (!email.value.includes('@')) errors.value.email = '邮箱格式不正确';
}
</script>

<template>
  <CfForm layout="vertical">
    <CfFormField label="姓名" required :error="errors.name">
      <CfInput v-model="name" placeholder="张三" />
    </CfFormField>
    <CfFormField label="邮箱" required hint="用于登录与接收通知" :error="errors.email">
      <CfInput v-model="email" type="email" placeholder="[email protected]" />
    </CfFormField>
    <div style="display: flex; gap: 8px;">
      <CfButton @click="submit">提交</CfButton>
    </div>
  </CfForm>
</template>

Rule-based validation

Pass model + rules + name and the built-in validator takes over:

  • required / min / max / pattern / type: 'email' | 'url' | 'string' | 'number' | 'array' — built-in rules
  • validator: async (value, model) => string | void — any custom check
  • validateOn="submit" | "change" | "blur" — when validation runs
  • The required asterisk is auto-derived from the required rule; no need to also pass required
const rules: Record<string, FieldRules> = {
  email: [{ required: true, type: 'email' }],
  password: [{ required: true, min: 8, message: 'At least 8 characters' }],
  confirm: [
    { required: true },
    { validator: (v, m) => (v !== (m as any).password ? "Doesn't match" : undefined) },
  ],
};
背景
<script setup lang="ts">
import { reactive, ref } from 'vue';
import {
  CfForm,
  CfFormField,
  CfInput,
  CfButton,
  CfSwitch,
  toast,
  type FieldRules,
} from '@chufix-design/vue';

interface SignupModel {
  name: string;
  email: string;
  password: string;
  confirm: string;
  agree: boolean;
}

const model = reactive<SignupModel>({
  name: '',
  email: '',
  password: '',
  confirm: '',
  agree: false,
});

const rules: Record<string, FieldRules> = {
  name: [{ required: true, min: 2, max: 24, message: '姓名 2~24 个字符' }],
  email: [{ required: true, type: 'email' }],
  password: [{ required: true, min: 8, message: '密码至少 8 位' }],
  confirm: [
    { required: true },
    {
      validator: (v, m) => (v !== (m as SignupModel).password ? '两次输入的密码不一致' : undefined),
    },
  ],
  agree: [{ validator: (v) => (v === true ? undefined : '请阅读并同意条款') }],
};

const formRef = ref<{ validate: () => Promise<{ valid: boolean }> ; resetFields: () => void } | null>(null);

async function onSubmit({ valid }: { valid: boolean }) {
  if (valid) toast({ type: 'success', message: '注册成功' });
}
function reset() {
  formRef.value?.resetFields();
}
</script>

<template>
  <CfForm
    ref="formRef"
    layout="vertical"
    :model="model"
    :rules="rules"
    validate-on="blur"
    @submit="onSubmit"
  >
    <CfFormField label="姓名" name="name">
      <CfInput v-model="model.name" placeholder="2~24 字符" />
    </CfFormField>
    <CfFormField label="邮箱" name="email" hint="用于登录与接收通知">
      <CfInput v-model="model.email" placeholder="[email protected]" />
    </CfFormField>
    <CfFormField label="密码" name="password">
      <CfInput v-model="model.password" type="password" />
    </CfFormField>
    <CfFormField label="确认密码" name="confirm">
      <CfInput v-model="model.confirm" type="password" />
    </CfFormField>
    <CfFormField name="agree" :label="undefined">
      <label style="display: inline-flex; gap: 8px; align-items: center; font-size: 13px;">
        <CfSwitch v-model="model.agree" />
        我已阅读并同意《用户协议》
      </label>
    </CfFormField>
    <div style="display: flex; gap: 8px;">
      <CfButton type="submit">注册</CfButton>
      <CfButton variant="tertiary" @click="reset">重置</CfButton>
    </div>
  </CfForm>
</template>

Async validator

validator can return a Promise — typical use case: “is this username taken?” type backend checks.

背景
<script setup lang="ts">
import { reactive, ref } from 'vue';
import {
  CfForm,
  CfFormField,
  CfInput,
  CfButton,
  toast,
  type FieldRules,
} from '@chufix-design/vue';

const model = reactive({ username: '' });

const TAKEN = new Set(['admin', 'root', 'chufix']);

const rules: Record<string, FieldRules> = {
  username: [
    { required: true, min: 3, max: 16 },
    { pattern: /^[a-z][a-z0-9_]*$/, message: '只能小写字母 / 数字 / 下划线,且需小写字母开头' },
    {
      validator: (v) =>
        new Promise<string | void>((resolve) => {
          setTimeout(() => {
            resolve(TAKEN.has(String(v)) ? '该用户名已被占用' : undefined);
          }, 700);
        }),
    },
  ],
};

const formRef = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null);
const checking = ref(false);

async function onSubmit({ valid }: { valid: boolean }) {
  if (valid) toast({ type: 'success', message: '用户名可用' });
}

async function check() {
  checking.value = true;
  await formRef.value?.validate();
  checking.value = false;
}
</script>

<template>
  <CfForm
    ref="formRef"
    layout="vertical"
    :model="model"
    :rules="rules"
    validate-on="blur"
    @submit="onSubmit"
  >
    <CfFormField
      label="用户名"
      name="username"
      hint="`admin` / `root` / `chufix` 已被占用,可触发异步验证"
    >
      <CfInput v-model="model.username" placeholder="3~16 字符" />
    </CfFormField>
    <div style="display: flex; gap: 8px;">
      <CfButton type="submit" :loading="checking">提交</CfButton>
      <CfButton variant="tertiary" @click="check">手动校验</CfButton>
    </div>
  </CfForm>
</template>

Imperative methods

Grab the Form instance via ref to call:

MethodDescription
submit()Run validate, then fire @submit
validate()Run validate only, returns { valid, errors }
validateField(name)Validate a single field
clearValidate(name?)Clear error messages (does not touch data)
resetFields()Restore model to initial values and clear errors

On submit failure the component scrolls to and focuses the first invalid field. Disable via :scroll-to-error="false".

背景
<script setup lang="ts">
import { reactive, ref } from 'vue';
import {
  CfForm,
  CfFormField,
  CfInput,
  CfButton,
  toast,
  type FieldRules,
} from '@chufix-design/vue';

const model = reactive({ project: '', desc: '' });

const rules: Record<string, FieldRules> = {
  project: [{ required: true, min: 2 }],
  desc: [{ max: 200 }],
};

const formRef = ref<{
  validate: () => Promise<{ valid: boolean }>;
  validateField: (n: string) => Promise<unknown>;
  clearValidate: (n?: string) => void;
  resetFields: () => void;
  submit: () => Promise<void>;
} | null>(null);

async function onlyProject() {
  await formRef.value?.validateField('project');
}
function clear() {
  formRef.value?.clearValidate();
  toast({ type: 'info', message: '已清空错误信息(数据不变)' });
}
function reset() {
  formRef.value?.resetFields();
  toast({ type: 'info', message: '已重置到初始值' });
}
</script>

<template>
  <CfForm
    ref="formRef"
    layout="vertical"
    :model="model"
    :rules="rules"
    @submit="(p: { valid: boolean }) => p.valid && toast({ type: 'success', message: '提交成功' })"
  >
    <CfFormField label="项目名" name="project">
      <CfInput v-model="model.project" />
    </CfFormField>
    <CfFormField label="描述" name="desc" hint="最多 200 字">
      <CfInput v-model="model.desc" />
    </CfFormField>
    <div style="display: flex; flex-wrap: wrap; gap: 8px;">
      <CfButton type="submit">submit()</CfButton>
      <CfButton variant="tertiary" @click="onlyProject">validateField('project')</CfButton>
      <CfButton variant="tertiary" @click="clear">clearValidate()</CfButton>
      <CfButton variant="tertiary" @click="reset">resetFields()</CfButton>
    </div>
  </CfForm>
</template>

Complex form

Real-world template combining Input / Select / Textarea / Button.

背景

用于登录与接收通知

<script setup lang="ts">
import { ref } from 'vue';
import {
  CfForm,
  CfFormField,
  CfInput,
  CfTextarea,
  CfSelect,
  CfButton,
} from '@chufix-design/vue';

const name = ref('');
const email = ref('');
const role = ref('user');
const bio = ref('');

const roles = [
  { label: '普通用户', value: 'user' },
  { label: '管理员', value: 'admin' },
  { label: '只读', value: 'viewer' },
];
</script>

<template>
  <CfForm layout="vertical">
    <CfFormField label="姓名" required>
      <CfInput v-model="name" placeholder="张三" />
    </CfFormField>
    <CfFormField label="邮箱" required hint="用于登录与接收通知">
      <CfInput v-model="email" type="email" placeholder="[email protected]" />
    </CfFormField>
    <CfFormField label="角色">
      <CfSelect v-model="role" :options="roles" />
    </CfFormField>
    <CfFormField label="简介">
      <CfTextarea v-model="bio" :rows="3" placeholder="一句话介绍自己" />
    </CfFormField>
    <div style="display: flex; gap: 8px;">
      <CfButton>提交</CfButton>
      <CfButton variant="ghost" type="reset">重置</CfButton>
    </div>
  </CfForm>
</template>

API · Form Props

PropTypeDefaultDescription
layout'vertical' | 'horizontal' | 'inline''vertical'Overall layout
size'sm' | 'md' | 'lg''md'Default size
labelWidthnumber | stringOnly effective in horizontal layout
disabledbooleanfalseGlobal disabled flag
modelRecord<string, unknown>Reactive value map; required for rule validation
rulesRecord<string, FieldRule[]>Field name → rule array
validateOn'submit' | 'change' | 'blur''submit'When to run rule validation
scrollToErrorbooleantrueScroll / focus first error on failed submit

Events: @submit({ valid, values, errors }) / @validate({ valid, errors }) / @reset.

API · FormField Props

PropTypeDescription
namestringField name; required for rule-based validation
labelstring | ReactNodeLabel text
requiredbooleanForce-show required asterisk; otherwise inferred from rules
hintstring | ReactNodeHelper text below the control
errorstring | ReactNodeExplicit error; takes precedence over rule errors
for (Vue) / htmlFor (React)stringCustom input id; auto-generated if omitted
layoutFormLayoutOverride parent Form layout for this field

FieldRule fields

FieldMeaning
requiredDisallow undefined / null / ” / empty array
min / maxLength bounds for strings/arrays; numeric bounds for numbers
patternRegex match (string only)
typeBuilt-in type check: 'string' | 'number' | 'email' | 'url' | 'array'
validator(value, model)Custom; return error string or void. May be async
messageOverride default error message

反馈与讨论

Form · Discussion

0
0 / 600
一键发送
正在加载评论...