Google Forms 完全に理解した

この記事は公開されてから2年以上経過しています。 情報が古い可能性がありますのでご注意ください。

Google Drive に Google Forms というものがあります。プログラムの知識がなくとも管理画面上の操作だけでフォームを設置出来る優れものです。この Google Forms が、何ができ、そして何ができないのか、何をどこまでカスタマイズする事ができるのか、実現可能性について思いを馳せてみるのが今回のテーマです。

TL;DR

  • デザインを変更できる(テーマを選ぶ or 自前のフォームで送信する)
    • 自前のフォームを使うことでデザインは完全にコントロールできる
    • 完了画面までコントロールするためには、一部の実装を犠牲にする必要がある(エラーを正しく検知できなくなる)
  • フォーム送信時に通知メールを送る事ができる
    • 通知するだけならば備え付けの機能でできる
    • メール内容をカスタマイズするには Google Apps Script による実装が必要
  • 自動返信メールを送ることができる
    • メールデザインや送信元アドレスにこだわらないならば備え付けの機能でできる
    • メール内容・送信元アドレスをカスタマイズするには Google Apps Script による実装が必要
      • 送信元アドレスは Gmail のエイリアスとして登録されていなければならない

デザインはどこまでカスタマイズできるか?

例えば Google Forms をWebサイト上でフォームとして利用する場合にまずはじめにやりたい事といえば、デザインの変更に違いないでしょう。Webサイトのトーンにあわせた見た目にしたかったり、ヘッダーとフッターを合わせたかったり、iframeによる埋め込みをやめたかったり。

備え付けのテーマ機能

Google Forms には「テーマをカスタマイズ」する機能が備わっており、ある程度の見た目のカスタマイズが可能です。

「テーマをカスタマイズ」機能

しかし、この機能でコントロールできるのは、「ヘッダー画像」「テーマの色」「背景色」「フォントスタイル(4択)」の4点だけで、自由自在に、とはとても言い難いです。

Google Forms のフォームを複製して直接送信する

ならば自前のフォームから直接リクエストを送ってしまえば良いのでは? という事で、Google Forms と同じ送信先( action )とフィールド( input[name] )を備えたフォームを用意します。これらの情報を抽出するために、対象となる Google Forms の送信ページを開き、適当な値を入力した上で、開発者ツールで次のように実行してみましょう。 (管理画面ではなく、回答画面ですのでご注意ください)

// 送信先を取得する
$('form[action]').action
// フィールドリストを取得する
$$('input[name^=entry.],input[name^=email]').map(input => `${input.name}: ${input.value}`)

Google Forms は、各フィールドに数値からなる id を割り当てて、その id から entry.[id] のような name を付与する仕様になっています。例外として、「メールアドレスを収集する」機能を有効にした場合は、 emailAddress という独自の name を持つフィールドが追加されます。

例えば、このようなフォームが出来上がります。

<form action="https://docs.google.com/forms/d/e/xXxxXxx_xXxxXxxX/formResponse" method="POST">
  Email: <input type="email" name="emailAddress" value="" />
  Name: <input type="text" name="entry.12345678" value="" />
  Age: <input type="text" name="entry.23456789" value="" />
  Gender: <input type="text" name="entry.34567891" value="" />
  <button type="submit">Submit</button>
</form>

出来上がったフォームで Google Forms に送信すると完了画面(「回答を記録しました。」)が表示されます。

この完了画面はURLを見てもわかるように Google Forms で提供されている物なので、スタイルはカスタマイズ出来ませんし、Webサイトへ戻る導線等もありません。 制作者は当然 「完了画面もコントロールしたい」 と考えるでしょう。

完了画面をカスタマイズしたい

完了画面(いわゆるサンクスページ)をカスタマイズするためには、そのページもセルフホスティングしなければなりません。つまり、フォームへの送信を非同期通信で行い、その完了を検知して自前のサンクスページに遷移する、といった処理を行う必要があります。方法は2つ考えられます。(他にもあるかもしれません)

  • 隠しiframeに送信する
  • Ajaxで送信する

難易度が低い順に見ていきましょう。

隠しiframeを使ったハック

formtarget に名前付きの iframe を指定する事で、ページをGoogle Formに遷移させずに通信を行い、iframe の読み込みが完了したタイミングでサンクスページの表示を行います。

