mcpField
F
formisch

formisch

github.com/open-circle开发16424 次安装今天更新
代码仓库

关于 formisch

formisch 是帮助 AI Agent 使用 Formisch 构建表单的 skill,面向多种框架的无头、基于 schema 的表单库。内容涵盖 Valibot 校验、表单状态和类型安全的字段操作。

主要功能

  1. 01基于 Valibot schema 校验
  2. 02管理表单与字段状态
  3. 03脏状态和错误处理工具
  4. 04动态字段数组
  5. 05TypeScript 类型推断
  6. 06八种框架的使用指南

适用场景

  1. 01在不同框架中实现校验表单
  2. 02跟踪编辑状态与初始值
  3. 03构建包含动态列表的表单
  4. 04在 React Native 中使用 Formisch

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/open-circle/agent-skills --skill formisch

安装后在 Claude Code 中输入/formisch确认已加载。

附带文件.claude-plugin/plugin.json

详细介绍

Formisch

此技能可帮助 AI Agent 高效地使用 Formisch,这是一款面向现代框架、基于 schema 的无头表单库。

何时使用此技能

  • 用户询问如何使用 Formisch 处理表单时
  • 管理表单状态和验证时
  • 使用 Angular、Preact、Qwik、React、React Native、Solid、Svelte 或 Vue 表单时
  • 将 Valibot schema 集成到表单中时

简介

Formisch 是一款基于 schema 的无头表单库,可用于多个框架。主要特点:

  • 包体积小 — 起始大小约为 2.5 kB
  • 基于 schema 的验证 — 使用 Valibot 进行类型安全的验证
  • 无头设计 — 完全由你控制 UI
  • 类型安全 — 完整支持 TypeScript 和自动补全
  • 原生适配框架 — 在每个受支持的框架中提供原生性能
支持的框架
框架包Hook/原语
Angular@formisch/angularinjectForm
Preact@formisch/preactuseForm
Qwik@formisch/qwikuseForm$
React@formisch/reactuseForm
React Native@formisch/react-nativeuseForm
SolidJS@formisch/solidcreateForm
Svelte@formisch/sveltecreateForm
Vue@formisch/vueuseForm

安装

1. 安装 Valibot(peer dependency)
npm install valibot
2. 为你的框架安装 Formisch
npm install @formisch/react         # React
npm install @formisch/angular       # Angular
npm install @formisch/vue           # Vue
npm install @formisch/solid         # SolidJS
npm install @formisch/preact        # Preact
npm install @formisch/svelte        # Svelte
npm install @formisch/qwik          # Qwik
npm install @formisch/react-native  # React Native

核心概念

Schema 优先设计

每个表单都始于一个 Valibot schema。类型会根据 schema 自动推断。

import * as v from "valibot";

const LoginSchema = v.object({
  email: v.pipe(
    v.string("Please enter your email."),
    v.nonEmpty("Please enter your email."),
    v.email("The email address is badly formatted."),
  ),
  password: v.pipe(
    v.string("Please enter your password."),
    v.nonEmpty("Please enter your password."),
    v.minLength(8, "Your password must have 8 characters or more."),
  ),
});
表单存储

表单存储管理所有表单状态。通过框架专属的 hook/原语访问它。

表单存储属性:

  • isSubmitting — 表单当前正在提交
  • isSubmitted — 已尝试提交表单
  • isValidating — 正在进行验证
  • isTouched — 至少有一个字段已被触碰
  • isEdited — 至少有一个字段已被编辑
  • isDirty — 至少有一个字段的值与初始值不同
  • isValid — 所有字段均通过验证
  • errors — 根级验证错误
字段存储

每个字段都有自己的响应式存储,其中包含:

  • path — 指向该字段的路径数组
  • input — 当前字段值
  • errors — 字段专属错误
  • isTouched — 字段已获得焦点
  • isEdited — 字段值已被编辑
  • isDirty — 字段值与初始值不同
  • isValid — 字段通过验证
  • props — 要展开到原生元素上的属性(Angular 则使用 [formischControl] 连接控件)
  • onChange(React 和 React Native)/ onInput(Solid、Svelte、Preact 和 Qwik)/ setInput(Angular)— 以编程方式设置字段输入值。当字段无法连接到原生元素时使用。在 Vue 中,直接设置 field.input(例如使用 v-model)。

