表单字段
表单字段提供的是字符串,而不是数字或日期。因此 Schema 取决于 v-model 实际写入的值。
每种字段提供什么
| 字段 | v-model 提供的值 | Schema |
|---|---|---|
<input>、<textarea> | '文本' 或 '' | string() |
输入数字的 <input> | '42' 或 '' | coerce.number() |
<input v-model.number> | 42 或 '' | number() |
<input type="date"> | '2026-10-02' 或 '' | coerce.date() |
<input type="checkbox"> | true / false | boolean(),同意条款用 literal(true) |
<input type="radio">、<select> | 选中项的值 | oneOf([...]) |
<select multiple>、复选框组 | 值的数组 | array(oneOf([...])) |
未填写的字段提供 '',而 tinyval 将空字符串视为未填写 —— 见 空值。因此必填字段和 optional 无需额外配置即可用于输入框。
示例:资料表单
vue
<script setup lang="ts">
import { object, string, optional, pipe, coerce, integer, min, minLength, oneOf, array, literal, check } from 'tinyval';
import { useForm } from 'tinyval-vue';
const Profile = object({
name: string(),
age: optional(pipe(coerce.number(), integer(), min(14))),
birthday: pipe(coerce.date(), check((d) => d < new Date())),
city: oneOf(['moscow', 'spb', 'other']),
interests: pipe(array(oneOf(['js', 'php', 'go'])), minLength(1)),
terms: literal(true),
});
const form = useForm(Profile, {
initial: { name: '', age: '', birthday: '', city: '', interests: [] as string[], terms: false },
});
</script>
<template>
<input v-model="form.values.name" @blur="form.touch('name')" />
<input v-model="form.values.age" inputmode="numeric" @blur="form.touch('age')" />
<input v-model="form.values.birthday" type="date" @blur="form.touch('birthday')" />
<select v-model="form.values.city" @blur="form.touch('city')">
<option value="" disabled>—</option>
<option value="moscow">Moscow</option>
<option value="spb">Saint Petersburg</option>
<option value="other">Other</option>
</select>
<label v-for="i in ['js', 'php', 'go']" :key="i">
<input v-model="form.values.interests" type="checkbox" :value="i" @change="form.touch('interests')" /> {{ i }}
</label>
<label>
<input v-model="form.values.terms" type="checkbox" @change="form.touch('terms')" /> I accept the terms
</label>
</template>handleSubmit 收到的是已转换的值:age 是数字,birthday 是 Date。
嵌套字段和数组
错误路径与 values 中的路径一致。列表请使用 useFieldArray —— 它为 v-for 提供稳定的键,删除时不会弄混各行的错误:
vue
<script setup lang="ts">
const items = useFieldArray<{ title: string }>(form, 'items');
</script>
<template>
<div v-for="field in items.fields.value" :key="field.key">
<input v-model="field.value.title" :name="`${field.path}.title`" @blur="form.touch(`${field.path}.title`)" />
<span>{{ form.errors[`${field.path}.title`] }}</span>
<button type="button" @click="items.remove(field.index)">×</button>
</div>
<button type="button" @click="items.append({ title: '' })">+</button>
</template>掩码
电话、银行卡、金额和日期使用掩码输入更方便 —— 请使用 tinyval-mask 包。v-mask 指令可用于 <input>、<TvField> 以及 UI 组件库的字段;可以在 Schema 中先去掉掩码再校验 —— 见 tinyval-mask 中的 Vue。
vue
<!-- the form gets 79991234567, the field shows +7 (999) 123-45-67 -->
<TvField name="phone" v-mask.raw="{ mask: phoneMask, placeholder: '_' }" inputmode="tel" />文件
tinyval 目前还没有专门的文件 Schema。可以通过 createSchema 或 check 校验文件。<input type="file"> 不支持 v-model,因此需要手动把值放入表单:
vue
<script setup lang="ts">
const avatar = pipe(
unknown(),
check((f) => f instanceof File, 'Choose a file'),
check((f) => (f as File).size <= 2 * 1024 * 1024, 'Max 2 MB'),
);
</script>
<template>
<input type="file" accept="image/*" @change="form.values.avatar = ($event.target as HTMLInputElement).files?.[0] ?? null" />
</template>