📝

Superforms v2 with Formsnap and Valibot

2024/05/31に公開

はじめに

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

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

ハンズオン

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

環境は、以下の操作を実施済みと想定しています。

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

上記実施済みのgitブランチ

質問などあれば遠慮なくコメントでも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は比較的新しいライブラリであるため、先行するバリデーションライブラリの利点を参考にしながら、バンドルサイズを小さくするための工夫が多くなされています。

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

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

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

  1. まず、必要なパッケージをインストールします。
pnpm i -D sveltekit-superforms valibot
  1. 次に、スキーマを定義します。
src/lib/schema.ts
import * as v from 'valibot';

export const schema = object({
 // メールアドレスの形式であることを検証
 email: v.pipe(v.string(), v.email('メールアドレスの形式で入力してください')),
 // 8文字以上の文字列であることを検証
 password: v.pipe(v.string(), v.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