📝

Superforms v2 with Formsnap and Valibot

2024/05/31に公開

はじめに

こんにちは、株式会社Liquitousのエンジニアのかずうみ(@Kazuumi_n)です。

SvelteKitでいい感じのフォームを実装するために、これらのライブラリについて紹介していきます。

ハンズオン

なお、今回は紹介しながらハンズオンしていきます。

  • SvelteKit初期化
  • TailwindCSSインストール

以上の操作を実施済みの環境で行なっていくのでお手持ちの環境でもそのまま実行できると思います。
https://github.com/KazuumiN/sveltekit-superforms-valibot-formsnap-demo/tree/initial

質問などあれば遠慮なくコメントでもSvelte Japan Discordの#helpチャンネルでもご質問ください!

Superformsについて

SuperformsはSvelteKitに最適化されたフォームライブラリです。値を検証してエラーを処理して、、という面倒なフォーム実装を楽にしてくれます。

今回は詳しくは先人の資料に丸投げするのでぜひご覧ください。
https://zenn.dev/ryoppippi/articles/aea8dcbc21c39e
https://speakerdeck.com/kubotak/superformsben-fan-tou-ru-defen-katutaliang-satohamaridokoro

2024年2月に、Superformsのv2がリリースされました。旧バージョンでは値のバリデーション検証にZodしか使えなかったのに対し、v2からは他のバリデーションライブラリも使えるようになりました。

この記事ではValibotというバリデーションライブラリを使っていきます。

Valibotについて

ValibotはJavaScriptのバリデーションライブラリです。入力した値が、事前に定義したスキーマに一致するかどうかを検証できます。

Valibotは比較的新しいライブラリであるため、先行するバリデーションライブラリの利点を参考にしながら、バンドルサイズを小さくするための工夫が多くなされています。

https://valibot.dev/guides/migrate-from-v0.30.0/
(記事執筆時点でも更なるバンドルサイズ削減のアップデートが計画されている)

これから実際に使っていきます。

SuperformsとValibotを組み合わせてフォームを作る

今回は、新規アカウント作成とアンケートが組み合わさったUIを作ってみます。

  1. まず、必要なパッケージをインストールします。
pnpm i -D sveltekit-superforms valibot@0.30.0
  1. 次に、スキーマを定義します。
src/lib/schema.ts
import { object, string, email, minLength } from 'valibot';

export const schema = object({
 // メールアドレスの形式であることを検証
 email: string([email('メールアドレスの形式で入力してください')]),
 // 8文字以上の文字列であることを検証
 password: string([minLength(8, '8文字以上で入力してください')])
});
  1. load関数でフォームを初期化する
src/routes/superforms/+page.server.ts
import { superValidate } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import { schema } from '$lib/schema';

export const load = (async () => {
  const form = await superValidate(valibot(schema));
  return { form };
});
  1. フォームを表示する
src/routes/superforms/+page.svelte
<script lang="ts">
 import { superForm } from 'sveltekit-superforms';
 export let data;
 const { form, errors, constraints, message } = superForm(data.form);
</script>

