Getting Started

In the examples below, you can click <File Name> to view real code, and click Result to show the result of the code.

Install

npm install vfm
yarn add vfm

Create Form Getter

A Form Getter is a function that return the form.

Details:
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'
});

Register Fields

Details:
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>

Use form in child components

  1. Use FormProvider or useProvideForm to provide form to children by Dependency Injectionopen in new window.

  2. Use useForm in child components to get the form.

Tips: If you use typescript, you can pass form getter to useForm for get more type intellisense.

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

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