快速开始
在下面的例子中,你可以点击
文件名
来查看代码,点击运行结果
来使用代码的结果。
安装
npm install vfm
yarn add vfm
创建表单结构
详情:
import { createForm, NestedValue } from 'vfm';
// define form sturcture
// 定义 form 的结构
export const getForm = () => createForm<
// 表单结构
{
// base fields
// 基础字段
username: string;
password: string;
passwordConfirm: string;
// nested fields
// 嵌套字段
baseInfo: {
birthDay: string;
age: string;
},
// array fields
// 数组字段
tags: string[],
// nested array fields
// 嵌套的数组字段
address: {
phone: string;
detail: string;
}[],
// object value
// object类型的值
// the NestedValue<xxx> will be treat as the `value` type of field `schools`, not nested fields
// NestedValue<xxx> 被当做字段`schools`的值,而不是嵌套的字段
schools: NestedValue<{
name: string;
address: string;
}[]>;
},
// 虚拟字段
'tags'
>({
// required, form init values
// 必须,表达初始值
initValues: {
username: '',
password: '',
passwordConfirm: '',
baseInfo: {
birthDay: '',
age: ''
},
tags: [],
address: [],
schools: []
},
// form default values, used for reset fields, and determine whether fields are dirty. If not pass, same with `initValues`.
// 表达默认值, 用来重置字段,和确定字段是否是dirty的。如果不传,默认和`initValues`相同。
// defaultValues:
// when to set touched status, 'BLUR' or 'FOCUS', default is 'BLUR'.
// 什么时候标志一个字段为`touched`,可选值为'BLUR' 和 'FOCUS',默认是'BLUR'。
touchType: 'BLUR',
// if true, the form.state is readonly, avoid to manual edit it.
// 如果为true,form.state 将会是只读,避免手动地编辑它
readonly: process.env.NODE_ENV === 'development'
});
注册字段
详情:
User Name:
Password:
Password Confirm:
Base Info
Birth Day:
Age:
Tags:
+
Virtual Field (Tags):
Address
+ Add Address
import { createForm, NestedValue } from 'vfm';
// define form sturcture
// 定义 form 的结构
export const getForm = () => createForm<
// 表单结构
{
// base fields
// 基础字段
username: string;
password: string;
passwordConfirm: string;
// nested fields
// 嵌套字段
baseInfo: {
birthDay: string;
age: string;
},
// array fields
// 数组字段
tags: string[],
// nested array fields
// 嵌套的数组字段
address: {
phone: string;
detail: string;
}[],
// object value
// object类型的值
// the NestedValue<xxx> will be treat as the `value` type of field `schools`, not nested fields
// NestedValue<xxx> 被当做字段`schools`的值,而不是嵌套的字段
schools: NestedValue<{
name: string;
address: string;
}[]>;
},
// 虚拟字段
'tags'
>({
// required, form init values
// 必须,表达初始值
initValues: {
username: '',
password: '',
passwordConfirm: '',
baseInfo: {
birthDay: '',
age: ''
},
tags: [],
address: [],
schools: []
},
// form default values, used for reset fields, and determine whether fields are dirty. If not pass, same with `initValues`.
// 表达默认值, 用来重置字段,和确定字段是否是dirty的。如果不传,默认和`initValues`相同。
// defaultValues:
// when to set touched status, 'BLUR' or 'FOCUS', default is 'BLUR'.
// 什么时候标志一个字段为`touched`,可选值为'BLUR' 和 'FOCUS',默认是'BLUR'。
touchType: 'BLUR',
// if true, the form.state is readonly, avoid to manual edit it.
// 如果为true,form.state 将会是只读,避免手动地编辑它
readonly: process.env.NODE_ENV === 'development'
});
<script setup lang="ts">
import { Field, VirtualField, FieldArray, FormProvider } from 'vfm';
import { getForm } from './form';
import BaseInfo from './partial/BaseInfo.vue';
import AddressList from './partial/AddressList.vue';
import SchoolList from './partial/SchoolList.vue';
const form = getForm();
const formState = form.state;
const submit = () => {
form.submit({
onSuccess: (data) => {
alert(JSON.stringify(data, null, 2));
},
onError: (err) => {
alert(err.message);
}
})
}
// async validate
const checkName = (name: string) => {
console.log('checkName')
return new Promise<string>((resolve) => {
setTimeout(() => {
if (name === 'test') {
resolve('Username already exists');
return;
}
resolve('');
}, 1000)
});
}
// disoiseable validate
const checkName2 = (name: string) => {
console.log('checkName2')
let timer: ReturnType<typeof setTimeout> | null = null;
const promise = new Promise<string>((resolve) => {
timer = setTimeout(() => {
if (name === 'test') {
resolve('Username already exists');
return;
}
resolve('');
}, 1000)
});
return {
promise,
dispose: () => {
timer && clearTimeout(timer);
}
}
}
</script>
<template>
<FormProvider :form="form">
<div class="vfm">
<!-- // base fields -->
<!-- // 基础字段 -->
<div class="vfm-p">
<div class="vfm-label">User Name:</div>
<div class="vfm-value">
<Field
:form="form"
name="username"
:rules="[
{
required: true
},
{
validator: (v) => {
return checkName(v);
},
debounce: 300
}
]"
change-type="ONINPUT"
#default="{ field }"
>
<input
class="vfm-input"
type="text"
v-bind="field"
/>
<div v-if="form.isValidating('username')" class="vfm-error">
loading...
</div>
<div v-else class="vfm-error">
{{ form.fieldError('username')?.message }}
</div>
</Field>
</div>
</div>
<div class="vfm-p">
<div class="vfm-label">Password:</div>
<div class="vfm-value">
<Field
:form="form"
name="password"
:rules="[
{
required: true,
pattern: /[a-zA-Z0-9]{8,20}/
}
]"
#default="{ field }"
>
<input
class="vfm-input"
type="password"
v-bind="field"
/>
<div class="vfm-error">
{{ form.fieldError('password')?.message }}
</div>
</Field>
</div>
</div>
<div class="vfm-p">
<div class="vfm-label">Password Confirm:</div>
<div class="vfm-value">
<Field
:form="form"
name="passwordConfirm"
:deps="() => ({
password: formState.values.password
})"
:rules="[
{
required: true,
pattern: /[a-zA-Z0-9]{8,20}/
}, {
validator: (v, { password }) => {
if (!password) return '';
if (v !== password) {
return 'The passwordConfirm must same as password'
}
return ''
}
}
]"
#default="{ field }"
>
<input
class="vfm-input"
type="password"
v-bind="field"
/>
<div class="vfm-error">
{{ form.fieldError('passwordConfirm')?.message }}
</div>
</Field>
</div>
</div>
<!-- // nested fields -->
<!-- // 嵌套字段 -->
<BaseInfo />
<!-- array fields -->
<!-- 数组字段 -->
<div class="vfm-p">
<div class="vfm-label">Tags:</div>
<div class="vfm-value">
<FieldArray :form="form" name="tags" #default="{ append, remove, fields }">
<div class="vfm-flex">
<div class="vfm-flex-item" v-for="(item, index) in fields" :key="item.id">
<div class="vfm-flex-item-box">
<div class="vfm-flex-item-ins">
<Field
:form="form"
:name="`tags.${index}`"
:rules="[
{
required: true,
message: 'Required'
}
]"
#default="{ field }"
>
<input
class="vfm-input"
type="text"
v-bind="field"
/>
</Field>
</div>
<div class="vfm-action red" @click="remove(item.id)">-</div>
</div>
<div class="vfm-error">
{{ form.fieldError(`tags.${index}`)?.message }}
</div>
</div>
</div>
<div class="vfm-action" @click="append('')">+</div>
</FieldArray>
</div>
</div>
<!-- virtual fields: only validate with form state, no field value -->
<!-- 虚拟字段: 值根据表单状态进行校验,没有字段值 -->
<div class="vfm-p">
<VirtualField
:form="form"
name="tags"
:value="() => {
// visit length, so tags.length change, revalidate
form.state.values.tags.length;
return form.state.values.tags
}"
:rules="[
{
requiredLength: true,
message: 'must have one tag'
}
]"
/>
<div class="vfm-label">Virtual Field (Tags):</div>
<div class="vfm-error">
{{ form.virtualFieldError('tags')?.message }}
</div>
</div>
<!-- // nested array fields -->
<!-- // 嵌套的数组字段 -->
<AddressList />
<!-- // object value -->
<!-- // object类型的值 -->
<!-- // the NestedValue<xxx> will be treat as the `value` type of field `schools`, not nested fields -->
<!-- // NestedValue<xxx> 被当做字段`schools`的值,而不是嵌套的字段 -->
<SchoolList />
<div class="vfm-p">
<button class="vfm-button" @click="submit">Submit</button>
<div class="vfm-error" :style="{ width: '100%', textAlign: 'center' }" v-if="form.state.isError">
Have error, cannot submit
</div>
</div>
</div>
</FormProvider>
</template>
<!-- // nested fields -->
<!-- // 嵌套字段 -->
<script setup lang="ts">
import { Field, useForm } from 'vfm';
import { getForm } from '../form';
const form = useForm(getForm);
const formState = form.state;
</script>
<template>
<div class="vfm-block-title">Base Info</div>
<div class="vfm-p">
<div class="vfm-label">
Birth Day:
</div>
<div class="vfm-value">
<Field
:form="form"
name="baseInfo.birthDay"
:rules="[
{
required: true
}
]"
value="1988"
#default="{ field }"
>
<input
class="vfm-input"
type="text"
v-bind="field"
/>
</Field>
<div class="vfm-error">
{{ form.fieldError('baseInfo.birthDay')?.message }}
</div>
</div>
</div>
<div class="vfm-p">
<div class="vfm-label">
Age:
</div>
<div class="vfm-value">
<Field
:form="form"
name="baseInfo.age"
:rules="[
{
required: true
}
]"
#default="{ field }"
>
<input
class="vfm-input"
type="text"
v-bind="field"
/>
</Field>
<div class="vfm-error">
{{ form.fieldError('baseInfo.age')?.message }}
</div>
</div>
</div>
</template>
<!-- // nested array fields -->
<!-- // 嵌套的数组字段 -->
<script setup lang="ts">
import { useFieldArray, Field, useForm } from 'vfm';
import { getForm } from '../form';
const form = useForm(getForm);
const { fields, append, remove } = useFieldArray({
form, path: 'address'
});
const add = () => {
append({
phone: '',
detail: ''
});
};
const del = (id: string) => {
remove(id);
};
</script>
<template>
<div class="vfm-block-title">Address</div>
<div class="block" v-for="(item, index) in fields" :key="item.id">
<div class="vfm-p">
<div class="vfm-label">
Phone:
</div>
<div class="vfm-value">
<Field
:form="form"
:name="`address.${index}.phone`"
:rules="[
{
required: true
}
]"
#default="{ field }"
>
<input
class="vfm-input"
type="text"
v-bind="field"
/>
</Field>
<div class="vfm-error">
{{ form.fieldError(`address.${index}.phone`)?.message }}
</div>
</div>
</div>
<div class="vfm-p">
<div class="vfm-label">
Detail:
</div>
<div class="vfm-value">
<Field
:form="form"
:name="`address.${index}.detail`"
:rules="[
{
required: true
}
]"
#default="{ field }"
>
<input
class="vfm-input"
type="text"
v-bind="field"
/>
</Field>
<div class="vfm-error">
{{ form.fieldError(`address.${index}.detail`)?.message }}
</div>
</div>
</div>
<div class="vfm-p">
<div class="vfm-action red" @click="() => del(item.id)">- delete</div>
</div>
</div>
<div class="vfm-p">
<div class="vfm-action" @click="() => add()">+ Add Address</div>
</div>
</template>
<style scoped>
.block {
border: 1px dashed #ddd;
padding: 20px;
margin-bottom: 20px;
}
</style>
<!-- // object value -->
<!-- // object类型的值 -->
<!-- // the NestedValue<xxx> will be treat as the `value` type of field `schools`, not nested fields -->
<!-- // NestedValue<xxx> 被当做字段`schools`的值,而不是嵌套的字段 -->
<script setup lang="ts">
import { ref } from 'vue';
import { Field, useForm } from 'vfm';
import { getForm } from '../form';
import SelectSchool from './SelectSchool.vue';
const form = useForm(getForm);
const values = form.getPathValueRef('schools');
const visible = ref(false);
const showSelectSchool = () => {
visible.value = true;
};
</script>
<template>
<div class="box">
<Field
:form="form"
name="schools"
:rules="[
{
requiredLength: true,
message: 'Must select one school'
}
]"
#default="{ field }"
>
<div class="vfm-block-title">Schools</div>
<div class="vfm-p">
<div class="vfm-label">
Selected Schools:
</div>
<div class="vfm-value">
<div v-show="!visible" class="vfm-input" @click="showSelectSchool">
{{ values.map((v) => v.name).join(',') }}
</div>
<div v-show="!visible" class="vfm-error">
{{ form.fieldError('schools')?.message }}
</div>
<!-- select schools -->
<SelectSchool v-bind="field" v-model:visible="visible" v-if="visible" />
</div>
</div>
</Field>
</div>
</template>
<style scoped>
.box {
position: relative;
}
.sel {
height: 180px;
}
.sel option {
line-height: 30px;
height: 30px;
}
</style>
<script setup lang="ts">
import { PropType, ref } from 'vue';
type School = {
name: string;
address: string;
};
const props = defineProps({
visible: {
type: Boolean,
default: false
},
value: {
type: Array as PropType<School[]>,
default: () => []
}
});
const emit = defineEmits<{
(e: 'change', v: School[]): void;
(e: 'update:visible', v: boolean): void;
}>();
const allSchools: School[] = [
{
name: 'School A',
address: 'School Address A'
},
{
name: 'School B',
address: 'School Address B'
},
{
name: 'School C',
address: 'School Address C'
},
{
name: 'School D',
address: 'School Address D'
},
{
name: 'School E',
address: 'School Address E'
}
];
const items = ref(allSchools.map((item) => {
const find = props.value.find((v) => v.name === item.name);
return {
...item,
selected: !!find,
}
}));
const onClose = () => {
emit('update:visible', false);
}
const onConfirm = () => {
const values = items.value.filter((v) => v.selected).map((v) => {
return {
name: v.name,
address: v.address
}
});
emit('change', values);
emit('update:visible', false);
}
</script>
<template>
<div class="sbox">
<div class="item" v-for="item in items" :key="item.name">
<label>
<input type="checkbox" v-model="item.selected" />
<span>{{ item.name }} <{{ item.address }}></span>
</label>
</div>
<div class="btns">
<button @click="onClose">close</button>
<button @click="onConfirm">confirm</button>
</div>
</div>
</template>
<style scoped>
.sbox {
position: relative;
background: #fff;
padding-bottom: 20px;
}
.btns {
margin-top: 20px;
display: flex;
}
.btns button {
margin-right: 20px;
}
</style>
在子组件中使用form
使用
FormProvider
或useProvideForm
来向子组件注入form Dependency Injection.在子组件中使用
useForm
来获取form。
提示: 如果你使用 typescript, 你可以传递 form getter
给 useForm
来获取更好的类型提示。
// in vue
import { useForm } from 'vfm';
import { getForm } from '../form';
const form = useForm(getForm);
// or
const form = useForm();