Googleフォームをカスタマイズする - フォームに管理用のパラメータを自動挿入する

# Googleフォームは超有能
[「Googleフォーム」](https://docs.google.com/forms/)は、チェックボックスは普通に必須にできるし、最近は項目に画像の追加もできるので、使い勝手としては悪くない。むしろ十分。めちゃくちゃ有能。

しかし、「Googleフォーム」の使用が前提で **「Googleフォーム」にはない機能要求**がある場合は、どんな工夫ができるのか。

# Googleフォームでやりたいこと
商品の問い合わせと仮定した場合、回答側としては、商品名が入っていた方がスムーズなやり取りができる。
商品詳細ページから遷移した時は、商品名やIDが自動挿入されていると便利だなあ…。

ということで、Googleフォームに対してやりたいこと。

1. 商品IDを**自動挿入**したい
1. 商品IDは**URLから取得**したい
1. 商品IDは**編集不可**にしたい

### あらかじめ入力済みにしておくことはできる
『1』については、固定テキストなら「**事前入力したURLを取得する**」機能が使える。
固定なので、商品別に作りたくなると、ちょっと死んじゃうかもしれない。

動的な挿入はこちらが参考になる → [GoogleフォームにURLパラメータ使って初期値を指定する方法 | なかちょんブログ](https://blog.nakachon.com/2016/12/22/how-to-add-url-parameter-for-google-form/)
ほぼやりたいことだが、今回は参考にしなかった[^参考にしなかった理由]。 ~~いつかやる。~~
[^参考にしなかった理由]:この記事の元ネタでは「ページタイトル」が存在しなかったから。

**↓やりました。**
「GoogleフォームにURLパラメータ使って初期値を指定する方法」はWPのプラグインだったので、こちらはjQueryで対応。

https://qiita.com/mrd-takahashi/items/22919a98e23949e5e5ed

### 運用でカバーは絶対にイヤ ♪
人間には頑張り切れる事とできない事がある:japanese_ogre:

商品IDを**どこかから取得**したいし、**自動挿入**された商品IDは**誤って削除できない**ようにしたい。
これらの希望をを叶えるためには、ごにょごにょしないと無理(2022年5月現在)。

…ということで、3つの要件を同時に満たすために、**フォーム部分はHTMLを別に作る**事にする。
Googleフォームをカスタマイズする記事はかなりの数存在するので、それらを参考にすれば難しいことではない（はず）。

# Googleフォームをカスタマイズする
GASを使ってスプレットシートからフォームを作る方法もあるようですが、今回は通常通りに作ります。

https://qiita.com/__chicken__/items/f063520721e96a74cdb9

作業の手順は下記の通り。
1. **Googleフォームを作る**
1. **Googleフォームから```form action```と```name```を写し取る**
1. **フォームと送信完了画面のHTMLを作る**
1. **フォームHTMLからGoogleフォームにデータが送信できているか確認**
1. **フォームHTMLにごにょして送信完了画面に遷移させる**

送信完了画面はGoogleフォームのデフォルトを使うこともできるし、フォームデザインに合わせたいということであれば、HTMLを用意すれば良いと思います。

## Googleフォームを作る
Googleフォームはこんな感じ → [フォームをカスタマイズしたい！](https://forms.gle/vrKjLGH35CBR22vY6)
項目はなんとなくなので、大した意味はありません。

ここで重要なのは```form action```と各項目の```name```をゲットすること。

```html:index.html
<form action="https://docs.google.com/forms/u/0/d/e/xxxxxxxxxx" method="post" id="contact">
```
GoogleフォームとHTMLを紐づけた後、各項目を紐づけます。

```name```は```entry.xxxxxx```という感じで数字との組み合わせです。
Webデベロッパーツールで検索すればいいよというアドバイスが多いのですが、それでも見つけるのに骨が折れる（自分調べ）。
ひとつづつ項目を検索するのは面倒なので、実際に選択・入力して```name```を確認します。
![Webデベロッパーツール](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44795/fed33bbd-3c90-0111-03e7-10f00350b370.jpeg)
記事内にGoogleフォームのURLがあるので、モザイク処理する甲斐もないんですが、まあ一応。

今回はチェックボックスと連動する入力フォームがあるので、『あなたが習得したいのは？（複数回答可）』の項目は「その他」を選択してテキストを入力しました。
この部分は他の```name```や```value```と違って書き方が特殊なので、あえて選びます。

```html:index.html
<div class="customForm_desc_comments">
	<input type="checkbox" id="otherFavorite" name="entry.xxxxxx" value="__other_option__">
	<label for="otherFavorite">その他</label>
	<div class="customForm_desc_comments_input">
		<input type="text" name="entry.xxxxxx.other_option_response">
	</div><!-- /.customForm_desc_comments_input -->
</div><!-- /.customForm_desc_comments -->
```
チェックボックスの選択項目の```name```は同じ内容、```value```は項目名ですが、チェックボックスとそれに連動する項目の書き方はこんな感じ。
この辺りは、Googleフォームのお作法通りに写し取ると間違いありません。

:::note alert
このフォームは「結果の概要を表示する」をONにしているので回答結果も見えちゃいます。
今のところテスト用にオープンにしていますが、回答にはくれぐれもご注意を :relieved:
:::

## HTMLでフォーム画面を作る
### バリデーションが走るようにする

最近のHTMLは```required```を書くと簡単なバリデーションが走ります。

```html:index.html
<form action="#" method="post">
	<dl class="customForm">
		<dt class="customForm_term">
			あなたの性別を教えてください<span class="customForm_term_requiredMark">*</span>
		</dt>
		<dd class="customForm_desc customForm_desc-gender">
			<input type="radio" id="man" name="" value="男性" required>
			<label for="man">男性</label>
			<input type="radio" id="woman" name="" value="女性">
			<label for="woman">女性</label>
			<input type="radio" id="etc" name="" value="その他">
			<label for="etc">その他</label>
		</dd>
	</dl>
	<input id="submit" type="submit" value="送信する">
</form>
```
エラーテキストはブラウザによって微妙に違います。
![ブラウザごとのエラーテキスト例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44795/be9fedec-cc5f-3578-7e06-b878eb6a8e0d.jpeg)
チェックボックスにも使えますが、**いくつかある項目の最低1つを選ぶ**という内容には使えません。
今回の場合、『あなたが習得したいのは?（複数回答可）』には```required```によるバリデーションがかけられないので、これ以上の仕込みをせずに運用すると次のような挙動になります。

:::note warn
バリデーションが走らない時に起こること
1. 完了画面はGoogleフォームのもの → Googleフォーム本体に遷移してエラーが表示される
2. 完了画面はカスタマイズしたもの → **正常送信された風に見える**
:::

ウルトラスーパー完全無欠の見栄っ張りでもない限り、『1』のようにGoogleフォームに遷移しちゃっても「（無料で使っているし）まあ仕方ないか」と諦めもつく（商品IDは取れていない可能性はある）。
対して『2』はとってもまずい。
**正常に送信された風に見える**だけで、実際は送信されていません。

これを防ぐために、HTMLの```required```は使わず、[jQuery Validation Engine](https://github.com/posabsolute/jQuery-Validation-Engine)を使います。

https://qiita.com/mrd-takahashi/items/524b11440c5763fbef52

そう言えば、↑では細かい指定方法は書いていなかったのだった。
```html:index.html
<dt class="customForm_term">
	あなたが習得したいのは?（複数回答可）<span class="customForm_term_requiredMark">*</span>
</dt>
<dd class="customForm_desc customForm_desc-leastFavorite">
	<div class="customForm_desc_wrap customForm_desc_wrap-favorite">
		<input type="checkbox" id ="favorite01" name="entry.xxxxxx" value="CSS" class="validate[minCheckbox[1]]">
		<label for="favorite01">CSS</label>

		<input type="checkbox" id ="favorite02" name="entry.xxxxxx" value="HTML" class="validate[minCheckbox[1]]">
		<label for="favorite02">HTML</label>
						︙
		<input type="checkbox" id ="favorite21" name="entry.xxxxxx" value="この中にはない" class="validate[minCheckbox[1]]">
		<label for="favorite21">この中にはない</label>

		<div class="customForm_desc_comments">
			<input type="checkbox" id="otherFavorite" name="entry.xxxxxx" value="__other_option__" class="validate[minCheckbox[1]]">
			<label for="otherFavorite">その他</label>
			<div class="customForm_desc_comments_input">
				<input type="text" name="entry.xxxxxx.other_option_response" class="validate[condRequired[otherFavorite]]" disabled="disabled">
			</div><!-- /.customForm_desc_comments_input -->
		</div><!-- /.customForm_desc_comments -->

	</div><!-- /.customForm_desc_wrap -->
</dd><!-- /.customForm_desc -->
```
複数のチェックボックスから選択するので、**n以上選択する**を必須にします。
今回はひとつ以上でOKなので、```class="validate[minCheckbox[1]]"```という書き方をしています。
```maxCheckbox```という指定もできるので、選択できる最大数もいける。

チェックボックスと連動するテキストフォームに対しては、チェックボックスの```id="otherFavorite"```を使い、```class="validate[condRequired[otherFavorite]]"```という感じで関連づけます。

小回りが効くプラグインなので、設定方法については[公式ドキュメント](https://github.com/posabsolute/jQuery-Validation-Engine)をどうぞ。

### URLからパラメータを取得してフォームに自動挿入する
URLに商品IDのパラメータを付け、その値を取得してフォームに自動挿入します。
今回はURLに```?parameters=ItemID2022```というパラメータを付けます。
この部分はユーザに編集されたくないので```readonly```を付けます。
```html:index.html
<dt class="customForm_term">
	送信されるパラメーター
</dt>
<dd class="customForm_desc">
	<input type="input" id="inputParam" name="entry.xxxxxx" value="" readonly>
</dd>
```
```Javascript:JavaScript
const param = location.search //URLからパラメータを取得する
document.getElementById("inputParam").value = param.replace('?parameters=','');
```
jQueryを使っているのでこんな感じです。
```input id="inputParam"```があるフォームに挿入されます。```?parameters=```が邪魔なのでreplaceを使って削除しています。
[カスタマイズHTMLの確認 / パラメータ：ItemID2022 (外部サイト)](https://www.m-r-design.com/86e37d9e0e5213d4d164/index.html?parameters=ItemID2022)

日本語にしたい時は、取得する際にデコードすればいけます。
```Javascript:JavaScript
const param = location.search //パラメータを取得する
const paramDecode = decodeURI(param); //パラメータをデコードする
document.getElementById("inputParam").value = paramDecode.replace('?parameters=','');

```
[カスタマイズHTMLの確認 / パラメータ：商品名は日本語(外部サイト)](https://www.m-r-design.com/86e37d9e0e5213d4d164/index.html?parameters=商品名は日本語)

自動挿入したパラメータを見せたくない時は```input type="input"```から```input type="hidden"```にします。
レイアウトから要素を除去してしまう```display: none;```は**データが送信されないので使いません**。
[display - CSS: カスケーディングスタイルシート | MDN ](https://developer.mozilla.org/ja/docs/Web/CSS/display)

今回はパラメータは1つですが、複数つけることも可能。

ここまで作って、送信テストをしてみます。
Goggleフォームの送信完了画面が表示され、送信したデータが蓄積されていればOKです :tada:

送信完了画面もオリジナルにしたいよ、という場合は次に進む。

## 送信完了画面を作る
この記事用に起こしているので、非常に簡素なものです。
結果をオープンにしているので、そのリンクがついてるだけ。
```html:complete.html
<main class="wrap">
	<article class="contents">
		<h1 class="contents_title">
			フォームをカスタマイズしたい！ 挙動テスト
		</h1>
		<div class="thanks">
			<p class="thanks_desc">
				入力内容を送信しました！
			</p>
		</div>
		<input type="button" class="contents_btnClose" value="結果の概要を表示する" onClick="window.open('https://docs.google.com/forms/d/xxxxxx')">
	</article>
</main>
```
データを送信しつつ、任意の場所に遷移するので、フォームのHTMLには次のようなコードを書きます。
まずは```<body>```直下に```<iframe>```を追加
```html:index.html
<body ontouchstart="">
	<iframe name="hidden_iframe" id="hidden_iframe" style="display:none;" onload="if(submitted) {window.location='complete.html';}"></iframe>
	<main>
		<article class="contents">
			<h1 class="contents_title">
				フォームをカスタマイズしたい！
			</h1>
			<div class="contents_wrap">
				<p class="contents_desc">
					Googleフォームをカスタマイズする - フォームに管理用のパラメータを自動挿入する(Qiita記事)用に作りました。<span class="contents_desc-note">※</span>
				</p>
				 ︙
```
```form```タグを下記のように変更
```diff_html:index.html
- <form action="https://docs.google.com/forms/u/0/d/e/xxxxxx" method="post" id="contact">
+ <form action="https://docs.google.com/forms/u/0/d/e/xxxxxx" method="post" id="contact" target="hidden_iframe" onsubmit="submitted=true;">
```
JavaScriptも追加
```Javascript:JavaScript
const submitted = false;
```
これで任意の送信完了画面が表示されるようになります。
# つまづいたところ
この記事の元ネタを制作していた時、ここまで作るのに執拗に送信テストを繰り返していました。
はっきりした数は覚えていませんが、内容をさまざまに変えて、ひとりで軽く300は送信したはず（3桁蓄積毎にデータ削除を数回繰り返した） :runner: :runner: :runner: :runner:  :runner:
テスト中につまづいたところがいくつかあったので、メモがわりに記載。
**正直凡ミスの数々でしかない**。

## 送信できてるはずなのにデータが蓄積されない
1. 必須項目に**バリデーション用のCLASS**を振っていなかった
1. なぜか**Googleフォームが編集途中**になっていた
1. **entry.xxxx**の番号が間違ってる

『1』はただの実装抜け漏れ、『2』『3』は制作中に項目の増減・修正等が発生して、GoogleフォームとローカルのHTML制作を行ったり来たりすると起こりやすかった。
『2』については毎回発生するわけではなく、本当にタイミングが分からなかった。「送信データ蓄積されてないなー」と思った時の原因は、ほとんどコレ。

送信完了画面を任意のものにすり替えた後に起こったので、**挙動が安定するまではGoogleの送信完了画面を使った方が**無駄な労力を使わなくて良いです。
送信完了画面も任意のものを使うと、フォームHTMLとの疎通がうまくできていなくても、しれっと**正常送信できた風を装う**ので。

## 連打できちゃうどうしよう
裏側はGoogleフォームなので、無視してもいいかな案件（自分で掃除しないからポイ捨てOKと同様の思考 :expressionless:）。
とはいえ、送信完了画面への遷移がちょっとでも遅いと感じると[^連打ユーザ]、待ちきれずに送信ボタンをめちゃくちゃ連打するユーザさんは存在します。
データはその回数分蓄積されるので、数十件送信された同じ問い合わせを弾く作業が発生する事に。

[^連打ユーザ]:今回の作り、Androidでは遷移がやや遅いようです。

そこで```pointer-events```プロパティを使って、送信ボタンを押下したらポインタータップイベントを無効にするようにしました。
ただ、バリデーションが走ってエラーになった時も同じ仕組みが働くので、何かのタイミングで復帰させないと必須項目を入力しても内容を送信できないwww
```javascript:Javascript
//連打緩和
$("#submit").on("click", function () {
	//「送信」ボタンを押下するとボタンが半透明になり、ポインタータップイベントが無効になる
	$(this).css({ opacity: " .5", "pointer-events": "none" });
});

$("form").change(function () {
	//フォーム内の要素に変更があるとボタンは透過表示をやめ、ポインタータップイベントが有効になる
	$("#submit").css({ opacity: "1", "pointer-events": "auto" });
});
```
バリデーションが走った後、エラーが表示されている項目を選択または入力するはずなので、フォーム内の要素に変更があった時に、再び送信ボタンを押下できるようにしています。

## ヒストリーバックで再送できちゃう
連打にも関連する話で、このままだとブラウザの「戻る」ボタンでフォーム画面に戻れちゃいます。
調べてみたのですが、**SafariとFirefoxはヒストリーバックを止めることができるものの、Chromeは完全に止めることができません**。
参考URL：[Google Chromeは「戻る」ボタンで戻れない悪質なウェブサイトを駆逐する予定 - GIGAZINE](https://gigazine.net/news/20181219-google-chrome-drive-out-history-manipulation/)

Chromeの場合は**悪質なウェブサイトを駆逐する**という理由でそのような仕様になっているようです。
まあ、悪いことする人たちが一部にいるので仕方ないですね。ほんとやめてほしい。

ただ、不完全ながらヒストリーバックを止めることはできます。

参考：[Google Chromeではhistory APIを使った戻るボタンの制御ができない | 11:30 AM](https://11-30am.com/27/)
> chromeの仕様を詳細に書くと、「ページ読み込み直後に、ブラウザバックでのpopstateイベントが発火しない」ということみたいです。
> ユーザーがクリックなどの能動的なアクションを一度でも起こした後であれば発火します。
> なので、ページに飛んできたユーザーに一番最初にボタンを押してもらうようなUIを作れればこの問題は回避できるかもしれません。

こちらの追記通り、発火したりしなかったりします。
私がテストした時はほとんど発火しましたが、使用している端末などの環境次第でヒストリーバックができちゃったりします。
```Javascript:Javascript
//ヒストリーバックを止める
history.pushState(null, null, location.href);
$(window).on('popstate', function(){
	history.go(1);
});
```
完全には止められないので、もう一つ設定を入れる事にします。
```html:index.html
<form action="https://docs.google.com/forms/u/0/d/e/xxxxxx" method="post" id="contact" target="hidden_iframe" onsubmit="submitted=true;" autocomplete="off">
```
```autocomplete="off"```を追加して**自動補完機能を無効**にします。
この設定を入れたので、**ヒストリーバックで戻ってもフォーム内に情報が残りません**。

**ユーザにとって利便性があり、デフォルトでできることを止めるというのは悪手**に近いですが、いたずら防止という視点で考えると自動補完機能の無効に軍配が上がります。

住所や電話番号の入力には便利ですが、そもそも、**細かい個人情報の入力が必要になった時点でGoogleフォームは使用しない**選択になると思います。

ちなみに、Googleフォームも送信完了画面からヒストリーバックが可能ですが、戻ってもフォーム内に情報は残りません。

# HTMLを置くサーバを準備できないとき
Googleフォームのカスタマイズをする場合、HTMLを置くサーバをどこかに持っておく必要があります。
ただ、Webアプリだとサーバは不要になる様子。
参考：[Google Apps Script (GAS)で動的HTMLページを作成する方法！](https://bzbot.work/2019/04/17/gas-html/)

無料版の場合、ヘッダに下記の文字と利用規約のリンクが表示される。

> このアプリケーションは、Google ではなく、別のユーザーによって作成されたものです。

英語では
> This Application was created by another user, not by Google.

参考：[HTML Serviceで作ったWebアプリの上部に表示されるバナーについて](https://www.pre-practice.net/2018/11/html-serviceweb.html)
まあ、その通りなんですけど、若干不穏な感じしませんかねw

# Googleフォームを利用するメリット・デメリット
Googleフォーム最大のメリットは、**手軽にアンケートや問い合わせフォームを作ることができる**という事に尽きると思います。
適切に利用すれば、最大限のメリットを受けることができます。

今回のような**管理用の動的パラメータを送信内容に含めたい**という事になると、若干のカスタマイズが必要になりますが、スプレットシートでデータの集計も可能なので、小規模だったり、期間限定で使用する内容に対しては十分だと思います。
ただ、Google次第のところが大きく、フォームの仕様変更があった場合、この**カスタマイズが使用できなくなる**可能性があります。

そのため、小規模・短期利用に留めて、利便性を最大限に享受するのが、つまらない事故を防ぐ事になるかと思います。

# 参考にしたサイト
+ [Google Form をカスタマイズして、hidden フィールドを設定する | Monotalk](https://www.monotalk.xyz/blog/Customize-Google-Form-and-set-hidden-field/)
+ [Googleフォームを自由にデザインカスタマイズする方法 - Kazuki Yonemoto - Medium](https://medium.com/@yonemoto/google%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%82%92%E8%87%AA%E7%94%B1%E3%81%AB%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95-f91a617b402b)
+ [GoogleAppsScriptを使ってGoogleフォームの画面を自由にカスタマイズする！ | まさきのエンジニア図書館](https://masa-enjoy.com/gas-createform)
+ [GoogleフォームにURLパラメータ使って初期値を指定する方法 | なかちょんブログ](https://blog.nakachon.com/2016/12/22/how-to-add-url-parameter-for-google-form/)
+ [URLに含まれるGETパラメータを取得する | GRAYCODE JavaScript](https://gray-code.com/javascript/get-parameter-of-url/)
+ [Javascript でURLのパラメータを取得する方法 | WWWクリエイターズ](https://www-creators.com/archives/4463)
+ [jQueryでURLとパラメータを取得する方法：location | UX MILK](https://uxmilk.jp/9205)
+ [JavaScriptでブラウザバックを "ほぼ完全禁止" する方法 | PisukeCode - Web開発まとめ](https://pisuke-code.com/javascript-prohibit-browser-back/)
+ [ブラウザバックしても今いるページに留まらせて欲しいと言われて三千里 using React router](https://zenn.dev/marimoofficial/scraps/05338df0cd8829)
+ [Google Chromeではhistory APIを使った戻るボタンの制御ができない | 11:30 AM](https://11-30am.com/27/)