はじめに
この記事は「お問い合わせフォームを作ってみよう」チュートリアルの一部であり、このチュートリアルの【確認画面の実装編】として書かれています。
実際のお問い合わせフォームの制作では、送信する前に確認画面を表示させるといった実装が一般的です。この記事では、確認画面の必要性について学び、お問い合わせフォームの値を反映させた確認画面をモーダルウィンドウで実装します。
この記事の対象者
- ウェブ制作初心者の方
- お問い合わせページを作りたい方
- JavaScript初心者の方
この記事で学べること
- 確認画面の必要性について
- モーダルウィンドウでの確認画面の実装方法
確認画面について
ウェブフォームにおいて、確認画面の実装は必ず必要なものではありません。
例えば、掲示板やニュースレターの購読フォームなど、ユーザーが迅速な送信を期待している場合はユーザー体験を損ねる可能性があります。
一方で、安全性やデータの正確性が特に重要な場合、確認画面を実装することでユーザーに安心感を与えることができるため、お問い合わせフォームなどには確認画面を実装することが一般的です。
確認画面の実装は、モーダルウィンドウで表示させる場合と他のページで実装する場合が考えられます。それぞれには以下の特徴があります。
モーダルウィンドウでの確認画面の実装
モーダルウィンドウで確認画面を実装する場合、以下のようなメリット・デメリットが考えられます。
メリット
- ユーザーエクスペリエンス向上:モーダルウィンドウで実装することで、ユーザーは同じページ内で確認画面を素早く表示できます。また、ページの遷移が発生しないので、修正したい場合でも「入力したデータが失われるかもしれない」という懸念を減らすことができます。
- サーバーへの負荷低減:別のページに遷移せずに確認画面を表示できるので、サーバーへのリクエスト回数が削減され、サーバーへの負荷が低減します。
デメリット
- デザインの課題:モーダルウィンドウ内に全ての情報を収める必要があり、デザイン面で問題が発生する可能性があります。多くの入力項目がある場合、デザインを工夫する必要があります。
- アクセシビリティの配慮:モーダルウィンドウに対してスクリーンリーダー等でアクセスできるようにするためには、キーボードでの操作やスクリーンリーダーとの互換性を確保する必要があります。
他のページでの確認画面の実装
他のページとして確認画面を実装する場合、以下のようなメリット・デメリットが考えられます。
メリット
- デザインの自由度が高い:別ページで確認画面を実装することで、デザインやアクセシビリティに関する制約が少なくなります。
- 簡潔な実装:確認画面を別のページとして分離することで、コードを簡潔に保つことができ、メンテナンス性が向上します。
デメリット
- 遷移コスト:確認や修正のたびにページを遷移することになり、そのたびにサーバーとの通信が行われるため、ユーザーエクスペリエンスがわずかに低下する可能性があります。
- データの引き渡しが必要:入力フォームのデータを別のページに引き渡さないといけません。JavaScriptでデータを渡す場合はセッションやクッキーを使用する必要があります。
モーダルウィンドウでの確認画面はユーザーエクスペリエンスの向上と遷移回数の削減に寄与しますが、デザインやアクセス性の課題が生じる可能性があります。
一方、他のページでの確認画面はデザインの自由度が高く単純な実装が可能ですが、遷移コストが発生することや、入力データをそのページに引き渡すための実装が必要です。
どちらの方法を選択するかは、プロジェクトのニーズやデザイン、ユーザーエクスペリエンスに合わせて検討する必要があります。
モーダルウィンドウで確認画面を実装しよう
ここでは確認画面をモーダルウィンドウで実装します。表示や要素・値の取得をメインに解説するので、この記事ではアクセシビリティに関しては触れません。アクセシビリティ対策は章を分けて行います。
モーダルウィンドウでの確認画面の実装は以下の手順で行います。
- HTMLのセットアップ
- モーダルウィンドウのスタイリング
- モーダルウィンドウの表示制御
- 入力データの取得と表示
まずはHTMLのセットアップから行います。
HTMLのセットアップ
ここでは、モーダルウィンドウの要素はHTMLで行っていきます。これまでで制作した index.html に以下を追加しましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>お問い合わせ</title>
<link rel="stylesheet" href="style.css">
<script src="validation.js" defer></script>
</head>
<body>
<header>
<h1>LOGO</h1>
</header>
<main>
<h2>お問い合わせ</h2>
<form id="form" action="" method="GET">
…
<div class="btn-wrap">
<button type="submit" id="submit">お問い合わせ内容を送信</button>
</div>
</form>
</main>
<!-- 以下を追加 -->
<div id="modal" class="modal-wrap">
<div class="modal-body">
<h2 class="modal-ttl">入力内容の確認</h2>
<div class="form-item">
<p class="form-ttl">お問い合わせの種類</p>
<p id="type-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">お名前</p>
<p id="name-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">性別</p>
<p id="gender-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">メールアドレス</p>
<p id="email-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">学習中の言語</p>
<p id="learning-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">お問い合わせ内容</p>
<p id="content-confirm"></p>
</div>
<p class="modal-desc">上記の内容で送信します。<br>よろしければ「お問い合わせ内容を送信」を、<br>修正する場合は「修正する」を<br>クリックしてください。</p>
<div class="btn-wrap">
<button type="button" id="back" class="secondary-btn">修正する</button>
<button type="submit" id="submit" class="primary-btn">お問い合わせ内容を送信</button>
</div>
</div>
</div>
<footer>©copyright.</footer>
</body>
</html>
続いて form 要素内の送信ボタンを変更します。ここでは以下のように修正します。
<form id="form" action="" method="GET">
…
<div class="btn-wrap">
<button type="submit" id="confirm" class="primary-btn">入力内容を確認</button>
</div>
</form>
id 属性を confirm に変更し、 primary-btn クラスを追加しています。また、ボタンのテキストを「入力内容を確認」に変更しています。
HTMLのセットアップは以上です。続いて、モーダルウィンドウのスタイリングを行います。
モーダルウィンドウのスタイリング
ここでは、モーダルウィンドウのスタイルは以下のようにします。 style.css に追記しましょう。
/* 確認画面 */
.modal-wrap {
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
overflow: scroll;
display: none;
}
.modal-wrap.show {
display: grid;
grid-template-columns: 1em 1fr auto 1fr 1em;
grid-template-rows: 1em 1fr auto 1fr 1em;
}
.modal-body {
grid-column: 2 / 5;
grid-row: 3;
justify-self: center;
width: 100%;
max-width: 480px;
background-color: #fff;
padding: 2em 1em 4em;
box-shadow: 1px 1px 3px rgba(0, 0, 0, .1);
}
.modal-ttl {
font-size: 1.25em;
text-align: center;
padding: 0;
}
.modal-desc {
text-align: center;
font-size: 0.875em;
margin-top: 2em;
}
.modal-body .form-item:first-of-type {
margin-top: 1.75em;
border-top: 1px solid #bba;
}
#email-confirm {
word-break: break-all;
}
#learning-confirm {
display: flex;
flex-wrap: wrap;
gap: 1em;
}
#content-confirm {
white-space: pre-wrap;
}
.modal-body .btn-wrap {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 2em;
margin-top: 2em;
}
.modal-body .btn-wrap button {
flex: 1 0;
word-break: keep-all;
}
また、モーダルウィンドウには「修正する」ボタンと「お問い合わせ内容を送信」ボタンがあり、それぞれ異なるクラス属性を定義しているのでボタンのスタイルも変更します。
/* 以下を削除します */
/* 送信ボタンのスタイル */
button {
padding: 1em 2em;
font-weight: bold;
letter-spacing: 0.05em;
border-radius: 0.5em;
background-color: #446;
box-shadow: 1px 1px 3px #446;
color: #fff;
transition:
color .3s ease,
background-color .3s ease,
outline-color .3s ease;
}
button:hover {
color: #446;
background-color: #fff;
outline: 2px solid #446;
}
/* 以下を追加します */
/* ボタンのスタイル */
button {
padding: 1em 2em;
font-weight: bold;
letter-spacing: 0.05em;
border-radius: .5em;
transition:
color .3s ease,
background-color .3s ease,
outline-color .3s ease;
}
.primary-btn {
background-color: #446;
box-shadow: 1px 1px 3px #446;
color: white;
}
.primary-btn:focus,
.primary-btn:hover {
color: #446;
background-color: white;
outline: 2px solid #446;
}
.secondary-btn {
background-color: #ccd;
box-shadow: 1px 1px 3px #ccd;
color: #333;
}
.secondary-btn:focus,
.secondary-btn:hover {
color: #66a;
background-color: white;
outline: 2px solid #66a;
}
モーダルウィンドウの各項目には form-item や form-ttl クラスを使用しているので、余白などの基本的なスタイルはお問い合わせフォームと同じになります。ここではモーダルウィンドウ特有のスタイルのみ記述しています。
ポイントとして、 .modal-wrap セレクタには display: none; を指定し、 .modal-wrap.show では display: grid; とグリッドレイアウトに関するスタイルを定義しています。これにより、 show クラスの追加・削除でモーダルウィンドウの表示切替ができるようになります。
.modal-wrap {
…
display: none;
}
.modal-wrap.show {
display: grid;
grid-template-columns: 1em 1fr auto 1fr 1em;
grid-template-rows: 1em 1fr auto 1fr 1em;
}
modal-wrap クラスを指定した div 要素に show クラスを追加することでモーダルウィンドウのスタイルを確認することができます。
<div id="modal" class="modal-wrap show">
…
</div>

