🚀

FlutterとSpearlyでHTMLなし・サーバーレスのお問い合わせフォームを作る

2022/10/09に公開

はじめに

自社サイトに埋め込んでいるお問い合わせフォームですが、iframe経由で運用するのがだいぶ辛くなってきました。
Flutter Webで作るのをやめれば問題は一気に解決するのですが、それを辞めるわけにはいかない(後述)ので、今回はFlutter Webの標準機能でお問い合わせフォームを作り変えることにしました。

今回も、Spearlyを利用するのでHTMLレス・サーバーレスになります。

https://cms.spearly.com/

この際なので、 microCMSでの実装も考えましたが、

  • フォーム作成がGoogleフォームやformrun、Spearlyと比べて直感的でない
  • メール通知を自前の実装する必要がある

という点から諦めました。逆にこの点が気にならない人はmicroCMSでフォームを作成しても良いかも知れません。

https://blog.microcms.io/nuxt-contact-zapier/

流れはざっと以下の通りです。

  1. Spearly CMSでフォームを作成する
  2. フォームに合わせてWidgetを生成する
  3. ボタン押下時にSpearlyのAPI経由でフォームを送信する

Spearly CMSでフォームを作成

公式サイトに従ってフォームを作ります。
今回は「API利用」で始めましょう。

https://docs.spearly.com/cms/tutorial/c-IfPNWbxdAVY2Q7JRqoh8

フォームに合わせてWidgetを生成する

作成したフォームに合わせてWidgetを生成していきます。

「埋め込みプレビュー」の画面からフォームをHTMLで生成したときの画面が表示されるので、参考にすると良いでしょう。

Flutterで組み上げるので、HTMLを書くより早く組み上がることでしょう。

とりあえずは簡素なもので作りますが、希望に応じてバリデーションを設置してあげるのもよいでしょう。
今では TextFormField のサンプル集を出している人がたくさんいるので、ぜひ活用させていただきましょう!

https://zenn.dev/pressedkonbu/articles/copy-paste-text-form-field

https://qiita.com/OzWay_Jin/items/60c90ff297aec4ac743c

ここに関してはFlutterでUIを組み上げているだけなので特別な知識はいりません。

