mcpField
F
formisch

formisch

github.com/open-circle開発16424 回インストール1 日前に更新
リポジトリ

formisch について

formischは、複数のフレームワークに対応するヘッドレスなスキーマベースのフォームライブラリFormischを、AIエージェントが扱うためのスキルです。Valibotによる検証、フォーム状態、型安全なフィールド操作を解説します。

主な機能

  1. 01Valibotスキーマによる検証
  2. 02フォームとフィールドの状態管理
  3. 03変更状態とエラーのヘルパー
  4. 04動的なフィールド配列
  5. 05TypeScriptの型推論
  6. 068つのフレームワークのガイド

活用シーン

  1. 01各種フレームワークでの検証付きフォーム実装
  2. 02編集状態と初期値の追跡
  3. 03動的リストを含むフォームの構築
  4. 04React NativeでのFormisch利用

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/formischを入力して読み込まれたか確認します。

同梱ファイル.claude-plugin/plugin.json

詳細

Formisch

このスキルは、AI エージェントが最新のフレームワーク向けスキーマベースのヘッドレスフォームライブラリである Formisch を効果的に使用できるよう支援します。

このスキルを使用するタイミング

  • ユーザーが Formisch を使ったフォーム処理について尋ねたとき
  • フォームの状態やバリデーションを管理するとき
  • Angular、Preact、Qwik、React、React Native、Solid、Svelte、Vue のフォームを扱うとき
  • Valibot のスキーマをフォームに統合するとき

はじめに

Formisch は、複数のフレームワークで動作するスキーマベースのヘッドレスフォームライブラリです。主な特長は次のとおりです。

  • 小さなバンドルサイズ — 約 2.5 kB から
  • スキーマベースのバリデーション — 型安全なバリデーションに Valibot を使用
  • ヘッドレス設計 — UI を完全に制御可能
  • 型安全性 — 自動補完を含む完全な TypeScript サポート
  • フレームワークネイティブ — サポートされる各フレームワークでネイティブなパフォーマンスを実現
サポートされるフレームワーク
フレームワークパッケージフック/プリミティブ
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 をインストールする(ピア依存関係)
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

基本概念

スキーマファースト設計

すべてのフォームは Valibot のスキーマから始まります。型はスキーマから自動的に推論されます。

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."),
  ),
});
フォームストア

フォームストアは、フォームのすべての状態を管理します。フレームワーク固有のフック/プリミティブを使ってアクセスします。

フォームストアのプロパティ:

  • isSubmitting — フォームを現在送信中
  • isSubmitted — フォームの送信が試みられた
  • isValidating — バリデーションを実行中
  • isTouched — 1 つ以上のフィールドがフォーカスされた
  • isEdited — 1 つ以上のフィールドが編集された
  • isDirty — 1 つ以上のフィールドが初期値と異なる
  • 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 のプロパティはシグナルであり、通常の値である path を除き、field.errors() のように呼び出します。Preact と Qwik のプロパティはシグナルです。条件式や通常の TypeScript ロジックでは .value を使用してください。あるフレームワークのアクセス構文を別のフレームワークにコピーしないでください。

変更追跡

Formisch は各フィールドについて、次の 2 つの入力を追跡します。

  • 初期入力 — 変更追跡の基準(サーバーの状態)
  • 現在の入力 — ユーザーが編集中の値(クライアントの状態)

現在の入力が初期入力と異なると、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 に変換できるようになります。

省略可能および nullable なフィールドは 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 パターンに従います。

  • 第 1 引数:フォームストア
  • 第 2 引数:設定オブジェクト
値の読み取り
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 を使用します。

フィールド配列ストアは、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 }> の制約は、スキーマに string 型の email フィールドを含む任意のフォームを受け付けます。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'変更イベント時に再検証します(値が確定したとき)
'blur'フィールドからフォーカスが外れたときに再検証します
'submit'フォームの送信時のみ再検証します

特殊な入力

セレクト(単一)
<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>
セレクト(複数)
<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 フック

複雑なフィールドコンポーネントでは、Field コンポーネントの代わりに useField フックを使用します。

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 フック — コンポーネントのロジックにアクセスする単一のフィールド

useFieldArray フックは、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>;

これらのセッターは、ネイティブ入力と同様にフィールドの値を更新し、検証を実行します。

次のような場合に便利です。

  • コンポーネントライブラリ — ネイティブ要素をラップしながら、それらを公開しないもの
  • 日付ピッカー、リッチテキストエディター、カラーピッカーなどの複雑なカスタム入力

非同期送信

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 が、それらを呼び出すコンポーネントでシグナルの追跡を有効にするためです。他のフレームワークでは、それぞれのリアクティブスコープによって読み取りが追跡されます。

追加リソース

コメント