確認が出来たら show クラスは削除しておきましょう。
モーダルウィンドウのスタイリングは以上です。続いて、モーダルウィンドウの表示制御を行います。
モーダルウィンドウの表示制御
モーダルウィンドウの表示制御はJavaScriptで行います。以下の手順で行います。
- validation.jsの修正
- 表示・非表示用の関数を定義
- 確認画面を表示するための関数を定義
まずは、HTMLを書き換えたので validation.js を少し修正する必要があります。
validation.jsの修正
お問い合わせフォームの送信ボタンは以下のように書き換えました。
<div class="btn-wrap">
<button type="submit" id="confirm" class="primary-btn">入力内容を確認</button>
</div>
id 属性が変わっているので、以前のコードでは動作しなくなっています。 submitBtn 変数はモーダルウィンドウの送信ボタンとして使用できるのでそのままにしておき、以下を追記します。
/*
==============
HTML要素の取得
==============
*/
const submitBtn = document.getElementById('submit');
// 以下を追記
const backBtn = document.getElementById('back');
const confirmBtn = document.getElementById('confirm');
const modal = document.getElementById('modal');
ここでは「入力内容を確認」ボタンの他に「修正する」ボタンとモーダルウィンドウ本体の要素を取得しています。「修正する」ボタンはモーダルウィンドウ内に配置されています。
クリックイベントの対象も変更する必要があります。
/*
===========================
イベントハンドラの設定
===========================
*/
// submitBtn を confirmBtn に変更
confirmBtn.addEventListener('click', execValidateAll);
続いて、 event.preventDefault() を実行するタイミングも変更します。
event.preventDefault() は execValidateAll() 関数の if 文内で実行していましたが、これはバリデーションに失敗した場合に送信処理を中断する目的で実装していました。
また、確認ボタンは type=”submit” としており、対策しなければ確認ボタンとして機能せずにフォームが送信されてしまうので、確認ボタンをクリックしたタイミングで event.preventDefault() を実行するようにします。
confirmBtn のクリックイベントの処理を以下のように書き換えます。
/*
===========================
イベントハンドラの設定
===========================
*/
confirmBtn.addEventListener('click', function(event) { // 引数にeventを記述
event.preventDefault(); // confirmBtnのsubmitイベントを中断
if (execValidateAll()) {
// モーダルウィンドウを表示するための処理
showConfirm();
};
});
これにより、確認ボタンがクリックされてバリデーションが成功した場合でもフォームの送信がされないようになります。
confirmBtnのsubmitイベントの中断は、type属性を”button”に変更することでも可能です。
違いとして、お名前入力欄やメールアドレス入力欄のようなテキストフィールド内でエンターキーを押したとき、type属性がsubmitの場合はconfirmBtnのクリックイベントが実行されますが、type属性がbuttonの場合は実行されません。
また、 if 文で( execValidateAll() )を条件式にしています。 execValidatAll() 関数はバリデーションが成功したら true を返すようにしているので、各フォーム部品のバリデーションが成功した場合のみモーダルウィンドウが表示されることになります。
if (execValidateAll()) {
showConfirm();
};
showConfirm() 関数は確認画面を表示するための関数で、後ほど定義していきます。この関数内で表示処理の関数の実行や非表示処理のイベントハンドラの登録などを行います。
execValidateAll() 関数の event.preventDefault() は不要なのになったので削除します。 event も受け取る必要はありません。
function execValidateAll() { // eventを削除
for (const key in validationData) {
if (!validate(validationData[key])) {
event.preventDefault(); // これを削除
return false;
}
}
return true;
}
validation.js の修正は以上です。続いて、モーダルウィンドウの表示・非表示の処理を関数として定義していきます。
表示・非表示用の関数を定義
モーダルウィンドウに関する処理は新しくJavaScriptファイルを作成してその中に記述していきます。 index.html と同じ階層に modal.js というファイルを作成しましょう。
また、HTMLの head 要素内で読み込んでいる validation.js の下に以下を記述します。
<head>
…
<script src="validation.js" defer></script>
<script src="modal.js" defer></script> <!-- これを追記 -->
</head>
準備が整いました。まずはモーダルウィンドウを表示する処理を関数として定義します。
モーダルウィンドウを表示する処理
modal.js 内に以下を記述してください。
/**
* モーダルを表示します。
* @param {HTMLElement} modal - モーダル要素
* @param {HTMLElement} submitBtn - 送信ボタン要素
*/
function openModal(modal, submitBtn) {
modal.classList.add('show');
document.body.style.overflow = 'hidden';
submitBtn.setAttribute('form', 'form');
}
関数名を「 openModal 」とし、引数としてモーダルウィンドウと送信ボタンの要素を受け取っています。
受け取ったモーダルウィンドウに show クラスを追加することで表示させることができます。
modal.classList.add('show');
また、モーダルウィンドウが表示されているときは背景をスクロールさせたくないので、 body 要素に対して overflow: hidden; のスタイルを追加してスクロールを制御しています。
document.body.style.overflow = 'hidden';
モーダルウィンドウは form 要素の外側にあるため、通常ではモーダルウィンドウ内の送信ボタンはフォームの送信ボタンとして機能しません。以下はその対策です。
submitBtn.setAttribute('form', 'form');
button 要素には form 属性というものを指定でき、 form 属性の値に form 要素の id 属性の値を追加することで、 form 要素の外側にある送信ボタンをフォームの送信ボタンとして機能させることができます。 form 要素には id 属性を指定しているため機能します。
<form id="form" action="" method="GET"> … </form>
続いて、モーダルウィンドウを非表示にする処理を関数として定義します。
モーダルウィンドウを非表示にする処理
モーダルウィンドウを非表示にするための関数は以下です。
/**
* モーダルを非表示にします。
* @param {HTMLElement} modal - モーダル要素
* @param {HTMLElement} submitBtn - 送信ボタン要素
*/
function closeModal(modal, submitBtn) {
modal.classList.remove('show');
document.body.style.overflow = 'visible';
submitBtn.removeAttribute('form');
}
openModal() 関数の処理をリセットするような内容です。
closeModal() 関数では show クラスの削除と body 要素の overflow プロパティの値の変更、送信ボタンに追加した form 属性の削除を行っています。
form 属性の削除を行わないと、修正後に入力フォームでエンターキーを押したら確認画面が表示されずに送信されることになるので注意しましょう。
これでモーダルウィンドウの表示・非表示の処理を関数として定義できました。続いて、これらを実行するための showConfirm() 関数を定義します。
なぜバリデーションが成功したときに openModal() 関数をそのまま実行しないのかについて説明します。
そのまま openModal() 関数を実行してもモーダルウィンドウを表示させることはできますが、そのような実装では後に行う入力データの定義なども openModal() 関数の中に記述しないといけなくなってしまいます。
closeModal() 関数は openModal() 関数をリセットする処理になっており、シンプルで理解しやすいかと思います。この関係が崩れると可読性が悪くなり、予期せぬ不具合があった場合に原因の特定が難しくなる場合があります。
openModal() 関数とcloseModal() 関数の関係を保とうとすると、openModal() 関数が複雑になるほどcloseModal() 関数も複雑になります。
これらの関数を呼び出すための関数を新たに定義することで、コードをシンプルに保つことができ、不具合の特定や機能の追加を容易にすることができます。
確認画面を表示するための関数を定義
ここでは新たに確認画面を表示するための showConfirm() 関数を定義し、この関数内で openModal() 関数の実行や「修正する」ボタンのクリックイベントの設定を実装します。
この関数のコードは以下です。
/**
* 確認画面モーダルを表示します。
*/
function showConfirm() {
openModal(modal, submitBtn);
// 戻るボタンにクリックイベントが設定されていない場合に設定します。
if ('hasClickEvent' in backBtn === false) {
backBtn.hasClickEvent = true;
backBtn.addEventListener('click', function () {
closeModal(modal, submitBtn);
});
}
}
関数名を「 showConfirm 」とし、その中で openModal() 関数を実行しています。また、「修正する」ボタンにクリックイベントが登録されていない場合のみクリックイベントの登録を行っています。
// 戻るボタンにクリックイベントが設定されていない場合に設定します。
if ('hasClickEvent' in backBtn === false) {
backBtn.hasClickEvent = true;
backBtn.addEventListener('click', function () {
closeModal(modal, submitBtn);
});
}
addEventListener メソッドはクリックイベントに対して複数の関数を登録できるため、 addEventListener メソッドが実行される度にクリックイベントに同じ関数が登録されることになります。
具体的には、「修正する」ボタンをクリックしてモーダルウィンドウを閉じた後に再度「入力内容を確認」ボタンをクリックすると、 backBtn のクリックイベントは同じ処理の関数がもう一度登録されることになります。
ユーザーが修正を何度も繰り返すようなことはあまり考えられないかもしれませんが、この現象を放置すると予期せぬバグを生み出す可能性があるので対策をしています。
backBtn は HTMLElement でありオブジェクトなので、任意でプロパティの追加ができます。 HTMLElement には hasClickEvent というプロパティは定義されていないため、最初に showConfirm() 関数が実行された場合、以下の条件式は成立することになります。
// backBtnにhasClickEventプロパティがなければ条件成立
if ('hasClickEvent' in backBtn === false) { … }
if 文内では、 backBtn に hasClickEvent プロパティの定義とクリックイベントの登録を行っています。
backBtn.hasClickEvent = true;
backBtn.addEventListener('click', function () {
closeModal(modal, submitBtn);
});
これにより、二度目以降は「 if (‘hasClickEvent’ in backBtn === false) 」の条件が不成立となるので、クリックイベントの登録の重複を防ぐことができます。
取得した要素がどのようなプロパティを持っているかを確認するには、開発ツールのコンソールでconsole.dir(要素)を実行するのが簡単です。
以下はコンソールでconsole.dir(backBtn);を実行した結果です。