例えば上で作ったフォームで実践してみると、こんな具合になるでしょう。

<iframe name="hiddenFrame" style="display: none"></iframe>

<form 
  action="https://docs.google.com/forms/d/e/xXxxXxx_xXxxXxxX/formResponse" 
  method="POST" 
  target="hiddenFrame"
>
  Email: <input type="email" name="emailAddress" value="" />
  Name: <input type="text" name="entry.12345678" value="" />
  Age: <input type="text" name="entry.23456789" value="" />
  Gender: <input type="text" name="entry.34567891" value="" />
  <button type="submit">Submit</button>
</form>
document.querySelector('iframe[name=hiddenFrame]')
  .addEventListener('load', () => {
    // フォームのリクエストを投げた iframe が読み込み完了した時点で
    // 完了画面にリダイレクトする
    location.href = 'complete.html'
  })

イベントリスナーを登録する際に <iframe>onload で直接書いていない点に注意しましょう。 iframe 要素は src 属性にリソースが指定されていようがいまいが、描画された時点で一度 load イベントが発火してしまう様なので、そのタイミングで実行されないように、後からJavaScriptで登録しています。

エラーが検知できない

ただし、このハックにはひとつ重大な落とし穴があって、 Google Forms側のエラーを検知する事が出来ません。 項目に不備がある状態で送信を行うと、Google Formはステータスコード 400 を返しつつ「見直しを必要とする質問があります」のようなメッセージを表示してきますが、 iframe はそれらを検知できず、送信が完了した物として load イベントを発火するのみです。

JavaScript(Ajax)で送信する

それでは、送信先からのレスポンスを詳細に受け取れるAjax通信を利用してはどうでしょう。とはいうものの、クロスオリジンの制約があるため、自サイトとGoogle Formとで直接通信する事は出来ません。そもそも Google Forms の送信先が Rest API として公式に提供されているものではないので、CORS対応などしているはずがありません。

GhostForm を利用する

そこで役立つのがつい先日拵えたこのWebアプリケーションです。 何をかくそうこの記事を書くためだけに生まれたといっても過言ではありません。

GhostForm - CORS endpoint only for Google Form
https://ghostform.net/

GhostForm

細かい説明は省きますが、このサービスを介して Google Forms へリクエストを送ることで、オリジンの異なるGoogle Formと通信をフロントエンドだけで行う事ができます。 サービスの使い方はサービスサイトをご覧ください。

上の例で実践してみると、こんな具合になるでしょう。

;(async function(){
  try {
    const response = await fetch('https://ghostform.net/api/v1/submit', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        url: 'https://docs.google.com/forms/d/e/xXxxXxx_xXxxXxxX/formResponse',
        data: {
          'emailAddress': 'test@example.com',
          'entry.12345678': '松風 太郎',
          'entry.23456789': '23',
          'entry.34567890': 'male'
        }
      })
    })

    if (!response.ok) {
      throw new Error('Failed to submit')
    }
    // 送信成功時の処理
    location.href = 'complete.html'
  } catch (error) {
    // 送信失敗時の処理
    alert(error.message)
  }
})()

正確にお伝えするため、一番細かに記述しないといけなさそうな Fetch API を使って書きました。もちろん axios でも ky でもお好みの方法で構いません。

やはりエラー詳細は検知できない

一見良さそうに見えるのですが、この方法にもやはり欠点があります。全体のエラーは検知できるものの、どの項目でエラーが発生しているか等の詳細なエラーを拾うことができないのです。したがって、ユーザーは何が原因で送信に失敗したのか知ることができません。

例えば、自サイト側とGoogle Form側とでバリデーションに齟齬があるような場合、その隙間を縫うような値を入力したユーザーは何度送信してもエラーが返され、何が間違っているのかわからないまま諦めてフォームを去る、などという事が起こり得るのです。

つまり、そういったケースで機会が損失されてもクリティカルにならないケース(例えばアンケートフォームなど)でのみ、この方法は使えるということになります。現在思いつく限りでは、この方法が最も自由度の高いカスタマイズ方法だと思われるので、そうでないケース(例えばお問い合わせフォームなど)ではデザインのフルカスタムは諦めるか、 Google Forms ではない他のソリューションを検討すべきだと考えます。

