07. フォーム操作とバリデーション

この章では、フォーム操作とバリデーションを学びます。

Web制作案件では、お問い合わせフォーム、資料請求フォーム、採用応募フォーム、予約フォームなど、フォームまわりの実装がよく出てきます。

フォームは見た目だけでなく、入力しやすさ、エラーの伝え方、二重送信防止、アクセシビリティにも関わります。JavaScriptで何でも解決しようとするのではなく、HTMLの標準機能と組み合わせて考えることが大切です。

フォーム部品の基本

フォームでは、HTMLの部品を正しく使うことがとても大切です。

JavaScriptを書く前に、まずHTMLの意味を整えます。

フォームの基本
<form class="contact-form js-contact-form" novalidate>
    <div class="form-field">
        <label for="name">お名前</label>
        <input id="name" class="js-name-input" type="text" name="name" required />
    </div>

    <div class="form-field">
        <label for="email">メールアドレス</label>
        <input id="email" class="js-email-input" type="email" name="email" required />
    </div>

    <button class="js-submit-button" type="submit">送信する</button>
</form>

labelと入力欄を対応させると、ラベルをクリックした時に入力欄へフォーカスできます。スクリーンリーダーにも項目名が伝わりやすくなります。

valueで入力値を取得する

テキスト入力、メールアドレス、電話番号、テキストエリアなどの値は、.valueで取得できます。

HTML
<input class="js-name-input" type="text" />
value
const nameInput = document.querySelector(".js-name-input");

if (nameInput) {
    console.log(nameInput.value);
}

入力されるたびに値を取得したい場合は、inputイベントを使います。

入力ごとに値を見る
const nameInput = document.querySelector(".js-name-input");

if (nameInput) {
    nameInput.addEventListener("input", () => {
        console.log(nameInput.value);
    });
}

文字数カウント、リアルタイムエラー表示、送信ボタンの有効化などで使います。

checkedでチェック状態を取得する

チェックボックスやラジオボタンでは、.checkedでチェック状態を取得します。

HTML
<label>
    <input class="js-agree-checkbox" type="checkbox" />
    利用規約に同意する
</label>
checked
const checkbox = document.querySelector(".js-agree-checkbox");

if (checkbox) {
    checkbox.addEventListener("change", () => {
        console.log(checkbox.checked);
    });
}

checkedは真偽値です。チェックされていればtrue、されていなければfalseになります。

送信ボタンの有効化でよく使います。

チェック状態で送信ボタンを切り替える
const checkbox = document.querySelector(".js-agree-checkbox");
const submitButton = document.querySelector(".js-submit-button");

if (checkbox && submitButton) {
    checkbox.addEventListener("change", () => {
        submitButton.disabled = !checkbox.checked;
    });
}

filesでファイルを取得する

ファイル入力では、.filesで選択されたファイルを取得します。

HTML
<input class="js-file-input" type="file" />
files
const fileInput = document.querySelector(".js-file-input");

if (fileInput) {
    fileInput.addEventListener("change", () => {
        const file = fileInput.files[0];

        if (file) {
            console.log(file.name);
        }
    });
}

ファイル名の表示、ファイルサイズの確認、画像プレビューなどで使います。

FormData

FormDataを使うと、フォーム内の値をまとめて取得できます。

HTML
<form class="js-contact-form">
    <input type="text" name="name" />
    <input type="email" name="email" />
    <button type="submit">送信する</button>
</form>
FormData
const form = document.querySelector(".js-contact-form");

if (form) {
    form.addEventListener("submit", (event) => {
        event.preventDefault();

        const formData = new FormData(form);

        console.log(formData.get("name"));
        console.log(formData.get("email"));
    });
}

この教材ではAPI送信を本格的には扱いませんが、入力値をまとめて扱う考え方として知っておくと便利です。

HTML標準のバリデーション

HTMLには、入力チェックのための属性があります。

属性役割
required必須項目にする
type="email"メール形式をチェックする
minlength最小文字数を指定する
maxlength最大文字数を指定する
pattern正規表現で形式を指定する
HTML標準のチェック
<input type="email" name="email" required />

JavaScriptで全部を自作する前に、HTML標準でできることを使いましょう。

checkValidity

checkValidityは、フォームや入力欄がHTML標準の条件を満たしているか確認するメソッドです。

checkValidity
const emailInput = document.querySelector(".js-email-input");

if (emailInput) {
    const isValid = emailInput.checkValidity();
    console.log(isValid);
}

フォーム全体に対しても使えます。

フォーム全体をチェックする
const form = document.querySelector(".js-contact-form");

if (form) {
    form.addEventListener("submit", (event) => {
        if (!form.checkValidity()) {
            event.preventDefault();
            console.log("入力内容に問題があります。");
        }
    });
}

checkValidityは、問題なければtrue、問題があればfalseを返します。

validity

validityを見ると、どの種類のエラーかを確認できます。