存储的响应性因框架而异。React、React Native、Solid、Svelte 和 Vue 会公开普通响应式属性。Angular 属性是信号,调用方式为 field.errors(),但 path 除外,它是普通值。Preact 和 Qwik 属性是信号;在条件判断和普通 TypeScript 逻辑中使用 .value。不要将一个框架的访问语法照搬到另一个框架中。

Dirty 状态跟踪

Formisch 会跟踪每个字段的两个输入值:

  • 初始输入 — dirty 状态跟踪的基准值(服务端状态)
  • 当前输入 — 用户正在编辑的值(客户端状态)

当当前输入与初始输入不同时,isDirty 会变为 true。

框架示例

Angular 示例

Angular 使用信号、依赖注入和指令,而不是 JSX 组件 API。

import { Component } from "@angular/core";
import {
  FormischControl,
  FormischField,
  FormischForm,
  injectForm,
  type SubmitHandler,
} from "@formisch/angular";
import * as v from "valibot";

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

@Component({
  selector: "app-login",
  imports: [FormischForm, FormischField, FormischControl],
  template: `
    <form [formischForm]="loginForm" [formischSubmit]="handleSubmit">
      <ng-container *formischField="['email'] of loginForm; let field">
        <input [formischControl]="field" type="email" />
        @if (field.errors(); as errors) {
          <div>{{ errors[0] }}</div>
        }
      </ng-container>
      <button type="submit" [disabled]="loginForm.isSubmitting()">Login</button>
    </form>
  `,
})
export class LoginComponent {
  readonly loginForm = injectForm({ schema: LoginSchema });

  readonly handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
    console.log(output);
  };
}

让 [formischControl] 同步原生控件。除非 value 用于标识单选按钮组或复选框组中的选项,否则不要添加会产生冲突的 [value] 或 [checked] 绑定。

React Native 示例

React Native 没有 DOM <form> 元素或 Formisch Form 组件。使用 handleSubmit,并将 field.props 绑定到 TextInput。

import { Field, handleSubmit, useForm } from "@formisch/react-native";
import { Button, TextInput, View } from "react-native";
import * as v from "valibot";

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

export default function LoginScreen() {
  const loginForm = useForm({ schema: LoginSchema });
  const submitForm = handleSubmit(loginForm, (output) => console.log(output));

  return (
    <View>
      <Field of={loginForm} path={["email"]}>
        {(field) => (
          <TextInput
            {...field.props}
            value={field.input}
            autoCapitalize="none"
            keyboardType="email-address"
          />
        )}
      </Field>
      <Field of={loginForm} path={["password"]}>
        {(field) => (
          <TextInput {...field.props} value={field.input} secureTextEntry />
        )}
      </Field>
      <Button title="Login" onPress={submitForm} />
    </View>
  );
}

React Native 文本输入框是受控的,因此始终传入 value={field.input}。对于开关、滑块、选择器和非字符串值,请使用 field.onChange(value)。

React 示例
import { Field, Form, useForm } from "@formisch/react";
import type { SubmitHandler } from "@formisch/react";
import * as v from "valibot";

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

export default function LoginPage() {
  const loginForm = useForm({
    schema: LoginSchema,
  });

  const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
    console.log(output); // { email: string, password: string }
  };

  return (
    <Form of={loginForm} onSubmit={handleSubmit}>
      <Field of={loginForm} path={["email"]}>
        {(field) => (
          <div>
            <input {...field.props} value={field.input} type="email" />
            {field.errors && <div>{field.errors[0]}</div>}
          </div>
        )}
      </Field>
      <Field of={loginForm} path={["password"]}>
        {(field) => (
          <div>
            <input {...field.props} value={field.input} type="password" />
            {field.errors && <div>{field.errors[0]}</div>}
          </div>
        )}
      </Field>
      <button type="submit" disabled={loginForm.isSubmitting}>
        {loginForm.isSubmitting ? "Submitting..." : "Login"}
      </button>
    </Form>
  );
}
Vue 示例
<script setup lang="ts">
import { Field, Form, useForm } from "@formisch/vue";
import type { SubmitHandler } from "@formisch/vue";
import * as v from "valibot";

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

