solidjsでも使えるformライブラリ「modular-forms」の紹介

この記事は、[SolidJSアドベントカレンダー 2024](https://qiita.com/advent-calendar/2024/solidjs) 9日目の記事です。

## フォームライブラリ

現状私が確認してる限りではsolidjsで使えるフォームライブラリは以下の２つです。

- modular-forms
- @solid-mediakit/form

ドキュメントが充実してたのでmodular-formsを触ってみました。

## 概要

- デフォルトではuncontrolled、controlledへの移行も簡単
- validationではzodだけでなくvalibotも対応
- ArrayやObjectの形式の構成も可能
- Actionsは残念ながら非対応
    - [FAQ](https://modularforms.dev/solid/guides/faq)の「**Does it work with SolidStart?**」参照

## 使い方

### 導入

`pnpm install modular-forms/solid` 

### フォーム実装


:::note info

getValueを `"@modular-forms/solid"` からimportしているように、react-hook-form等とは違い細かい関数はimportする必要があります。（バンドルサイズを減らすため）

:::

```tsx
import { createForm, getValues, insert, valiForm } from "@modular-forms/solid";
import * as v from "valibot";

const LoginSchema = v.object({
  username: v.string(),
  password: v.string(),
});

export default function Page() {
  const [form, { Form, Field }] = createForm<SpecialForm>({
    validate: valiForm(LoginSchema),
  });
  createEffect(() => {
    console.log(getValues(form));
  });
  return (
    <div>
      <h1>Modular Forms</h1>
      <Form onSubmit={(v)=>{console.log(v)}}>
        <Field name="username" >
          {(field, props) => <input {...props}  />}
        </Field>
        <Field name="password" >
          {(field, props) => <input type="password" {...props}  />}
        </Field>
        <button type="submit">Submit</button>
      </Form>
    </div>
  );
}

```

## その他

### Controlled

**デフォルトではuncontrolled**なのでcontrolledにする場合はfield.valueをvalueに渡す

```tsx
        <Field name="username" >
          {(field, props) => <input value={field.value} {...props}  />}
        </Field>
```

### Error表示

```tsx
        <Field name="username" >
          {(field, props) => {
            return <div>
              <input {...props} />
              <div>{field.error}</div>
            </div>;
          }}
        </Field>
```

### FieldArray

```tsx
// ↓こんな感じのフォームのとき
const LoginSchema = v.object({
  list: v.array(v.object({ value: v.string() }))
});
// ↓フォームはこうなる
        <FieldArray name="list">
          {(fieldArray) => (
            <>
              <For each={fieldArray.items}>
                {(item, index) => (
                  <div>
                    <Field name={`list.${index()}.value`}>
                      {(field, props) => <input {...props} type="text" />}
                    </Field>
                  </div>
                )}
              </For>
              <button
                type="button"
                onClick={() => insert(form, "list", { value:{ value: "" } })}
              >
                Add
              </button>
            </>
          )}
        </FieldArray>
```

### Custom  Input

必要なプロパティをcontextで引き回してあげるとシンプルに実装できる

```tsx
import {
  Field as BaseField,
  type FormStore,
  createForm,
  valiForm,
} from "@modular-forms/solid";
import { Show, createContext, useContext } from "solid-js";
import * as v from "valibot";

const LoginSchema = v.object({
  username: v.string(),
});

type LoginForm = v.InferInput<typeof LoginSchema>;

const formContext = createContext<FormStore<LoginForm>>();

const UserName = () => {
  const context = useContext(formContext);
  return (
    <Show when={context}>
      {(form) => {
        return (
          <BaseField name="username" of={form()}>
            {(field, props) => {
              return (
                <>
                  <input {...props} />
                  {field.error}
                </>
              );
            }}
          </BaseField>
        );
      }}
    </Show>
  );
};

export default function Page() {
  const [form, { Form, Field, FieldArray }] = createForm<LoginForm>({
    validate: valiForm(LoginSchema),
  });
  return (
    <div>
      <h1>Modular Forms</h1>
      <formContext.Provider value={form}>
        <Form
          onSubmit={(v) => {
            console.log(v);
          }}
        >
          <UserName />
          <button type="submit">Submit</button>
        </Form>
      </formContext.Provider>
    </div>
  );
}

```