validity
const emailInput = document.querySelector(".js-email-input");

if (emailInput) {
    console.log(emailInput.validity.valueMissing);
    console.log(emailInput.validity.typeMismatch);
}

よく使うプロパティは次の通りです。

プロパティ意味
valueMissing必須なのに空
typeMismatchtype="email"などの形式に合わない
tooShortminlengthより短い
tooLongmaxlengthより長い
patternMismatchpatternに合わない
validすべての条件を満たしている

エラーメッセージを出し分けたい時に使います。

validationMessage

validationMessageには、ブラウザ標準のエラーメッセージが入ります。

validationMessage
const emailInput = document.querySelector(".js-email-input");

if (emailInput && !emailInput.checkValidity()) {
    console.log(emailInput.validationMessage);
}

ただし、ブラウザや言語設定によって文言が変わります。

案件のデザインや文体に合わせたい場合は、自分でメッセージを作ることが多いです。

エラー文言を自分で作る
function getEmailErrorMessage(input) {
    if (input.validity.valueMissing) {
        return "メールアドレスを入力してください。";
    }

    if (input.validity.typeMismatch) {
        return "メールアドレスの形式で入力してください。";
    }

    return "";
}

setCustomValidity

setCustomValidityを使うと、独自のエラー状態を設定できます。

setCustomValidity
const passwordInput = document.querySelector(".js-password-input");
const confirmInput = document.querySelector(".js-confirm-input");

if (passwordInput && confirmInput) {
    confirmInput.addEventListener("input", () => {
        if (passwordInput.value !== confirmInput.value) {
            confirmInput.setCustomValidity("パスワードが一致しません。");
        } else {
            confirmInput.setCustomValidity("");
        }
    });
}

空文字を渡すと、カスタムエラーを解除できます。

エラー表示の基本

実務では、ブラウザ標準の吹き出しではなく、デザインに合わせたエラー表示を作ることが多いです。

HTML
<div class="form-field">
    <label for="email">メールアドレス</label>
    <input
        id="email"
        class="js-email-input"
        type="email"
        name="email"
        required
        aria-describedby="email-error"
    />
    <p class="form-error js-email-error" id="email-error" hidden></p>
</div>
エラー表示
const emailInput = document.querySelector(".js-email-input");
const emailError = document.querySelector(".js-email-error");

function showEmailError(message) {
    emailInput.setAttribute("aria-invalid", "true");
    emailError.hidden = false;
    emailError.textContent = message;
}

function clearEmailError() {
    emailInput.setAttribute("aria-invalid", "false");
    emailError.hidden = true;
    emailError.textContent = "";
}

aria-invalidは、入力欄にエラーがあるかを伝える属性です。

aria-describedbyでエラーメッセージのIDを指定しておくと、入力欄とエラー文の関係を伝えやすくなります。

submit時バリデーション

送信ボタンを押した時に入力チェックをする基本形です。

submit時の入力チェック
const form = document.querySelector(".js-contact-form");
const emailInput = document.querySelector(".js-email-input");
const emailError = document.querySelector(".js-email-error");

if (form && emailInput && emailError) {
    form.addEventListener("submit", (event) => {
        if (emailInput.checkValidity()) {
            return;
        }

        event.preventDefault();

        if (emailInput.validity.valueMissing) {
            showEmailError("メールアドレスを入力してください。");
        } else if (emailInput.validity.typeMismatch) {
            showEmailError("メールアドレスの形式で入力してください。");
        }
    });
}

送信時バリデーションは、ユーザーが「送信する」を押したタイミングでエラーを表示します。

最初からエラーを出しすぎないので、比較的やさしい操作感になります。

リアルタイムバリデーション

入力中にエラーを消したり、条件を満たしたらボタンを有効化したりする場合は、inputイベントを使います。

入力中にエラーを消す
emailInput.addEventListener("input", () => {
    if (emailInput.checkValidity()) {
        clearEmailError();
    }
});

ただし、入力中に毎回強いエラーを出すと、ユーザーにとってうるさく感じることがあります。

エラー位置へスクロールする

フォームが長い場合、送信時に最初のエラー位置へスクロールすると親切です。

最初のエラーへスクロールする
const firstInvalidInput = form.querySelector('[aria-invalid="true"]');

if (firstInvalidInput) {
    firstInvalidInput.scrollIntoView({
        behavior: "smooth",
        block: "center",
    });

    firstInvalidInput.focus();
}

固定ヘッダーがある場合は、CSSでscroll-margin-topを指定しておくと調整しやすいです。

CSS
.form-field input {
    scroll-margin-top: 100px;
}

確認画面風UI

静的なWeb制作案件では、本格的な確認画面ではなく、入力内容を同じページ内に表示する「確認画面風UI」を求められることがあります。

HTML
<input class="js-name-input" type="text" name="name" />
<p>確認: <span class="js-name-preview"></span></p>
入力内容を表示する
const nameInput = document.querySelector(".js-name-input");
const namePreview = document.querySelector(".js-name-preview");