確認画面を表示するための関数ができました。記述ミスなどがなければ、ブラウザで正常な動作を確認できるかと思います。
続いて、入力データを取得して確認画面に表示させる処理を実装していきます。
入力データの取得と表示
確認画面に表示するためのデータをオブジェクトとして定義することで、フォーム部品が増えたときの対応を簡潔にすることができます。入力データの取得と表示は以下の手順で行います。
- 確認画面のデータの定義
- 確認画面に値を挿入する関数を定義
- labelのテキストを取得する関数を定義
確認画面のデータの定義
ここでは確認画面のデータとして、値を挿入する要素と入力データに基づいたテキストを定義します。データ構造は以下になります。
const confirmData = {
'フォーム部品のID': {
insert: '値を挿入する要素',
value: '入力データに基づいたテキスト',
},
…
}
confirmData の定義はバリデーションが成功して確認画面が表示されたときに行いたいので、 showConfirm() 関数内に記述します。
上記のデータ構造で各フォーム部品を定義すると以下のようになります。
function showConfirm() {
/**
* フォームの確認データ
* @type {Object.<string, { insert: HTMLElement, value: string }>}
*/
const confirmData = {
type: {
insert: document.getElementById('type-confirm'),
value: document.getElementById('type').options[document.getElementById('type').selectedIndex].text,
},
name: {
insert: document.getElementById('name-confirm'),
value: document.getElementById('name').value,
},
gender: {
insert: document.getElementById('gender-confirm'),
value: 'チェックされた項目のlabelのテキストを返す関数を実行します',
},
email: {
insert: document.getElementById('email-confirm'),
value: document.getElementById('email').value,
},
learning: {
insert: document.getElementById('learning-confirm'),
value: 'チェックされた項目のlabelのテキストを返す関数を実行します',
},
content: {
insert: document.getElementById('content-confirm'),
value: document.getElementById('content').value,
}
}
…
}
ポイントとして、セレクトボックスの値を value プロパティから取得した場合は value 属性の値がそのまま表示されるので、選択された option 要素を特定してそのテキストを取得しています。
value: document.getElementById('type').options[document.getElementById('type').selectedIndex].text,
また、チェックボックスやラジオボタンは項目それぞれに id 属性を指定しているので、チェックされた項目のテキストを取得するには少し複雑な処理を行う必要があります。これは後ほど関数として定義します。
データの定義ができたので、次は確認画面に値を挿入する関数を定義します。
確認画面に値を挿入する関数を定義
confirmData オブジェクトは insert と value というプロパティを持っています。データ構造としてはシンプルなものなので、再利用可能な関数として定義します。
/**
* valueをinsertに指定した要素のinnerHTMLに挿入します。
* @param {Object.<string, { insert: HTMLElement, value: string }>} data - データオブジェクト
*/
function insertValue(data) {
for (const key in data) {
if (data[key].value) {
data[key].insert.innerHTML = data[key].value;
}
}
}
関数名は「 insertValue 」とし、引数としてオブジェクトを受け取ります。この関数は confirmData オブジェクトと同じ構造で定義されたオブジェクトを受け取れば機能します。
処理の内容は単純です。 for 文で insert プロパティに指定したHTML要素の innerHTML に対し、データに定義されている value プロパティの値を代入しています。
例として、 confirmData の name プロパティは以下のように定義しています。
name: {
insert: document.getElementById('name-confirm'),
value: document.getElementById('name').value,
},
insert プロパティにはモーダルウィンドウの name-confirm という id を持つ要素を指定しており、この要素は以下です。
<div id="modal" class="modal-wrap">
<div class="modal-body">
…
<div class="form-item">
<p class="form-ttl">お名前</p>
<p id="name-confirm"></p>
</div>
…
</div>
</div>
name-confirm の id を持つ要素は空の p 要素として定義しています。この要素の innerHTML に value プロパティの値が挿入されます。
insertValue() 関数の実行は showConfirm() 関数内で行います。以下を追記しましょう。
function showConfirm() {
const confirmData = { … }
insertValue(confirmData); // これを追加
…
}
確認画面に値を挿入する関数はこれで完成ですが、現状では「性別」と「学習中の言語」のテキストが取得できていません。次にこれらを取得する関数を定義します。
labelのテキストを取得する関数を定義
チェックボックスは複数の項目をチェックすることができますが、ラジオボタンは基本的にはそれができません。
これらを区別するには関数を別々で定義するか関数内で条件分岐を行う必要がありますが、ここではシンプルさを優先してチェックボックスとラジオボタンを区別せずに処理を行う関数を定義します。
関数の内容は以下です。
/**
* ラベル要素のテキストを取得し、HTML要素として返します。
* @param {NodeListOf<HTMLInputElement>} targets - 対象の入力要素ノードリスト
* @returns {string} HTML形式のテキスト
*/
function getLabels(targets) {
let result = '';
if (targets.length === 0) {
result = '選択されていません。';
} else {
targets.forEach(function(target) {
const label = document.querySelector('label[for=' + target.id + ']');
const spanElement = document.createElement('span');
spanElement.innerHTML = label.textContent;
result += spanElement.outerHTML;
});
}
return result;
};
関数名を「 getLabels 」とし、引数として NodeList 型のオブジェクトを受け取ります。この関数の引数について解説します。
NodeList にはチェックされている項目の input 要素が入ります。例として、「学習中の言語」の値を取得する場合は以下のようにこの関数を呼び出します。
learning: {
…
value: getLabels(document.querySelectorAll('input[name=learning]:checked')),
}
document.querySelectorAll() メソッドで取得した各要素は NodeList 型のオブジェクトとして配列のような形で配置されます。
「学習中の言語」の各項目は name 属性の値が learning となっているので、 querySelectorAll() メソッドの引数を「 input[name=learning]:checked 」と指定することで、チェックされている input 要素が NodeList 内に配置されます。
続いて、 getLabels() 関数内の処理を見ていきます。まずはこの関数の戻り値です。
function getLabels(targets) {
let result = '';
…
return result;
}
getLabels() 関数は戻り値として文字列を返します。変数として result を定義し、空の文字列を代入しています。
以下では、条件分岐により「項目が選択されていない場合」のテキストを定義しています。
function getLabels(targets) {
let result = '';
if (targets.length === 0) {
result = '選択されていません。';
} else { … }
return result;
};
NodeList は配列のような特徴があるオブジェクトなので、一部の配列用のメソッドやプロパティを扱うことができます。 length プロパティはその一つであり、 NodeList 内の要素の数を取得することができます。
条件式を (targets.length === 0) とすることで、チェックされている項目がない場合の処理を定義することができます。
次に、チェックされている項目があった場合の処理を見ていきます。まずはループ処理です。
targets.forEach(function(target) {
…
}
NodeList 型のオブジェクトは配列用のメソッドである forEach() も使用できます。 forEach() メソッドを使用することで、簡単に配列内の要素にアクセスし、それぞれの要素に対して同じ操作を行うことができます。
上記では targets 内の各要素を target という名前でコールバック関数の引数に渡しています。これにより、 targets オブジェクト内の各要素を target という名前でコールバック関数内で扱うことができます。
続いて、コールバック関数の処理を見ていきます。
targets.forEach(function(target) {
const label = document.querySelector('label[for=' + target.id + ']');
const spanElement = document.createElement('span');
spanElement.innerHTML = label.textContent;
result += spanElement.outerHTML;
}
引数として受け取る target は input 要素であることを前提とします。以下では、この要素に紐づいている label 要素を取得しています。
const label = document.querySelector('label[for=' + target.id + ']');
また、新たに spanElement という変数名で span 要素を作成し、その innerHTML に取得した label 要素のテキストを代入しています。
const spanElement = document.createElement('span');
spanElement.innerHTML = label.textContent;
span要素の追加はスタイリングを目的として行っています。モーダルウィンドウのスタイリングでは以下のような記述をしています。
#learning-confirm {
display: flex;
flex-wrap: wrap;
gap: 1em;
}
テキストをspan要素で囲むことでフレックスアイテムとして扱われるため、上記の記述では上下に1文字分の余白ができます。
最後に result 変数に spanElement を追加しています。「=」ではなく「+=」とすることで、上書きではなく追加という動作になります。
result += spanElement.outerHTML;
これで getLabels() 関数ができました。 confirmData オブジェクトで定義している「性別」と「学習中の言語」の value プロパティを変更し、この関数を呼び出します。
const confirmData = {
…
gender: {
insert: document.getElementById('gender-confirm'),
value: getLabels(document.querySelectorAll('input[name=gender]:checked')),
},
…
learning: {
insert: document.getElementById('learning-confirm'),
value: getLabels(document.querySelectorAll('input[name=learning]:checked')),
},
…
}
性別はラジオボタンなのでquerySelectorメソッドで引数を渡したいところですが、querySelectorで取得した要素はNodeListではなくHTMLElementになるので現状のgetLabels関数では扱えません。
解説が冗長になるのでこの対策は行いませんが、学習を深めたい方はこの問題の解決にチャレンジしてみると良いと思います。
モーダルウィンドウによるお問い合わせフォームの確認画面の実装ができました。最後に動作確認をしましょう。
問題が無ければ以下のように表示され、「修正する」ボタンと「お問い合わせ内容を送信」ボタンも機能します。

サンプルコードの全体
以下はサンプルコードの全体です。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>お問い合わせ</title>
<link rel="stylesheet" href="style.css">
<script src="validation.js" defer></script>
<script src="modal.js" defer></script>
</head>
<body>
<header>
<h1>LOGO</h1>
</header>
<main>
<h2>お問い合わせ</h2>
<form id="form" action="" method="GET">
<div class="form-item">
<label for="type" class="form-ttl">お問い合わせの種類</label>
<div class="select-wrap">
<select id="type" name="type">
<option value="learn" selected>ウェブ制作の学び方について</option>
<option value="article">記事の内容について</option>
<option value="site">このサイトの使い方について</option>
<option value="admin">このサイトの管理者について</option>
</select>
</div>
</div>
<div class="form-item">
<label for="name" class="form-ttl">お名前<span class="required">必須</span></label>
<p class="example-txt">例:山田 太郎</p>
<p class="err-msg"></p>
<input type="text" id="name" name="name" required>
</div>
<div class="form-item">
<fieldset>
<legend class="form-ttl">性別</legend>
<label for="male"><input type="radio" id="male" name="gender" value="male">男性</label>
<label for="female"><input type="radio" id="female" name="gender" value="female">女性</label>
</fieldset>
</div>
<div class="form-item">
<label for="email" class="form-ttl">メールアドレス<span class="required">必須</span></label>
<p class="example-txt">例:hogehoge@example.com</p>
<p class="err-msg"></p>
<input type="mail" id="email" name="email" required>
</div>
<div class="form-item">
<fieldset>
<legend class="form-ttl">学習中の言語</legend>
<label for="html"><input type="checkbox" id="html" name="learning" value="html" checked>HTML</label>
<label for="css"><input type="checkbox" id="css" name="learning" value="css">CSS</label>
<label for="javascript"><input type="checkbox" id="javascript" name="learning" value="javascript">JavaScript</label>
<label for="php"><input type="checkbox" id="php" name="learning" value="php">PHP</label>
</fieldset>
</div>
<div class="form-item">
<label for="content" class="form-ttl">お問い合わせ内容<span class="required">必須</span></label>
<p class="err-msg"></p>
<textarea id="content" name="content" rows="20" required></textarea>
</div>
<div class="policy-check-wrap">
<label for="policy-check">
<p class="err-msg"></p>
<input
type="checkbox"
id="policy-check"
name="policy-check"
required>個人情報保護方針に同意します。
</label>
</div>
<div class="btn-wrap">
<button type="submit" id="confirm" class="primary-btn">入力内容を確認</button>
</div>
</form>
</main>
<div id="modal" class="modal-wrap">
<div class="modal-body">
<h2 class="modal-ttl">入力内容の確認</h2>
<div class="form-item">
<p class="form-ttl">お問い合わせの種類</p>
<p id="type-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">お名前</p>
<p id="name-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">性別</p>
<p id="gender-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">メールアドレス</p>
<p id="email-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">学習中の言語</p>
<p id="learning-confirm"></p>
</div>
<div class="form-item">
<p class="form-ttl">お問い合わせ内容</p>
<p id="content-confirm"></p>
</div>
<p class="modal-desc">上記の内容で送信します。<br>よろしければ「お問い合わせ内容を送信」を、<br>修正する場合は「修正する」を<br>クリックしてください。</p>
<div class="btn-wrap">
<button type="button" id="back" class="secondary-btn">修正する</button>
<button type="submit" id="submit" class="primary-btn">お問い合わせ内容を送信</button>
</div>
</div>
</div>
<footer>©copyright.</footer>
</body>
</html>
style.css
@charset "UTF-8";
/* ================
基本設定
================ */
body {
font-family: "Helvetica Neue", "Helvetica", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Arial", "Yu Gothic", "Meiryo", sans-serif;
background-color: #fefeef;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
padding: 0;
}
/* ================
ページ全体のスタイル
================ */
header {
padding: .5em 1em;
background-color: #fff;
box-shadow: -5px 1px 5px rgba(0, 0, 0, 0.1);
}
main {
width: calc(100% - 2em);
max-width: 768px;
margin: 0 auto 6em;
}
footer {
text-align: center;
padding: 1em 0;
background-color: #fff;
box-shadow: -5px -1px 5px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
padding: 2em 0;
}
form {
background-color: white;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
padding: 2em 1em;
}
.form-ttl {
font-weight: bold;
display: block;
width: fit-content;
margin-bottom: 0.5em;
}
.form-item {
padding: 1em 0.5em 2em;
}
/* .form-item:not(:first-child) {
border-top: 1px solid #bba;
} */
.form-item {
border-bottom: 1px solid #bba;
}
fieldset label {
font-weight: 500;
}
.required {
font-size: 0.75em;
font-weight: bold;
color: #f03030;
margin-left: 1em;
}
.example-txt {
color: #888;
font-size: 0.875em;
margin-bottom: .5em;
}
.policy-check-wrap {
text-align: center;
margin: 2em 0 3em;
font-size: 0.875em;
}
.btn-wrap {
text-align: center;
}
@media screen and (min-width: 768px) {
main {
width: calc(100% - 4em);
}
form {
padding: 3em;
}
.form-ttl {
font-size: 1.25em;
}
.form-item {
padding: 1em 1em 2em;
}
.policy-check-wrap {
font-size: 1em;
}
}
/* ================
フォーム部品
================ */
/* フォーム部品のリセット */
fieldset,
legend,
button,
select,
input[type="text"],
input[type="email"],
input[type="checkbox"],
input[type="radio"],
textarea {
border: none;
outline: none;
background: none;
appearance: none;
}
button:focus,
select:focus,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="checkbox"]:focus,
input[type="radio"]:focus,
textarea:focus {
outline: none;
}
/* フォーム部品の基本設定 */
select,
option,
input,
textarea {
font-family: "Helvetica Neue", "Helvetica", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Arial", "Yu Gothic", "Meiryo", sans-serif;
font-size: 1em;
font-weight: 500;
}
textarea {
overflow: auto;
resize: none;
}
label,
input[type="checkbox"],
input[type="radio"],
button {
cursor: pointer;
}
fieldset {
display: flex;
flex-wrap: wrap;
gap: 1em;
}
input[type="text"],
input[type="email"],
textarea {
width: 100%;
}
/* ドロップダウンメニューのスタイル */
select {
border: 1px solid #bba;
border-radius: 0.25em;
padding: 1em 2em 1em 1em;
}
select:focus {
outline: 1px solid #bba;
}
.select-wrap {
position: relative;
width: fit-content;
}
.select-wrap::before {
content: "▼";
position: absolute;
color: #bba;
right: 0.5em;
top: 50%;
transform: translateY(-50%);
}
/* テキストフィールドのスタイル */
input[type="text"],
input[type="email"],
textarea {
border: 1px solid #bba;
border-radius: 0.25em;
padding: 0.5em;
background: #fefeef;
box-shadow: inset 1px 1px 3px #c8c4aa;
}
input[type="text"]:focus,
input[type="email"]:focus,
textarea:focus {
outline: 1px solid #c8c4aa;
background: #fffff2;
}
/* ラジオボタン・チェックボックス
共通ののスタイル */
input[type="radio"],
input[type="checkbox"] {
border: 1px solid #bba;
width: 1.25em;
height: 1.25em;
position: relative;
top: 0.25em;
margin-right: 0.5em;
}
input[type="radio"]:focus,
input[type="checkbox"]:focus {
outline: 1px solid #bba;
}
/* ラジオボタンのスタイル */
input[type="radio"] {
border-radius: 50%;
}
input[type="radio"]:checked {
border-width: 0.375em;
}
/* チェックボックスのスタイル */
input[type="checkbox"] {
border-radius: 0.25em;
}
input[type="checkbox"]::before {
content: "";
display: block;
position: absolute;
top: -0.125em;
left: 0.375em;
width: 0.5em;
height: 1em;
border-bottom: 0.25em solid #bba;
border-right: 0.125em solid #bba;
border-radius: 0.25em;
transform: rotate(45deg);
visibility: hidden;
}
input[type="checkbox"]:checked::before {
visibility: visible;
}
/* ボタンのスタイル */
button {
padding: 1em 2em;
font-weight: bold;
letter-spacing: 0.05em;
border-radius: .5em;
transition:
color .3s ease,
background-color .3s ease,
outline-color .3s ease;
}
.primary-btn {
background-color: #446;
box-shadow: 1px 1px 3px #446;
color: white;
}
.primary-btn:focus,
.primary-btn:hover {
color: #446;
background-color: white;
outline: 2px solid #446;
}
.secondary-btn {
background-color: #ccd;
box-shadow: 1px 1px 3px #ccd;
color: #333;
}
.secondary-btn:focus,
.secondary-btn:hover {
color: #66a;
background-color: white;
outline: 2px solid #66a;
}
/* エラーメッセージのスタイル */
select {
width: 100%;
}
.select-wrrap {
max-width: fit-content;
}
.policy-check-wrap {
font-size: 1em;
}
.form-item,
.policy-check-wrap {
position: relative;
}
.err-msg {
position: absolute;
left: 2em;
bottom: -0.375em;
color: #f06060;
font-size: 0.875em;
font-weight: bold;
border: 2px solid #f06060;
border-radius: 0.25em;
padding: 0.25em 1em;
background: #fffafa;
display: none;
}
.err-msg.show {
display: block;
}
.err-msg::before {
content: "";
position: absolute;
top: -1em;
left: 0.5em;
border: 0.5em solid transparent;
border-bottom: 0.5em solid #f06060;
}
.err-msg::after {
content: "";
position: absolute;
top: -0.875em;
left: 0.5em;
border: 0.5em solid transparent;
border-bottom: 0.5em solid #FFF;
}
.err-msg.show+input[type="text"],
.err-msg.show+input[type="email"],
.err-msg.show+textarea {
border: 1px solid #f06060;
background: #fffafa;
}
.err-msg.show+input[type="text"]:focus,
.err-msg.show+input[type="email"]:focus,
.err-msg.show+textarea:focus {
border: 1px solid #f06060;
outline: 1px solid #f06060;
background: #fffcfc;
}
.policy-check-wrap .err-msg {
left: 50%;
transform: translateX(-50%);
width: max-content;
bottom: -3em;
}
.policy-check-wrap .err-msg::before,
.policy-check-wrap .err-msg::after {
left: 50%;
transform: translateX(-50%);
}
/* 確認画面 */
.modal-wrap {
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5)
overflow: scroll;
display: none;
}
.modal-wrap.show {
display: grid;
grid-template-columns: 1em 1fr auto 1fr 1em;
grid-template-rows: 1em 1fr auto 1fr 1em;
}
.modal-body {
grid-column: 2 / 5;
grid-row: 3;
justify-self: center;
width: 100%;
max-width: 480px;
background-color: #fff;
padding: 2em 1em 4em;
box-shadow: 1px 1px 3px rgba(0, 0, 0, .1);
}
.modal-ttl {
font-size: 1.25em;
text-align: center;
padding: 0;
}
.modal-desc {
text-align: center;
font-size: 0.875em;
margin-top: 2em;
}
.modal-body .form-item:first-of-type {
margin-top: 1.75em;
border-top: 1px solid #bba;
}
#email-confirm {
word-break: break-all;
}
#learning-confirm {
display: flex;
flex-wrap: wrap;
gap: 1em;
}
#content-confirm {
white-space: pre-wrap;
}
.modal-body .btn-wrap {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 2em;
margin-top: 2em;
}
.modal-body .btn-wrap button {
flex: 1 0;
word-break: keep-all;
}
validation.js
/*
==============
HTML要素の取得
==============
*/
const submitBtn = document.getElementById('submit');
const backBtn = document.getElementById('back');
const confirmBtn = document.getElementById('confirm');
const modal = document.getElementById('modal');
/*
=====================
バリデーションのデータ
=====================
*/
/**
* バリデーションに使用するオブジェクトです。
*/
const validationData = {
name: {
element: document.getElementById('name'),
errMsgs: {
empty: {
pattern: /^.+$/,
errMsg: 'お名前を入力してください'
},
spaceOnly: {
pattern: /^(?!\s+$).+$/,
errMsg: 'スペースのみの入力はできません'
},
noSeparated: {
pattern: /^.+\s.+$/,
errMsg: '姓と名の間にスペースを入れてください'
},
max: {
pattern: /^.{0,30}$/,
errMsg: '30文字以内で入力してください'
},
invalid: {
pattern: /^[ぁ-んァ-ヶ一-龠a-zA-Z\s]+$/,
errMsg: 'ひらがな、カタカナ、漢字、半角英字で入力してください'
}
},
eventListeners: ['blur', 'input']
},
email: {
element: document.getElementById('email'),
errMsgs: {
empty: {
pattern: /^.+$/,
errMsg: 'メールアドレスを入力してください'
},
invalid: {
pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
errMsg: 'メールアドレスの形式が正しくありません'
}
},
eventListeners: ['blur', 'input']
},
content: {
element: document.getElementById('content'),
errMsgs: {
empty: {
pattern: /^[\s\S]+$/,
errMsg: 'お問い合わせ内容を入力してください'
}
},
eventListeners: ['blur', 'input']
},
policyCheck: {
element: document.getElementById('policy-check'),
errMsgs: {
unchecked: {
pattern: /^checked$/,
errMsg: '個人情報保護方針に同意してください'
}
},
eventListeners: ['change']
}
}
/*
==========
関数の定義
==========
*/
/**
* validationDataのプロパティに対してバリデーションを実行し、エラーがある場合にフォームの送信を防ぎます。
* @returns {boolean} - バリデーションが成功した場合は true、エラーがある場合は false
*/
function execValidateAll() {
for (const key in validationData) {
if (!validate(validationData[key])) {
return false;
}
}
return true;
}
/**
* validationDataのプロパティに対してvalidation関数を実行するためのイベントを設定します。
* @param {Object} data - バリデーションデータオブジェクト
*/
function setValidateListeners(data) {
for (const key in data) {
const target = data[key];
const element = target.element;
const eventListeners = target.eventListeners;
for (const eventType in eventListeners) {
if (eventListeners[eventType] === 'input') {
continue;
} else {
element.addEventListener(eventListeners[eventType], function () {
validate(target);
});
}
}
}
}
/**
* 入力フィールドの値をバリデーションします。
* @param {Object} target - validationDataのプロパティ
* @returns {boolean} - バリデーションが成功した場合は true、エラーがある場合は false
*/
function validate(target) {
let element = target.element;
let errMsgs = target.errMsgs;
let eventListeners = target.eventListeners;
let value;
if (element.type === 'checkbox') {
value = element.checked ? 'checked' : '';
} else {
value = element.value;
}
let errMsgElem = element.previousElementSibling;
for (const key in errMsgs) {
const { pattern, errMsg } = errMsgs[key];
if (!pattern.test(value)) {
errMsgElem.textContent = errMsg;
errMsgElem.classList.add('show');
element.focus();
if (eventListeners.includes('input')) {
if (!target.inputListener) {
element.addEventListener('input', function () {
validate(target);
});
target.inputListener = true;
}
}
return false;
}
}
errMsgElem.classList.remove('show');
return true;
}
/*
===========================
イベントハンドラの設定
===========================
*/
confirmBtn.addEventListener('click', function(event) {
event.preventDefault();
if (execValidateAll()) {
showConfirm();
};
});
setValidateListeners(validationData);
modal.js
/**
* 確認画面モーダルを表示します。
*/
function showConfirm() {
/**
* フォームの確認データ
* @type {Object.<string, { insert: HTMLElement, value: string }>}
*/
const confirmData = {
type: {
insert: document.getElementById('type-confirm'),
value: document.getElementById('type').options[document.getElementById('type').selectedIndex].text,
},
name: {
insert: document.getElementById('name-confirm'),
value: document.getElementById('name').value,
},
gender: {
insert: document.getElementById('gender-confirm'),
value: getLabels(document.querySelectorAll('input[name=gender]:checked')),
},
email: {
insert: document.getElementById('email-confirm'),
value: document.getElementById('email').value,
},
learning: {
insert: document.getElementById('learning-confirm'),
value: getLabels(document.querySelectorAll('input[name=learning]:checked')),
},
content: {
insert: document.getElementById('content-confirm'),
value: document.getElementById('content').value,
}
}
insertValue(confirmData);
openModal(modal, submitBtn);
if ('hasClickEvent' in backBtn === false) {
backBtn.hasClickEvent = true;
backBtn.addEventListener('click', function () {
closeModal(modal, submitBtn);
});
}
}
/**
* モーダルを表示します。
* @param {HTMLElement} modal - モーダル要素
* @param {HTMLElement} submitBtn - 送信ボタン要素
*/
function openModal(modal, submitBtn) {
modal.classList.add('show');
document.body.style.overflow = 'hidden';
submitBtn.setAttribute('form', 'form');
}
/**
* モーダルを非表示にします。
* @param {HTMLElement} modal - モーダル要素
* @param {HTMLElement} submitBtn - 送信ボタン要素
*/
function closeModal(modal, submitBtn) {
modal.classList.remove('show');
document.body.style.overflow = 'visible';
submitBtn.removeAttribute('form');
}
/**
* valueをinsertに指定した要素のinnerHTMLに挿入します。
* @param {Object.<string, { insert: HTMLElement, value: string }>} data - データオブジェクト
*/
function insertValue(data) {
for (const key in data) {
if (data[key].value) {
data[key].insert.innerHTML = data[key].value;
}
}
}
/**
* ラベル要素のテキストを取得し、HTML要素として返します。
* @param {NodeListOf<HTMLInputElement>} targets - 対象の入力要素ノードリスト
* @returns {string} HTML形式のテキスト
*/
function getLabels(targets) {
let result = '';
if (targets.length === 0) {
result = '選択されていません。';
} else {
targets.forEach(function(target) {
const label = document.querySelector('label[for=' + target.id + ']');
const spanElement = document.createElement('span');
spanElement.innerHTML = label.textContent;
result += spanElement.outerHTML;
});
}
return result;
};
次のステップ
確認画面の必要性について学び、お問い合わせフォームの値を反映させた確認画面をモーダルウィンドウで実装しました。
ここで作成したコードはリファクタリングを行う余地がたくさんあるので、JavaScriptの学習を深めたい方はチャレンジしてみると良いと思います。ポイントとして、
- openModal() 関数や closeModal() 関数を汎用的にする
- モーダルウィンドウに関するHTML要素は modal.js 内で管理する
などは比較的簡単にリファクタリングを行うことができるでしょう。
ここで作成したモーダルウィンドウの確認画面にはアクセシビリティに関する課題が残されています。次のステップの「お問い合わせフォームを作ってみよう【アクセシビリティ対策編】」では、モーダルウィンドウのアクセシビリティ面での課題について学び、「Escキーを押してもモーダルウィンドウが閉じない」、「フォーカス移動に制限がないのでフォーカスを見失いやすい」といった問題を解決し、スクリーンリーダーを使用しているユーザーでも扱いやすいように改善を行っていきます。