デザインのカスタマイズについての話題はひとまずここで締めとしましょう。

管理者宛にメールを送ることができるか?

「フォームに送信されると管理者にメールで通知が届く」のはフォームとしてごく当たり前の機能なので、もちろん Google Forms にもそのための設定が備わっています。問題は、果たして要件にあう機能かどうかです。

備え付けのメール通知機能を利用する

メール通知の設定は、「設定」ではなく「回答」タブのメニューで選択するのでご注意ください。チェックを入れるだけで、次の回答からメールで通知が来るようになります。

新しい回答についてのメール通知を受け取る

しかし、この機能は本当に通知しか送ってくれないのです。詳しい回答内容を確認するためには、メールにある「概要を表示」リンクから Google Forms の回答ページへ飛ばなければならず、しかもその回答ページを見ても統計が表示されているだけなので、「その時なされた回答」の情報を得るにはスプレッドシートに送信して閲覧するか、CSVでダウンロードする必要があります。

もし通知だけで事足りる場合はこの機能を利用すれば良いだけで簡単なのですが、そうでない場合、フォームに送信される都度回答の情報がメールで届く様にしたいのならば、もう少し頑張らなければいけません。

Google Apps Script でメールを送信する

フォームのサブミット時にカスタマイズされたメールを送るには、Google Apps Script を利用します。

まず Google Forms の管理画面で右上のメニューから「スクリプトエディタ」に遷移します。すると、「コード.gs」がエディタで開かれているはずなので、次のような感じで書き換えましょう。

function sendEmail () {
  // 受け取りメールアドレスは適宜変更してください
  const recipient = 'info@example.com'
  GmailApp.sendEmail(recipient, 'TEST MAIL', 'Hi, there')
}

利用している GmailApp について詳しくは公式ドキュメントをどうぞ。

保存をしたら、エディタ上で実行してメールが送信されるかどうか試します。初めての実行では、権限を確認するダイアログが開くので、許可をしてあげましょう。

GASの関数をエディタから実行する

テストメールは無事届きましたか?

今度は、フォームのサブミット時のトリガーに登録します。左のメニューから「トリガー」を開き、右下の「トリガーを追加」から、トリガーを追加し、「フォーム送信時」を選択して保存します。

「フォーム送信時」のトリガーに登録する

保存したら、フォームから1件サブミットしてみます。無事メールが届けば成功です。

これが Google Apps Script によるメール送信の Getting Started なるものですが、さて、これだけではデフォルトのメール通知機能の劣化版なので、フォームで送信された情報でメールをもっとカスタマイズしたいと思います。

フォームで送信された情報を活用してメール本文をつくる

いきなりコードで失礼します。上で書いた Google Apps Script を改修して、送信されたデータを抽出する処理を足したものです。

function sendEmail (event) {
	const recipient = 'test@example.com'

  // フォームで送信された情報を扱いやすいオブジェクト配列に変換
  const items = event.response.getItemResponses().map(it => {
    return {
      title: it.getItem().getTitle(),
      value: it.getResponse()
    }
  })

  // メールアドレスは getItemResponses() に含まれないので、別途追加する
  items.push({
    title: 'emailAddress',
    value: event.response.getRespondentEmail()
  })

  // 抽出した情報を JSON.stringify しただけの本文を送信
  GmailApp.sendEmail(recipient, 'TEST MAIL', JSON.stringify(items))
}

引数として渡される eventresponse: FormResponse から、送信された情報にアクセスする事ができます。利用しているAPIは FormResponseItemResponse です。それぞれ公式ドキュメントで仕様を確認してみてください。

これで試しにフォームから送信してみると、初めての実行で権限のエラーが出ると思います。(エラーはエディタの左メニューの「実行数」から閲覧する事ができます)

