mcpField
F
falcondev-form

falcondev-form

github.com/falcondev-oss開発288 回インストール5 日前に更新
リポジトリ

falcondev-form について

`@vue/reactivity` を基盤とする、フレームワーク非依存のリアクティブなフォーム状態ライブラリです。Standard Schema による検証と遅延フィールドアクセサーを備え、React と Vue のバインディングを提供します。

主な機能

  1. 01Standard Schema による検証
  2. 02リアクティブなフォームとフィールド状態
  3. 03遅延型付きフィールドアクセサー
  4. 04Nullable な編集と検証後の送信
  5. 05React と Vue のバインディング

活用シーン

  1. 01Schema 検証付き React フォームの構築
  2. 02Schema 検証付き Vue フォームの構築
  3. 03ネストしたフィールドや配列フィールドの管理
  4. 04送信前の Nullable な編集の処理

インストール

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

$npx skills add https://github.com/falcondev-oss/skills --skill falcondev-form

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

同梱ファイルreference/frameworks.mdreference/patterns.md

詳細

@falcondev-oss/form

@vue/reactivity 上に構築された、フレームワークに依存しないフォーム状態ライブラリです。Standard Schema(zod v4、arktype など)とリアクティブな sourceValues を渡すと、フィールド アクセサーのツリーを持つリアクティブなフォーム ハンドルを返します。

公開されているパッケージは 3 つです。実行環境に合ったものを選び、アプリケーションコードから -core を直接インポートしないでください。

パッケージエントリーポイント追加機能
@falcondev-oss/form-coreuseFormCore、すべての型、/reactive ヘルパーエンジン。React/Vue 以外でのみ直接使用します
@falcondev-oss/form-reactuseForm、useField、FormFieldMemoReact の再レンダリング連携、field.model = {value,onUpdate}
@falcondev-oss/form-vueuseForm、useFormHandles書き込み可能な算出プロパティ field.model(v-model)

フレームワーク用パッケージの useForm は useFormCore をラップします。オプションもハンドルも同じです。以下はフィールドのバインディングと再レンダリングを除き、すべてのフレームワークで共通です(reference/frameworks.md を参照)。

すべてを説明する 2 つの考え方

  1. アクセサーは遅延評価されるパスで、$use() が実体化します。 form.fields.address.city は型付きパスのプロキシにすぎず、フィールドオブジェクトはまだ存在しません。.$use() を呼び出すと、実際のリアクティブな FormField が作成され、キャッシュされます。ドットまたは .at() でたどり、バインドする末端で .$use() を呼び出してください。

  2. フォームデータは NullableDeep です。 編集中は、すべての値が null になりえます。入力途中のフォームでは、あらゆる場所に null が存在します。そのため、form.data、field.value、アクセサーの型は、スキーマの 入力型を深く nullable にしたものです(オブジェクト → T | null、配列 → (T|null)[] | null)。検証済みの 出力型(null なし)が現れるのは submit({ values }) のみです。UI は nullable 型に合わせて記述し、クリーンなデータは submit で得られるものとして扱ってください。

基本的な使い方

import { useForm } from '@falcondev-oss/form-vue' // or -react
import z from 'zod'

const form = useForm({
  schema: z.object({ name: z.string(), age: z.number() }),
  sourceValues, // initial data (NullableDeep); a value, getter, or ref — React needs a stable reference
  async submit({ values }) {
    // `values` is validated OUTPUT: { name: string, age: number }
    await api.save(values)
    // return nothing → { success: true }; return { success: false } to keep form dirty
  },
})

const nameField = form.fields.name.$use()
nameField.handleChange('Jane')   // programmatic write (validates, marks dirty)
// or write straight through the reactive data:
form.data.name = 'Jane'          // equivalent effect

await form.submit()              // validates whole schema, runs submit if valid