下記のサンプルはいつものカウンターアプリを書きかえています。
長くなるので、シンプルな構成で書いてます。

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    final formKey = GlobalKey<FormState>();

    final nameFieldController = TextEditingController();
    final emailFieldController = TextEditingController();
    final contentFieldController = TextEditingController();

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Form(
        key: formKey,
        autovalidateMode: AutovalidateMode.disabled,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextFormField(
              decoration: const InputDecoration(
                labelText: 'お名前',
              ),
              controller: nameFieldController,
            ),
            TextFormField(
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(
                labelText: 'メールアドレス',
              ),
              controller: emailFieldController,
            ),
            TextFormField(
              minLines: 5,
              maxLines: 10,
              keyboardType: TextInputType.multiline,
              decoration: const InputDecoration(
                labelText: 'お問い合わせ内容',
              ),
              controller: contentFieldController,
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16),
              child: ElevatedButton(
                onPressed: () async {
                  // ボタン内のアクションは後で指定します
                },
                child: const Padding(
                  padding: EdgeInsets.all(10),
                  child: Text(
                    '送信',
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}  

ボタン押下時にSpearlyのAPI経由でフォームを送信する

APIにPOSTリクエストを送る必要があるので、httpパッケージを導入します。

https://pub.dev/packages/http

Spearlyの「埋め込み方法」からformのidを取得して、 form_version_id にセットします。

この form_version_id はフォームの内容を変更するたびに更新され、それぞれのバージョンごとにデータを送信できるので、うっかり古いフォーム(チェックボックスを新しく追加した、入力項目名を変更した等)を使ったままのページがあっても問題なく動作するようになっているみたいです。

final body = {
  // Spearlyのページからform_version_idを取得します
  'form_version_id': 0,
  'fields': {
    'name': nameFieldController.text,
    'email': emailFieldController.text,
    'content': contentFieldController.text
  }
};

ドキュメントに沿ってAPIキーの生成を行います。

https://docs.spearly.com/cms/reference

final body = {
  // Spearlyのページからform_version_idを取得します
  'form_version_id': 0,
  'fields': {
    'name': nameFieldController.text,
    'email': emailFieldController.text,
    'content': contentFieldController.text
  }
};
final jsonBody = jsonEncode(body);
final res = await http.post(
    Uri.parse('https://API.spearly.com/form_answers'),
    headers: {
      // SpearlyのページからAPIキーを取得します
      'Authorization': 'Bearer APIkey',
      'Accept': 'application/vnd.spearly.v2+json',
      'Content-Type': 'application/json'
    },
    body: jsonBody);

APIキーをアプリ内に載せたくない人は、flutter_dotenvパッケージを使うと良いでしょう。

https://pub.dev/packages/flutter_dotenv

そうして書いていったコードが次のようになります。
今回も長くなるので、ボタンの中身のみ書いています。

// 略
ElevatedButton(
  onPressed: () async {
    // APIでのフォーム送信 ここから
    final body = {
      // Spearlyのページからform_version_idを取得します
      'form_version_id': 0,
      'fields': {
        'name': nameFieldController.text,
        'email': emailFieldController.text,
        'content': contentFieldController.text
      }
    };
    final jsonBody = jsonEncode(body);
    final res = await http.post(
        Uri.parse('https://API.spearly.com/form_answers'),
        headers: {
          // SpearlyのページからAPIキーを取得します
          'Authorization': 'Bearer APIkey',
          'Accept': 'application/vnd.spearly.v2+json',
          'Content-Type': 'application/json'
        },
        body: jsonBody);
    // APIでのフォーム送信 ここまで

    if (res.statusCode != 201) {
      return;
    }

    await showDialog<void>(
      context: context,
      builder: (_) {
        return AlertDialog(
          title: const Text(
            '送信成功',
          ),
          content: const Text(
            'お問い合わせありがとうございます',
          ),
          actions: <Widget>[
            ElevatedButton(
              child: const Text(
                'OK',
              ),
              onPressed: () => Navigator.pop(context),
            ),
          ],
        );
      },
    );
  },
  child: const Padding(
    padding: EdgeInsets.all(10),
    child: Text(
      '送信',
    ),
  ),
),
// 略

送信成功後のダイアログを出してますが、ここも好きに組み替えて構いません。

完成品

そうして組み上げて行ったコードと実際に動かしているサイトが以下のとおりです。

サンプルコード

https://github.com/qst-exe/flutter-form/blob/main/lib/main.dart

利用しているパッケージはiOS・Androidでも使えるものなので、アプリに組み込むことも可能です。

というか、Flutterはアプリ用のフレームワークなので、そちらに使うのが最適でしょう。
サンプルは StatefulWidget を使ってますが、当然Riverpodなどの状態管理ライブラリも使えます。
APIにSpearlyを使っているだけなので。

作ったサイト

https://hhg-exe.jp/

こっちはちゃんとバリデーションとかも入ってます。
(ただイタズラの問い合わせはやめてね)

制作の背景

そもそもなぜiframeから書き換えたのか、他の候補は考えなかったのかという背景は以下のとおりです。

  1. Flutterメインで仕事をしているので、サイトもFlutter Web製のものにしたかった
  2. Flutter上でiframe経由でフォームを生成するとパフォーマンスが悪い
  3. Cloud Functionsを書くのもしんどい

Flutterメインで仕事をしているので、サイトもFlutter Web製のものにしたかった

サイトを作るだけなら、WordPressやWix、素のHTML/CSS、Nuxtなんかで作成するのが良いのは十分承知の上です。

HTMLで作ればWidgetのようなコンテンツを自動生成してくれるJSライブラリが使えますし、WordPressならプラグインを追加すれば簡単にお問い合わせフォームが作れます。

https://docs.spearly.com/cms/manual/c-HnRfaNzB1UYwGbKuIcJO

https://ja.wordpress.org/plugins/contact-form-7/

しかし、Flutterのお仕事を受けることが多い都合上、Flutterでできることを実験的に色々見せたいところがあったので、Flutter Webで製作しました。
SEOもそんなに強くないので、よほどの理由がない限りはFlutterで静的サイトはまだ作らないほうが良いです。

Flutter上でiframe経由でフォームを生成するとパフォーマンスが悪い

そもそもFlutter Web上でiframeを使うケースが稀なためかあまりパフォーマンスがよくありません。
iframeを置いた位置にスクロールが若干引っかかる感じが出てきてしまいます。

Cloud Functionsを書くのもしんどい

今回のサイトはFirebase Hostingでホスティングしているため、Cloud Functionsも簡単に(実質無料で)利用できます。
Firestoreを使えば比較的簡単にフォームのAPIを作成できるでしょう。

ただし、チョットめんどくさいなという気持ちが勝ったので、この案はなしになりました。

おまけ(通知について)

お問い合わせフォームから連絡があった際の通知について、

  • メールはSpearly内で設定したら自動で送ってくれるのでSpearly内で設定
  • Slack通知はWebhookが使えるので自分で作成

するようにしました

https://zenn.dev/qst/articles/63d32a5516ffcf

最後に

以上のような流れでFlutterのWidget製のサーバーレスのお問い合わせフォームを作ることができました。

今回は取り上げてないですが、チェックボックスやラジオボタン、ファイルアップロードにも対応しているようなので、Widgetを組んでSpearlyとの連携の設定をするだけで、簡単にお問い合わせフォームが作れそうです。

個人的には

  • フォームの更新に応じてWidgetを自動生成したい
  • 開発のヒアリングシートを別ページとしてFlutter Web製にしたい

という気持ちがあるので、気分が乗ったらやります。

Discussion