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="you@example.com" />
</CfFormField>
<CfButton>提交</CfButton>
</CfForm>
</template> <script setup>
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="you@example.com" />
</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 stylehorizontal— label and control on the same row, paired withlabelWidthfor alignmentinline— all fields packed on one line, useful for search bars / toolbars
<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> <script setup>
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="you@example.com" />
</CfFormField>
<div style="display: flex; gap: 8px;">
<CfButton @click="submit">提交</CfButton>
</div>
</CfForm>
</template> <script setup>
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="you@example.com" />
</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 rulesvalidator: async (value, model) => string | void— any custom checkvalidateOn="submit" | "change" | "blur"— when validation runs- The required asterisk is auto-derived from the
requiredrule; no need to also passrequired
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="you@example.com" />
</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> <script setup>
import { reactive, ref } from 'vue';
import {
CfForm,
CfFormField,
CfInput,
CfButton,
CfSwitch,
toast,
} from '@chufix-design/vue';
const model = reactive<SignupModel>({
name: '',
email: '',
password: '',
confirm: '',
agree,
});
const rules= {
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).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="you@example.com" />
</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> <script setup>
import { reactive, ref } from 'vue';
import {
CfForm,
CfFormField,
CfInput,
CfButton,
toast,
} from '@chufix-design/vue';
const model = reactive({ username: '' });
const TAKEN = new Set(['admin', 'root', 'chufix']);
const rules= {
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:
| Method | Description |
|---|---|
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> <script setup>
import { reactive, ref } from 'vue';
import {
CfForm,
CfFormField,
CfInput,
CfButton,
toast,
} from '@chufix-design/vue';
const model = reactive({ project: '', desc: '' });
const rules= {
project: [{ required: true, min: 2 }],
desc: [{ max: 200 }],
};
const formRef = ref<{
validate: () => Promise<{ valid: boolean }>;
validateField: (n) => 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="you@example.com" />
</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> <script setup>
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="you@example.com" />
</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
| Prop | Type | Default | Description |
|---|---|---|---|
layout | 'vertical' | 'horizontal' | 'inline' | 'vertical' | Overall layout |
size | 'sm' | 'md' | 'lg' | 'md' | Default size |
labelWidth | number | string | — | Only effective in horizontal layout |
disabled | boolean | false | Global disabled flag |
model | Record<string, unknown> | — | Reactive value map; required for rule validation |
rules | Record<string, FieldRule[]> | — | Field name → rule array |
validateOn | 'submit' | 'change' | 'blur' | 'submit' | When to run rule validation |
scrollToError | boolean | true | Scroll / focus first error on failed submit |
Events: @submit({ valid, values, errors }) / @validate({ valid, errors }) / @reset.
API · FormField Props
| Prop | Type | Description |
|---|---|---|
name | string | Field name; required for rule-based validation |
label | string | ReactNode | Label text |
required | boolean | Force-show required asterisk; otherwise inferred from rules |
hint | string | ReactNode | Helper text below the control |
error | string | ReactNode | Explicit error; takes precedence over rule errors |
for (Vue) / htmlFor (React) | string | Custom input id; auto-generated if omitted |
layout | FormLayout | Override parent Form layout for this field |
FieldRule fields
| Field | Meaning |
|---|---|
required | Disallow undefined / null / ” / empty array |
min / max | Length bounds for strings/arrays; numeric bounds for numbers |
pattern | Regex match (string only) |
type | Built-in type check: 'string' | 'number' | 'email' | 'url' | 'array' |
validator(value, model) | Custom; return error string or void. May be async |
message | Override default error message |
反馈与讨论
Form · Discussion