useForm(options)

  • schema — Standard JSON Schema も公開する Standard Schema(zod v4、arktype など)です。JSON Schema は field.schema のメタデータと必須項目の判定に使われます。JSON で表現できない型(Date、bigint、Map など)には codec/transform が必要です。指定しないとメタデータの抽出は警告なしに機能低下します(コンソールに警告が表示されます)。
  • sourceValues — 初期の NullableDeep データです。値、getter () => data、または(Vue の場合)ref を指定します。React では安定した参照である必要があります。詳細は後述します。undefined の場合、解決されるまでフォームは pending 状態になります(isLoading/field.isPending は true、data は undefined で、書き込みは無視されます)。その後値が変わったとき、フォームが dirty でなければ 新しい値でリセットされ、dirty であれば警告を出して更新をスキップします(submit 中を除く)。
  • submit({ values }) — async で、検証済みの出力を受け取ります。void/{success:true} を返すと、フォームは pristine としてマークされます。{success:false} を返すと、dirty のままになります。フォームの状態が確定する前に完了する必要がある処理は、すべて await してください。特にキャッシュやクエリの無効化は重要です。フォームが pristine としてマークされる前に最新データが sourceValues に反映される必要があります(無効化を await しないと、古いデータのままフォームの状態が確定します)。TanStack Query を使う場合は、submit 内で queryClient.invalidateQueries(...) を直接 await するか、mutation の onSuccess で無効化し、submit で await mutateAsync(...) を呼び出してください(mutation を await すると onSuccess も await されます)。
  • disabled? — boolean | Ref | getter です。すべてのフィールドで handleChange/handleBlur/reset をブロックします。
  • hooks? — ライフサイクルフックです(reference/patterns.md を参照)。
sourceValues の記述(この方法で行う)

順序どおりに3つの分岐を設けます。読み込み中 → 既存のエンティティ → 空のデフォルト値です。Vue ではゲッターとして、React では useMemo でメモ化した値として渡します。

// Vue — getter, tracked reactively
useForm({
  schema,
  sourceValues: () => {
    if (isLoading.value) return undefined    // 1. source still loading → form is pending
    if (entity.value) return entity.value    // 2. edit: return the whole entity object
    return { name: null, age: null }         // 3. create: blank defaults
  },
  async submit({ values }) { /* … */ },
})
// React — same branches, wrapped in useMemo
const sourceValues = useMemo(() => {
  if (isLoading) return undefined
  if (entity) return entity
  return { name: null, age: null }
}, [isLoading, entity])

useForm({ schema, sourceValues, async submit({ values }) { /* … */ } })

React では、sourceValues は安定した参照でなければなりません。 useForm の呼び出しでオブジェクト(またはゲッター)を直接インラインで指定しないでください。React はレンダーのたびにコンポーネント本体を再実行するため、新しいオブジェクトが作られると、レンダーのたびにフォームの初期値が設定されます。その結果、変更のないフォームはリセットされ、変更済みのフォームでは警告が表示されます。useMemo でメモ化するか、定数をモジュールスコープに置くか、クエリデータを直接渡してください(sourceValues: query.data — これはすでに安定しています)。ここではゲッターは役に立たないどころか、かえって問題になります。アダプターはマウント時にゲッターを一度だけ取得するため、React の状態や props に基づくゲッターは初回レンダーのクロージャーで固定され、更新を一切反映できません。

  • ソースの読み込み中は undefined を返します(例:編集対象のエンティティの取得が保留中の場合)。これにより、後で上書きする必要のある誤った、または空の形状で初期値を設定するのではなく、フォームを保留状態にできます。
  • 既存のエンティティをオブジェクト全体として返します。キーごとにオプショナルチェイニングを使った { name: entity?.name, age: entity?.age, … } を組み立てないでください。エンティティがスキーマと一致しない場合は、差異のあるフィールドだけをスプレッドして上書きします。return { ...entity, tags: entity.tags ?? [] }
  • 意味のあるデフォルト値がないフィールドには null を使います。型が文字列や数値だからといって、'' や 0 を使わないでください。NullableDeep により、すべてのフィールドで null が有効になります。空文字列は「ユーザーが何も入力しなかった」という実際の値であり、「未操作」とは異なります。

フォームハンドル

メンバー型説明
form.dataNullableDeep<T> | undefinedリアクティブです。直接読み取り、書き込みます(form.data.x = …)。保留中は undefined です。
form.fieldsアクセサーツリーたどってから .$use() を呼び出します
form.isDirtyboolean変更があったかどうか(編集回数 ≠ 0)。submit 成功時または reset によりリセットされます
form.isChangedboolean現在のデータと sourceValues をディープイコールで比較します(元の値に戻すと false になります)
form.isLoadingboolean送信中またはソース値が保留中
form.isDisabledboolean読み込み中、または disabled オプションが指定されている場合
form.errorsIssue[] | undefinedすべての検証エラー。ない場合は undefined
form.submit()() => Promise<{success:boolean}>すべてを検証し、submit を実行します
form.reset()() => voidsourceValues の値に戻します
form.hooksフック対応hook/hookOnce/addHooks