{#if $message}<h3>{$message}</h3>{/if}

<form method="POST">
 <label for="email">メールアドレス</label>
 <input
  type="email"
  name="email"
  aria-invalid={$errors.email ? 'true' : undefined}
  bind:value={$form.email}
  {...$constraints.email}
 />
 <!-- emailフィールドにエラーがあれば表示する -->
 {#if $errors.email}<span class="text-red-500">{$errors.email}</span>{/if}

 <label for="password">パスワード</label>
 <input
  type="password"
  name="password"
  aria-invalid={$errors.password ? 'true' : undefined}
  bind:value={$form.password}
  {...$constraints.password}
 />
 <!-- passwordフィールドにエラーがあれば表示する -->
 {#if $errors.password}<span class="text-red-500">{$errors.password}</span>{/if}

 <div><button>送信する</button></div>
</form>
  1. フォームで送信された値を検証する
src/routes/superforms/+page.server.ts
import { message } from 'sveltekit-superforms';
import { fail } from '@sveltejs/kit';

export const actions = {
    default: async ({ request }) => {
        const form = await superValidate(request, valibot(schema));

        if (!form.valid) {
            // ここでformを返すだけでok
            return fail(400, { form });
        }

        console.log(form.data)

        // Display a success status message
        return message(form, '送信成功しました');
    }
};

これで、フォームを実装することができました。pnpm devで開発サーバーを実行し、localhost:5173/superformsにアクセスすると実際に値が検証されて、スキーマに一致しないフィールドは画面上へ、スキーマに一致すれば開発サーバーのコンソールに入力した情報が表示されます。

Formsnap

Formsnapは、Superformsをベースに、より扱いやすくしたラッパーのようなライブラリです。

先ほど+page.svelteを書いただけでもかなりの量の共通ボイラープレートを書くことになりましたが、本来アクセシビリティを考慮するとさらに記述量が増えます。

Formsnapを使うことで、その記述量を劇的に減らすことができます。

Formsnapを組み合わせる

  1. Formsnapをインストールする
pnpm install formsnap
  1. Formsnapでフロント側を実装する
src/routes/formsnap/+page.svelte
<script lang="ts">
 import { superForm } from 'sveltekit-superforms';
 import { Field, Control, Label, Description, FieldErrors } from 'formsnap';
 import { valibotClient } from 'sveltekit-superforms/adapters';
 import { schema } from '$lib/schema';

 export let data;

 const form = superForm(data.form, {
  validators: valibotClient(schema)
 });
 const { form: formData, message, enhance } = form;
</script>

{#if $message}<h3>{$message}</h3>{/if}

<form use:enhance class="mx-auto flex max-w-md flex-col" method="POST">
 <Field {form} name="email">
  <Control let:attrs>
   <Label>メールアドレス</Label>
   <input {...attrs} type="email" bind:value={$formData.email} />
  </Control>
  <Description>メールアドレスを入力してください</Description>
  <FieldErrors />
 </Field>
 <Field {form} name="password">
  <Control let:attrs>
   <Label>パスワード</Label>
   <input {...attrs} type="password" bind:value={$formData.password} />
  </Control>
  <Description>パスワードを入力してください</Description>
  <FieldErrors />
 </Field>

 <button>送信</button>
</form>
  1. サーバーサイドは、先ほどの工程で使ったコードと同様
同様のため省略
src/routes/formsnap/+page.server.ts
import { superValidate } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import { schema } from '$lib/schema';

export const load = (async () => {
    const form = await superValidate(valibot(schema));
    return { form };
});

import { message } from 'sveltekit-superforms';
import { fail } from '@sveltejs/kit';

export const actions = {
    default: async ({ request }) => {
        const form = await superValidate(request, valibot(schema));

        if (!form.valid) {
            // ここでformを返すだけでok
            return fail(400, { form });
        }

        console.log(form.data)

        // Display a success status message
        return message(form, '送信成功しました');
    }
};

いかがでしょうか。似通ったコードを書く必要がかなり減ったと思います。
個人的には、次のshadcn-svelteも合わせるとより恩恵を得られると感じます。

shadcn-svelte

shadcn-svelteは、洗練されてカスタマイズ性も高いSvelte用のUIコンポーネント集です。
Bits UIというヘッドレスUIコンポーネントをベースに開発されています。

shadcn-svelteを組み合わせる

  1. shadcn-svelteの導入
    ここでは詳細を書きません、以下の公式ページに従ってください。
    https://www.shadcn-svelte.com/docs/installation/sveltekit#setup-path-aliases

  2. shadcn-svelteでformを追加する

npx shadcn-svelte add form
  1. shadcn-svelteでフロント側を実装する
    (Formsnap版をコピーした上で差分を表示します。)
src/routes/shadcn-svelte/+page.svelte
+ import { Field, Control, Label, Description, FieldErrors, Button } from '$components/ui/form';
- import { Field, Control, Label, Description, FieldErrors } from 'formsnap';

/// 略

+ <Button>送信</Button>
- <button>送信</button>
  1. サーバーサイドは、先ほどの工程で使ったコードと同様
同様のため省略
src/routes/formsnap/+page.server.ts
import { superValidate } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import { schema } from '$lib/schema';

export const load = (async () => {
    const form = await superValidate(valibot(schema));
    return { form };
});

import { message } from 'sveltekit-superforms';
import { fail } from '@sveltejs/kit';

export const actions = {
    default: async ({ request }) => {
        const form = await superValidate(request, valibot(schema));

        if (!form.valid) {
            // ここでformを返すだけでok
            return fail(400, { form });
        }

        console.log(form.data)

        // Display a success status message
        return message(form, '送信成功しました');
    }
};

項目2を見ていただくと分かるように、Formsnapからshadcn-svelteへの変更はとてもスムーズです。それもそのはず、shadcn-svelteの作者とFormsnapの作者は同じ人で、shadcn-svelteのformコンポーネントはFormsnapをベースに作られています。
src/lib/components/ui/form/配下のsvelteコンポーネントを直接確認・編集することもできます。

終わりに

この内容はSvelte Japan Online Meetup #3で話した内容を記事にしたものでした(アーカイブはこちら)!

ぜひ皆さんもSvelteKitのSuperformsに、Formsnapやshadcn-svelteを試してみてください!
記事に関連してわからないことがあれば、コメントでもSvelte Japan Discordの#helpチャンネルでもご質問いただけると幸いです。

株式会社Liquitous

Discussion