if (nameInput && namePreview) {
    nameInput.addEventListener("input", () => {
        namePreview.textContent = nameInput.value;
    });
}

この時も、ユーザー入力を表示するだけならtextContentを使います。

innerHTMLにそのまま入れないようにしましょう。

二重送信防止

フォーム送信時に、送信ボタンを何度も押されると、二重送信につながることがあります。

フロント側では、送信時にボタンを無効化する基本形をよく使います。

二重送信防止
const form = document.querySelector(".js-contact-form");
const submitButton = document.querySelector(".js-submit-button");

if (form && submitButton) {
    form.addEventListener("submit", (event) => {
        if (!form.checkValidity()) {
            return;
        }

        submitButton.disabled = true;
        submitButton.textContent = "送信中...";
    });
}

小さな実装例: お問い合わせフォーム

ここまでの内容を使って、名前とメールアドレスをチェックする小さな例を作ります。

HTML
<form class="contact-form js-contact-form" novalidate>
    <div class="form-field">
        <label for="name">お名前</label>
        <input id="name" class="js-name-input" type="text" name="name" required aria-describedby="name-error" />
        <p id="name-error" class="form-error js-name-error" hidden></p>
    </div>

    <div class="form-field">
        <label for="email">メールアドレス</label>
        <input id="email" class="js-email-input" type="email" name="email" required aria-describedby="email-error" />
        <p id="email-error" class="form-error js-email-error" hidden></p>
    </div>

    <button class="js-submit-button" type="submit">送信する</button>
</form>
JavaScript
const form = document.querySelector(".js-contact-form");
const nameInput = document.querySelector(".js-name-input");
const emailInput = document.querySelector(".js-email-input");
const nameError = document.querySelector(".js-name-error");
const emailError = document.querySelector(".js-email-error");
const submitButton = document.querySelector(".js-submit-button");

function setError(input, errorElement, message) {
    input.setAttribute("aria-invalid", "true");
    errorElement.hidden = false;
    errorElement.textContent = message;
}

function clearError(input, errorElement) {
    input.setAttribute("aria-invalid", "false");
    errorElement.hidden = true;
    errorElement.textContent = "";
}

function validateName() {
    if (nameInput.value === "") {
        setError(nameInput, nameError, "お名前を入力してください。");
        return false;
    }

    clearError(nameInput, nameError);
    return true;
}

function validateEmail() {
    if (emailInput.validity.valueMissing) {
        setError(emailInput, emailError, "メールアドレスを入力してください。");
        return false;
    }

    if (emailInput.validity.typeMismatch) {
        setError(emailInput, emailError, "メールアドレスの形式で入力してください。");
        return false;
    }

    clearError(emailInput, emailError);
    return true;
}

if (form && nameInput && emailInput && nameError && emailError && submitButton) {
    form.addEventListener("submit", (event) => {
        const isNameValid = validateName();
        const isEmailValid = validateEmail();

        if (!isNameValid || !isEmailValid) {
            event.preventDefault();
            return;
        }

        submitButton.disabled = true;
        submitButton.textContent = "送信中...";
    });

    nameInput.addEventListener("input", validateName);
    emailInput.addEventListener("input", validateEmail);
}

この例では、次の処理を入れています。

  • 入力値を取得する
  • 必須チェックをする
  • メール形式をチェックする
  • エラー表示を更新する
  • aria-invalidを更新する
  • 送信中にボタンを無効化する

よくある失敗

フォーム実装でよくある失敗を整理しておきます。

  • labelと入力欄が対応していない
  • エラー文を出しているが、aria-invalidaria-describedbyが更新されていない
  • preventDefaultを常に実行してしまい、正常時も送信できない
  • setCustomValidityの解除を忘れる
  • 入力中にエラーを出しすぎて使いにくい
  • フロント側チェックだけで安全だと思ってしまう
  • 二重送信防止でボタンを無効化したまま、エラー時に戻していない
  • ユーザー入力をinnerHTMLに入れてしまう

フォームはユーザーが直接触る部分です。

エラーがあることを伝えるだけでなく、どう直せばよいかがわかる文言にしましょう。

この章のまとめ

この章では、フォーム操作とバリデーションの基本を学びました。

  • 入力値は.value、チェック状態は.checked、ファイルは.filesで取得する
  • HTML標準のrequiredtype="email"minlengthなどを活用する
  • checkValidityで入力欄やフォーム全体が有効か確認できる
  • validityを見ると、エラーの種類を判定できる
  • エラー表示ではaria-invalidaria-describedbyも意識する
  • preventDefaultは、送信を止めたい時だけ使う
  • 二重送信防止はフロント側だけでなく、サーバー側の対応も重要

次の章では、スクロールに応じた表示アニメーションを実装するために、Intersection Observerを学びます。