const loginForm = useForm({
  schema: LoginSchema,
});

const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
  console.log(output);
};
</script>

<template>
  <Form :of="loginForm" @submit="handleSubmit">
    <Field :of="loginForm" :path="['email']" v-slot="field">
      <div>
        <input v-model="field.input" v-bind="field.props" type="email" />
        <div v-if="field.errors">{{ field.errors[0] }}</div>
      </div>
    </Field>
    <Field :of="loginForm" :path="['password']" v-slot="field">
      <div>
        <input v-model="field.input" v-bind="field.props" type="password" />
        <div v-if="field.errors">{{ field.errors[0] }}</div>
      </div>
    </Field>
    <button type="submit">Login</button>
  </Form>
</template>
SolidJS 示例
import { Field, Form, createForm } from "@formisch/solid";
import type { SubmitHandler } from "@formisch/solid";
import * as v from "valibot";

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

export default function LoginPage() {
  const loginForm = createForm({
    schema: LoginSchema,
  });

  const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
    console.log(output);
  };

  return (
    <Form of={loginForm} onSubmit={handleSubmit}>
      <Field of={loginForm} path={["email"]}>
        {(field) => (
          <div>
            <input {...field.props} value={field.input} type="email" />
            {field.errors && <div>{field.errors[0]}</div>}
          </div>
        )}
      </Field>
      <Field of={loginForm} path={["password"]}>
        {(field) => (
          <div>
            <input {...field.props} value={field.input} type="password" />
            {field.errors && <div>{field.errors[0]}</div>}
          </div>
        )}
      </Field>
      <button type="submit">Login</button>
    </Form>
  );
}
Svelte 示例
<script lang="ts">
  import { createForm, Field, Form } from '@formisch/svelte';
  import type { SubmitHandler } from '@formisch/svelte';
  import * as v from 'valibot';

  const LoginSchema = v.object({
    email: v.pipe(v.string(), v.email()),
    password: v.pipe(v.string(), v.minLength(8)),
  });

  const loginForm = createForm({
    schema: LoginSchema,
  });

  const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
    console.log(output);
  };
</script>