Exception: FormApp.getActiveForm を呼び出す権限がありません。必要な権限: (https://www.googleapis.com/auth/forms.currentonly || https://www.googleapis.com/auth/forms)  

Google Forms から情報を抽出する権限が不足しているので、新たに付与する必要があります。権限を要求された関数をコールする記述を書いて、エディタから実行します。

function sendEmail (event) {
  FormApp.getActiveForm()
}

これでダイアログが開くはずなので、許可をしてあげましょう。再度フォームから送信して、無事メールが送信されれば成功です。

この例では抽出した情報を JSON.stringify() でJSON文字列にしているだけなので、綺麗に整形された本文にしたいのなら、あとは皆様の力でなんやかんやしていただければ幸いです。と、丸投げするだけなのもアレですので、例えばこんな感じに。

function getValueByTitle (title, items) {
  const item = items.find(it => it.title === title)
  return item ? item.value : undefined
}

function createMessageBody (items) {
  return `Google Formに回答が送信されました。

-----------------------------
Name:   ${getValueByTitle('Name', items)}
Email:  ${getValueByTitle('emailAddress', items)}
Age:    ${getValueByTitle('Age', items)}
Gender: ${getValueByTitle('Gender', items)}
-----------------------------`
}

function sendEmail (event) {
  ...
  GmailApp.sendEmail(recipient, 'TEST MAIL', createMessageBody(items))
}

自動返信メールを送る事はできるか?

ユーザー向けの自動返信メールはどうかというと、管理者向けの通知メール同様に備え付けの機能があります。こちらは「回答」ではなく「設定」タブにあるのでご注意ください。

回答のコピーを回答者に送信する機能

「メールアドレスを収集する」をONにして、「回答のコピーを回答者に送信」を「常に表示」に設定すると、サブミット時に回答者にメールで回答情報のコピーが送信されるようになります。このようなHTMLメールが送られてきます。

回答のコピーのHTMLメール

いかにも「Google Forms から来ました!」と言わんばかりのメールです。むしろヘッダーでロゴが大きく自己主張していて、言わんばかりどころではなく、実際言っています。このメールで事足りるのであれば良し、そうでないのならば通知メールと同じ要領でカスタマイズしていくまでです。

Google Apps Script で自動返信メールを送る

基本的な設定方法は通知メールと同じなので省きます。今回は replyEmail という関数を作って、「フォーム送信時」のトリガーに登録します。

function replyEmail (event) {
  const recipient = event.response.getRespondentEmail()
  const messageBody = 'お問い合わせありがとうございました'
  // 送信元アドレスは適宜設定変更してください
  const fromAddress = 'noreply@example.com'

  GmailApp.sendEmail(recipient, 'TEST REPLY MAIL', messageBody, {
    from: fromAddress
  })
}

通知メール用の sendEmail と違うのは、以下2点です。

  • 宛先メールアドレスに、送信された「メールアドレス」を使用する (event.response.getRespondentEmail()
  • GmailApp.sendEmail の第4引数にオプションを渡し、送信元アドレス( From ヘッダー)を設定する

from オプションの設定は注意が必要で、このフォームを作ったGmailアカウントにエイリアスとして紐付いたアドレスのみ設定することができます。これは当然そうであるべき仕様です。 Google Workspace をご利用の方は問題ないですが、通常の Gmail を使っている場合は、送信元としたいアドレスをエイリアスに追加するために、SMTPサーバーやアカウント情報を入力しなければならないでしょう。

なお、 from のアドレスが適切でなかった場合、送信元アドレスはフォームを作ったGmailアカウントのアドレスそのものになります。

まとめ : Google Forms でどこまでやるのか

予想通り、深いところまでカスタマイズしようとすると、「Google Forms でやる必要ある?」「普通に実装した方がコストかからないんじゃない?」となりがちです。

特にデザインのカスタマイズは完全にコントロールしようとすると、機能面でコントロール不可能な箇所が生まれてしまうというジレンマがあるために採用自体が難しくなります。 そもそもデザインをフルカスタマイズするというのは、Google Forms の「自由自在にフィールドを編集できる」というせっかくの強みを投げ捨ててしまう行為に他ならないので、それをしたければ、結局フォームの機能も丸ごと自前でホスティングした方が皆が幸せになれそうですね。

では、Google Forms を使った幸せなシナリオは何かというと、極端に言えばほぼカスタマイズをせず、Google Forms を Google Forms のまま使う事になるでしょう。ありのままの姿見せるのよ。

とは言うものの、一見いじれなさそうな部分も Google Apps Script を使えば意外とカスタム出来たりするので、今回の収穫はむしろ GAS の可能性を垣間見れた事なのではないでしょうか。ひょっとすれば、これを駆使すればより良いカスタマイズ方法が模索できるやもしれません。