快速开始

在下面的例子中,你可以点击 文件名 来查看代码,点击 运行结果 来使用代码的结果。

安装

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 }} &lt;{{ item.address }}&gt;</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

  1. 使用 FormProvideruseProvideForm 来向子组件注入form Dependency Injectionopen in new window.

  2. 在子组件中使用 useForm 来获取form。

提示: 如果你使用 typescript, 你可以传递 form getteruseForm 来获取更好的类型提示。

// in vue
import { useForm } from 'vfm';
import { getForm } from '../form';

const form = useForm(getForm);
// or
const form = useForm();