フィールドアクセサー

form.fields.user.email.$use()        // nested object
form.fields.tags.at(0).$use()        // array item (negative index ok: .at(-1))
form.fields.tags.$use()              // the array field itself
for (const item of form.fields.tags) item.$use()  // iterate (reactive)
form.fields.tags.delete(field.key)   // remove array item by its key (see below)
form.fields.$use()                   // root field = whole form value
form.fields['a.b'].$use()            // keys containing dots work (auto-escaped)

配列やインデックスが存在する前でも、.at(i) アクセサーは存在します。データが届くまでは .$use().value は null です。.delete(key) は配列アイテムのキー(items[2])を対象にする必要があります。ネストされたプロパティを対象にすると、エラーが発生します。

FormField($use() の戻り値)

メンバー説明
field.valueフィールドの NullableDeep 値を持つ読み取り専用 ref です。ネストされたオブジェクトのプロパティは引き続き書き込み可能で(書き込みは form.data に反映されます)、配列には .push を使えます。
field.handleChange(v)値を設定し、検証を行い、変更済みとしてマークして、フィールド変更フックを発火します
field.handleBlur()フィールドが編集されている場合に検証します(入力の blur 時に使用します)
field.reset()このフィールドをソース値に戻します
field.errorsstring[] | undefined — このフィールド(およびネストされた未割り当てのエラー)のメッセージ
field.schemaSchemaMeta — スキーマから取得した required、title、min/maxLength、minimum/maximum など(パターンを参照)
field.disabled / field.isPendingフォームの状態を反映します
field.isDirty / field.isChangedフィールド単位の値で、フォームと同じ意味です
field.pathドット/角括弧形式のパス文字列
field.key安定した一意の ID(path@timestamp-rand)。リストの :key および array.delete() に使用します
field.modelフレームワークバインディング — Vue:書き込み可能な computed(v-model)。React:{ value, onUpdate }。フレームワークを参照してください。
field.$()フィールドからアクセサーツリーを取得します($use したフィールドの子要素にアクセスするため)

2 つの書き込み方法(同等)

  • field.handleChange(newValue) — 明示的な方法で、入力ハンドラーが呼び出します。
  • form.data.path = newValue — 直接リアクティブに変更する方法です。on-change が変更を検知し、同じバリデーションを実行します。

どちらもフォームを変更済みにし、再バリデーションを実行します。コンポーネントでは handleChange/model を使用してください。テストやエフェクトでは直接書き込むと便利です。

バリデーションが実行されるタイミング

スキーマ全体を検証し(フィールドごとの検証ではありません)、その後、各フィールドのパスに該当する問題を抽出します。

  • 送信時 — 常に実行します。
  • 変更時 — フィールドにすでにエラーがある場合のみ実行します(エラーを修正すると、その場で消去されます)。
  • フォーカス離脱時 — フィールドが編集済みの場合に実行します。

isLoading 中はエラーが消去されます。また、フィールドは、まだ $use されていないネストしたパスのバリデーション問題も表示します。

注意点

  • 読み書き時は nullable、送信時は non-null です。 UI で field.value が non-null だと仮定しないでください。
  • バインドには $use() が必要です。 アクセサーだけでは何も動作しないパスです。React では、$use() によって useForm を所有するコンポーネントが再レンダリングされます。フィールドの prop を読み取る子コンポーネントを再レンダリングするには、useField(field) または FormFieldMemo が必要です(フレームワークを参照してください)。
  • sourceValues は変更済みのフォームを上書きしません(仕様です)。強制的に反映する必要がある場合は、先にリセットしてください。
  • delete(key) には配列項目のキーが必要です。 handleChange で配列を置き換えると field.key が変わります(キャッシュがクリアされます)。
  • React/Vue アプリでは -core からインポートしないでください。再レンダリング/model との連携が失われます。

詳細リファレンス

  • フレームワークごとの詳細(React の再レンダリング/useField/FormFieldMemo、Vue の v-model/useFormHandles、core の単独使用)→ reference/frameworks.md を参照してください。
  • オプション機能 — 判別可能なユニオン($use({discriminator}))、値の変換($use({translate}))、field.schema のメタデータ、フック → reference/patterns.md を参照してください。

コメント