<Form of={loginForm} onsubmit={handleSubmit}>
  <Field of={loginForm} path={['email']}>
    {#snippet children(field)}
      <div>
        <input {...field.props} value={field.input} type="email" />
        {#if field.errors}
          <div>{field.errors[0]}</div>
        {/if}
      </div>
    {/snippet}
  </Field>
  <Field of={loginForm} path={['password']}>
    {#snippet children(field)}
      <div>
        <input {...field.props} value={field.input} type="password" />
        {#if field.errors}
          <div>{field.errors[0]}</div>
        {/if}
      </div>
    {/snippet}
  </Field>
  <button type="submit">Login</button>
</Form>
Qwik 示例
import { Field, Form, useForm$ } from "@formisch/qwik";
import { component$ } from "@qwik.dev/core";
import * as v from "valibot";

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

export default component$(() => {
  const loginForm = useForm$(() => ({
    schema: LoginSchema,
  }));

  return (
    <Form of={loginForm} onSubmit$={(output) => console.log(output)}>
      <Field
        of={loginForm}
        path={["email"]}
        render$={(field) => (
          <div>
            <input {...field.props} value={field.input.value} type="email" />
            {field.errors.value && <div>{field.errors.value[0]}</div>}
          </div>
        )}
      />
      <Field
        of={loginForm}
        path={["password"]}
        render$={(field) => (
          <div>
            <input {...field.props} value={field.input.value} type="password" />
            {field.errors.value && <div>{field.errors.value[0]}</div>}
          </div>
        )}
      />
      <button type="submit">Login</button>
    </Form>
  );
});

表单配置

const form = useForm({
  // Required: Valibot schema
  schema: MySchema,

  // Optional: Initial values (partial allowed)
  initialInput: {
    email: "[email protected]",
  },

  // Optional: Empty values for required fields without initial input
  // Required strings default to ''; number, boolean, and date to undefined
  emptyInput: {
    number: 0,
  },

  // Optional: When first validation occurs
  // Options: 'initial' | 'touch' | 'input' | 'change' | 'blur' | 'submit' (default)
  validate: "submit",

  // Optional: When a field is validated again once it already has an
  // error or the form has been submitted
  // Options: 'touch' | 'input' (default) | 'change' | 'blur' | 'submit'
  revalidate: "input",
});

在 Qwik 中,useForm$ 必须接收一个返回配置的函数,例如 useForm$(() => ({ schema: MySchema }))。这样 Qwik 才能将配置转换为 QRL。

可选字段和可空字段仍为 undefined。emptyInput 仅为输入值为 undefined 的必填字段提供回退值。

字段路径

路径是类型安全的数组,用于引用架构中的字段。

// Top-level field
<Field of={form} path={['email']} />

// Nested field (schema: { user: { email: string } })
<Field of={form} path={['user', 'email']} />

// Array item field (schema: { todos: [{ label: string }] })
<Field of={form} path={['todos', 0, 'label']} />

// Dynamic array index
{items.map((item, index) => (
  <Field of={form} path={['todos', index, 'label']} key={item} />
))}

表单方法

所有方法都遵循一致的 API 模式:

  • 第一个参数:表单 store
  • 第二个参数:配置对象
读取值
import {
  getDeepError,
  getDeepErrorEntries,
  getDeepErrorEntry,
  getDeepErrors,
  getErrors,
  getInput,
} from "@formisch/react";

// Get field value
const email = getInput(form, { path: ["email"] });

// Get entire form input
const allInputs = getInput(form);

// Get field errors
const emailErrors = getErrors(form, { path: ["email"] });

// Get all errors across all fields (including form-level errors)
const allErrors = getDeepErrors(form);

// Get all errors of a field and its descendants
const todoErrors = getDeepErrors(form, { path: ["todos"] });

// Get every error together with its field path
const errorEntries = getDeepErrorEntries(form);

// Get only the first error of a field and its descendants
const firstTodoError = getDeepError(form, { path: ["todos"] });

// Get only the first error together with its field path
const firstErrorEntry = getDeepErrorEntry(form);

表单级别的 form.errors 和 getErrors(form) 仅包含根级错误。需要获取后代字段错误时,请使用深层错误方法。单数形式的 getDeepError 和 getDeepErrorEntry 会在找到第一个有错误的字段时停止,这适用于为嵌套结构显示单条消息。

读取脏状态
import {
  getDirtyInput,
  getDirtyPaths,
  isDirty,
  pickDirty,
} from "@formisch/react";

// Raw dirty form input, or undefined when nothing is dirty
const dirtyInput = getDirtyInput(form);

// Paths of dirty fields (arrays are treated as atomic values)
const dirtyPaths = getDirtyPaths(form);

// Cheap boolean check when the dirty values are not needed
const hasChanges = isDirty(form);

// Boolean check scoped to a field and its descendants
const emailChanged = isDirty(form, { path: ["email"] });

// In a submit handler, keep transformed output only where fields are dirty
const dirtyOutput = pickDirty(form, { from: output });

getDirtyInput 返回原始表单输入。pickDirty 会将表单的脏值掩码应用于给定值,适用于已验证和转换后的提交输出。

同级方法 isTouched、isEdited 和 isValid 遵循与 isDirty 相同的模式。不带配置调用时,它们会检查整个表单;带 path 调用时,则会检查特定字段及其后代字段。

设置值
import { setInput, setErrors, reset } from "@formisch/react";

// Set field value (updates current input, not initial)
setInput(form, { path: ["email"], input: "[email protected]" });

// Set field errors manually
setErrors(form, { path: ["email"], errors: ["Email already taken"] });

// Clear errors
setErrors(form, { path: ["email"], errors: null });

// Reset entire form
reset(form);

// Reset a single field
reset(form, { path: ["email"] });

// Reset with new initial values
reset(form, {
  initialInput: { email: "", password: "" },
});

// Reset but keep current input
reset(form, {
  initialInput: newServerData,
  keepInput: true,
});

reset 还接受 keepTouched、keepEdited 和 keepErrors 标志。表单级别的重置还接受 keepSubmitted。所有标志默认均为 false。

表单控制
import { validate, focus, submit, handleSubmit } from "@formisch/react";

// Validate form manually (returns a Promise of a Valibot SafeParseResult)
const result = await validate(form);
if (result.success) {
  console.log(result.output);
} else {
  console.log(result.issues);
}

// Validate and focus first error field
await validate(form, { shouldFocus: true });

// Focus a specific field
focus(form, { path: ["email"] });

// Programmatically submit form
submit(form);

// Create submit handler for external buttons
const onExternalSubmit = handleSubmit(form, (output) => {
  console.log(output);
});

submit 需要已注册的 DOM 表单,且不会由 @formisch/react-native 导出。在 React Native 中以及没有 <form> 元素的布局中,请改为调用 handleSubmit 返回的函数。

字段数组

对于动态字段列表,请使用 FieldArray 和数组操作方法。

字段数组 store 会公开 path、items(用作键的稳定项目 ID)、errors、isTouched、isEdited、isDirty 和 isValid。

架构
const TodoSchema = v.object({
  heading: v.pipe(v.string(), v.nonEmpty()),
  todos: v.pipe(
    v.array(
      v.object({
        label: v.pipe(v.string(), v.nonEmpty()),
        deadline: v.pipe(v.string(), v.nonEmpty()),
      }),
    ),
    v.nonEmpty(),
    v.maxLength(10),
  ),
});
React 示例
import {
  Field,
  FieldArray,
  Form,
  useForm,
  insert,
  remove,
  move,
  swap,
} from "@formisch/react";

export default function TodoPage() {
  const todoForm = useForm({
    schema: TodoSchema,
    initialInput: {
      heading: "",
      todos: [{ label: "", deadline: "" }],
    },
  });

  return (
    <Form of={todoForm} onSubmit={(output) => console.log(output)}>
      <Field of={todoForm} path={["heading"]}>
        {(field) => <input {...field.props} value={field.input} type="text" />}
      </Field>

      <FieldArray of={todoForm} path={["todos"]}>
        {(fieldArray) => (
          <div>
            {fieldArray.items.map((item, index) => (
              <div key={item}>
                <Field of={todoForm} path={["todos", index, "label"]}>
                  {(field) => (
                    <input {...field.props} value={field.input} type="text" />
                  )}
                </Field>
                <Field of={todoForm} path={["todos", index, "deadline"]}>
                  {(field) => (
                    <input {...field.props} value={field.input} type="date" />
                  )}
                </Field>
                <button
                  type="button"
                  onClick={() =>
                    remove(todoForm, { path: ["todos"], at: index })
                  }
                >
                  Delete
                </button>
              </div>
            ))}
            {fieldArray.errors && <div>{fieldArray.errors[0]}</div>}
          </div>
        )}
      </FieldArray>

      <button
        type="button"
        onClick={() =>
          insert(todoForm, {
            path: ["todos"],
            initialInput: { label: "", deadline: "" },
          })
        }
      >
        Add Todo
      </button>

      <button type="submit">Submit</button>
    </Form>
  );
}
数组方法
import { insert, remove, move, swap, replace } from "@formisch/react";

// Add item at end
insert(form, { path: ["todos"], initialInput: { label: "", deadline: "" } });

// Add item at specific index
insert(form, {
  path: ["todos"],
  at: 0,
  initialInput: { label: "", deadline: "" },
});

// Remove item at index
remove(form, { path: ["todos"], at: index });

// Move item from one index to another
move(form, { path: ["todos"], from: 0, to: 3 });

// Swap two items
swap(form, { path: ["todos"], at: 0, and: 1 });

// Replace item at index
replace(form, {
  path: ["todos"],
  at: 0,
  initialInput: { label: "New task", deadline: "2024-12-31" },
});

TypeScript 集成

类型推断

类型会根据 Valibot 架构自动推断:

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

const form = useForm({ schema: LoginSchema });
// form is FormStore<typeof LoginSchema>

// Submit handler receives typed output
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
  output.email; // ✓ string
  output.password; // ✓ string
  output.username; // ✗ TypeScript error
};
输入类型与输出类型

带有转换的架构具有不同的输入类型和输出类型:

const ProfileSchema = v.object({
  age: v.pipe(
    v.string(), // Input: string
    v.transform((input) => Number(input)), // Output: number
    v.number(),
  ),
  birthDate: v.pipe(
    v.string(), // Input: string
    v.transform((input) => new Date(input)), // Output: Date
    v.date(),
  ),
});

// In Field: field.input is string
// In onSubmit: output.age is number, output.birthDate is Date
类型安全的 Props

以适当的类型将表单传递给子组件:

import { Form, type FormStore, useForm } from "@formisch/react";

export default function LoginPage() {
  const loginForm = useForm({ schema: LoginSchema });
  return <FormContent of={loginForm} />;
}

type FormContentProps = {
  of: FormStore<typeof LoginSchema>;
};

function FormContent({ of }: FormContentProps) {
  return (
    <Form of={of} onSubmit={(output) => console.log(output)}>
      {/* ... */}
    </Form>
  );
}
泛型字段组件

创建具有适当类型的可复用字段组件:

import { useField, type FormStore } from "@formisch/react";
import * as v from "valibot";

type EmailInputProps = {
  of: FormStore<v.GenericSchema<{ email: string }>>;
};

function EmailInput({ of }: EmailInputProps) {
  const field = useField(of, { path: ["email"] });

  return (
    <div>
      <input {...field.props} value={field.input} type="email" />
      {field.errors && <div>{field.errors[0]}</div>}
    </div>
  );
}

v.GenericSchema<{ email: string }> 约束接受架构中包含 email 字段且类型为 string 的任何表单。TypeScript 会在编译时捕获类型不匹配。

可用类型
import type {
  FormStore, // Form store type
  FieldStore, // Field store type
  FieldArrayStore, // Field array store type
  SubmitHandler, // Submit handler function type
  ValidPath, // Valid field path type
  ValidArrayPath, // Valid array field path type
  Schema, // Base schema type from Valibot
} from "@formisch/react";

验证时机

validate 选项

控制首次验证的时机:

值描述
'initial'创建表单时立即验证
'touch'字段首次获得焦点时验证
'input'每次输入事件时验证
'change'值提交时验证(值已确认)
'blur'字段失去焦点时验证
'submit'仅在提交表单时验证(默认)
revalidate 选项

控制字段在已有错误或表单已提交后,再次验证的时机:

值描述
'touch'字段首次获得焦点时重新验证
'input'每次输入事件时重新验证(默认)
'change'值提交时,在 change 事件时重新验证
'blur'字段失去焦点时重新验证
'submit'仅在提交表单时重新验证

特殊输入

Select(单选)
<Field of={form} path={["framework"]}>
  {(field) => (
    <select {...field.props} value={field.input ?? ""}>
      {options.map(({ label, value }) => (
        <option key={value} value={value}>
          {label}
        </option>
      ))}
    </select>
  )}
</Field>
Select(多选)
<Field of={form} path={["frameworks"]}>
  {(field) => (
    <select {...field.props} value={field.input ?? []} multiple>
      {options.map(({ label, value }) => (
        <option key={value} value={value}>
          {label}
        </option>
      ))}
    </select>
  )}
</Field>
复选框
<Field of={form} path={["acceptTerms"]}>
  {(field) => <input {...field.props} type="checkbox" checked={field.input} />}
</Field>
文件输入框

文件输入框无法受控。请通过其周围的 UI 进行处理:

<Field of={form} path={["avatar"]}>
  {(field) => (
    <div>
      <input {...field.props} type="file" />
      {field.input && <span>{field.input.name}</span>}
    </div>
  )}
</Field>

useField Hook

对于复杂的字段组件,请使用 useField hook,而不是 Field 组件:

import { useField } from "@formisch/react";
import { useEffect } from "react";

function EmailInput({ form }) {
  const field = useField(form, { path: ["email"] });

  // Access field state in component logic
  useEffect(() => {
    if (field.errors) {
      console.log("Email has errors:", field.errors);
    }
  }, [field.errors]);

  return (
    <div>
      <input {...field.props} value={field.input} type="email" />
      {field.errors && <div>{field.errors[0]}</div>}
    </div>
  );
}

如何选择:

  • Field 组件 — 在同一组件中使用多个字段
  • useField hook — 需要访问组件逻辑的单个字段

useFieldArray hook 是 FieldArray 组件的对应版本。在 Angular 中,请使用 injectField 和 injectFieldArray 函数,或 *formischField 和 *formischFieldArray 指令。

使用组件库

使用不公开底层原生元素的组件库时,不能直接展开 field.props。请改用 field.onChange(React 和 React Native)、field.onInput(Solid、Svelte、Preact 和 Qwik)、field.setInput(Angular)以编程方式更新值,或直接赋值给 field.input(Vue):

import { DatePicker } from "some-component-library";

<Field of={form} path={["date"]}>
  {(field) => (
    <DatePicker
      value={field.input}
      onChange={(newDate) => field.onChange(newDate)}
    />
  )}
</Field>;

这些 setter 会更新字段值并触发验证,效果与原生输入框相同。

适用于:

  • 组件库:封装原生元素但不公开这些元素
  • 复杂的自定义输入框,例如日期选择器、富文本编辑器或颜色选择器

异步提交

import { setErrors, useForm } from "@formisch/react";
import type { SubmitHandler } from "@formisch/react";

const loginForm = useForm({ schema: LoginSchema });

const handleSubmit: SubmitHandler<typeof LoginSchema> = async (output) => {
  try {
    const response = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(output),
    });

    if (!response.ok) {
      // Set server-side errors
      const data = await response.json();
      setErrors(loginForm, { path: ["email"], errors: [data.error] });
    }
  } catch (error) {
    console.error("Submission failed:", error);
  }
};

异步处理程序解析之前,表单的 isSubmitting 状态会一直保持为 true。如果处理程序抛出错误,Formisch 会捕获该错误,并将其消息作为表单级错误设置到 form.errors 中。

常见模式

加载状态
<button type="submit" disabled={form.isSubmitting}>
  {form.isSubmitting ? "Submitting..." : "Submit"}
</button>
按 Enter 提交

Formisch 会通过原生 <form> 元素自动处理此操作。

成功后重置
const handleSubmit: SubmitHandler<typeof Schema> = async (output) => {
  await saveData(output);

  // Full reset to initial state
  reset(form);

  // Or reset but keep current input values
  reset(form, { keepInput: true });
};
同步服务器数据

服务器数据发生变化时,更新基线而不丢失用户的编辑内容:

// After refetching data from server
reset(form, {
  initialInput: newServerData,
  keepInput: true, // Keep user's current edits
  keepTouched: true, // Keep touched state (optional)
});
条件字段
<Form of={form} onSubmit={handleSubmit}>
  <Field of={form} path={["hasAccount"]}>
    {(field) => (
      <input {...field.props} type="checkbox" checked={field.input} />
    )}
  </Field>
  {getInput(form, { path: ["hasAccount"] }) && (
    <Field of={form} path={["accountId"]}>
      {(field) => <input {...field.props} value={field.input} />}
    </Field>
  )}
</Form>

在 React 中,在渲染期间调用 getInput 具有响应性,因为 useForm 和 useField 会在调用它们的组件中启用信号追踪。在其他框架中,读取操作由各自的响应式作用域进行追踪。

更多资源

评论

支持 Markdown