05. イベント処理の基本

この章では、イベント処理を学びます。

イベントとは、ユーザーの操作やブラウザ上で起きる出来事のことです。クリック、入力、フォーム送信、キー操作、スクロール、画面サイズの変更などがイベントです。

Web制作のJavaScriptは、「何かが起きたら、画面の状態を変える」という形で書くことが多いです。その「何かが起きたら」を扱うのがイベント処理です。

イベントとは

イベントは、ブラウザ上で発生する出来事です。

たとえば、次のようなものがあります。

イベント起きるタイミング
clickクリック、タップされた時
input入力欄の値が変わった時
change選択肢やチェック状態が変わった時
submitフォームが送信された時
keydownキーボードのキーが押された時
scrollスクロールされた時
resize画面サイズが変わった時
DOMContentLoadedHTMLの読み込みが終わった時

Web制作では、これらのイベントをきっかけに、クラスや属性を切り替えます。

addEventListener

イベントを登録するには、addEventListenerを使います。

HTML
<button class="js-button" type="button">クリック</button>
JavaScript
const button = document.querySelector(".js-button");

if (button) {
    button.addEventListener("click", () => {
        console.log("クリックされました");
    });
}

addEventListenerは、次の形で書きます。

基本形
element.addEventListener("イベント名", () => {
    // イベントが起きた時の処理
});

この形は何度も使います。

clickイベント

clickは、ボタンやリンクなどがクリック、タップされた時に発火します。

Web制作で最もよく使うイベントの1つです。

clickイベント
const button = document.querySelector(".js-menu-button");
const menu = document.querySelector(".js-menu");

if (button && menu) {
    button.addEventListener("click", () => {
        menu.classList.toggle("is-active");
    });
}

ハンバーガーメニュー、アコーディオン、タブ、モーダル、ドロップダウンなど、多くのUIはクリックイベントから始まります。

inputイベント

inputは、入力欄の値が変わるたびに発火します。

HTML
<input class="js-name-input" type="text" />
<p class="js-count-text">0文字</p>
inputイベント
const input = document.querySelector(".js-name-input");
const countText = document.querySelector(".js-count-text");

if (input && countText) {
    input.addEventListener("input", () => {
        countText.textContent = `${input.value.length}文字`;
    });
}

文字数カウント、リアルタイムバリデーション、入力に応じたボタンの有効化などで使います。

changeイベント

changeは、入力値や選択状態が確定して変わった時に発火します。

チェックボックス、ラジオボタン、セレクトボックスでよく使います。

HTML
<label>
    <input class="js-agree-checkbox" type="checkbox" />
    利用規約に同意する
</label>

<button class="js-submit-button" type="submit" disabled>送信する</button>
changeイベント
const checkbox = document.querySelector(".js-agree-checkbox");
const submitButton = document.querySelector(".js-submit-button");

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

inputchangeは似ていますが、フォーム部品によって使い分けます。

イベントよく使う場面
inputテキスト入力の変化をすぐ反映したい時
change選択やチェック状態の変更を扱う時

submitイベント

submitは、フォームが送信される時に発火します。

フォームの入力チェックではとても重要です。

HTML
<form class="js-contact-form" novalidate>
    <input class="js-email-input" type="email" />
    <p class="js-email-error" hidden></p>
    <button type="submit">送信する</button>
</form>
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) => {
        const isEmpty = emailInput.value === "";

        if (isEmpty) {
            event.preventDefault();

            emailInput.setAttribute("aria-invalid", "true");
            emailError.hidden = false;
            emailError.textContent = "メールアドレスを入力してください。";
        }
    });
}

ここで出てきたevent.preventDefault()は、ブラウザ標準の送信を止めるための処理です。

入力内容に問題がある時は、送信を止めてエラーを表示します。

keydownイベント

keydownは、キーボードのキーが押された時に発火します。

モーダルをEscキーで閉じる、メニューをキーボードで操作する、入力中にEnterキーを扱う、といった場面で使います。

Escキーで閉じる
document.addEventListener("keydown", (event) => {
    if (event.key === "Escape") {
        modal.classList.remove("is-active");
        modal.hidden = true;
    }
});

event.keyには、押されたキーの名前が入ります。

よく使う値は次の通りです。

キーevent.key
Esc"Escape"
Enter"Enter"
Tab"Tab"
矢印上"ArrowUp"
矢印下"ArrowDown"
矢印左"ArrowLeft"
矢印右"ArrowRight"

アクセシビリティを考えると、クリックだけでなくキーボード操作も大切になります。

DOMContentLoaded

DOMContentLoadedは、HTMLの読み込みが終わった時に発火します。

JavaScriptをhead内で読み込む場合、HTML要素がまだ作られていないタイミングでJavaScriptが実行されることがあります。

その場合は、DOMContentLoadedを使ってHTMLの読み込み後に処理します。

DOMContentLoaded
document.addEventListener("DOMContentLoaded", () => {
    const button = document.querySelector(".js-button");

    if (button) {
        button.addEventListener("click", () => {
            console.log("クリックされました");
        });
    }
});

ただし、実務ではscriptタグにdeferを付けて読み込むことも多いです。

scriptの読み込み
<script src="./assets/js/main.js" defer></script>

deferを付けると、HTMLの解析が終わってからJavaScriptが実行されます。

イベントオブジェクト

イベントが発生すると、イベントに関する情報が入ったオブジェクトを受け取れます。

イベントオブジェクト
button.addEventListener("click", (event) => {
    console.log(event);
});

このeventの中には、どの要素で起きたか、どのキーが押されたか、標準動作を止めるためのメソッドなどが入っています。

event.target

event.targetは、実際にイベントが発生した要素です。

HTML
<button class="js-button" type="button">
    <span>開く</span>
</button>
event.target
button.addEventListener("click", (event) => {
    console.log(event.target);
});

上の例で、ボタンの中のspan部分をクリックすると、event.targetspanになることがあります。

event.currentTarget

event.currentTargetは、イベントリスナーを付けた要素です。

event.currentTarget
button.addEventListener("click", (event) => {
    console.log(event.currentTarget);
});

この場合、event.currentTargetは常にbuttonです。

preventDefault

preventDefaultは、ブラウザ標準の動きを止めます。

フォーム送信を止める時によく使います。

フォーム送信を止める
form.addEventListener("submit", (event) => {
    event.preventDefault();
});

リンクの遷移を止めることもできます。

リンクの標準動作を止める
link.addEventListener("click", (event) => {
    event.preventDefault();
});

ただし、むやみに標準動作を止めると、ユーザーにとって使いにくくなることがあります。

stopPropagation

stopPropagationは、イベントが親要素へ伝わるのを止めます。

たとえば、モーダルの背景をクリックしたら閉じるが、モーダル本体をクリックしても閉じたくない場合があります。

HTML
<div class="modal js-modal">
    <div class="modal-body js-modal-body">
        <p>モーダルの中身</p>
    </div>
</div>
stopPropagation
modal.addEventListener("click", () => {
    modal.hidden = true;
});

modalBody.addEventListener("click", (event) => {
    event.stopPropagation();
});

ただし、stopPropagationを多用するとイベントの流れが追いにくくなります。

イベント委譲と組み合わせる時も注意が必要です。

イベント委譲

イベント委譲は、子要素それぞれにイベントを付けるのではなく、親要素にイベントを付けて、クリックされた子要素を判定する考え方です。

たとえば、FAQの項目がたくさんある場合を考えます。

HTML
<div class="faq-list js-faq-list">
    <div class="faq-item">
        <button class="faq-item-button js-faq-button" type="button">質問1</button>
        <div class="faq-item-panel" hidden>回答1</div>
    </div>
    <div class="faq-item">
        <button class="faq-item-button js-faq-button" type="button">質問2</button>
        <div class="faq-item-panel" hidden>回答2</div>
    </div>
</div>

通常は、各ボタンにイベントを付けます。

各ボタンにイベントを付ける
const buttons = document.querySelectorAll(".js-faq-button");

buttons.forEach((button) => {
    button.addEventListener("click", () => {
        console.log("クリックされました");
    });
});

イベント委譲では、親の.js-faq-listにイベントを付けます。

イベント委譲
const faqList = document.querySelector(".js-faq-list");

if (faqList) {
    faqList.addEventListener("click", (event) => {
        const button = event.target.closest(".js-faq-button");

        if (!button) {
            return;
        }

        const item = button.closest(".faq-item");
        const panel = item.querySelector(".faq-item-panel");

        panel.hidden = !panel.hidden;
    });
}

このコードでは、クリックされた場所からclosest(".js-faq-button")でボタンを探しています。

ボタン以外をクリックした時は、returnで処理を終わらせています。

イベント委譲は便利ですが、最初から何でも委譲にする必要はありません。

まずは各要素にイベントを付ける書き方に慣れ、その後で「親にまとめた方がよさそう」と感じる場面で使いましょう。

scrollイベント

scrollは、ページがスクロールされた時に発火します。

ヘッダーの見た目変更、ページトップボタンの表示、スクロール位置に応じた処理などで使います。

スクロール量でヘッダーを変える
const header = document.querySelector(".js-header");

if (header) {
    window.addEventListener("scroll", () => {
        const isScrolled = window.scrollY > 100;

        header.classList.toggle("is-scrolled", isScrolled);
    });
}

window.scrollYには、ページ上部からどれだけスクロールしたかが入ります。

要素が画面に入ったら発火させたいだけなら、後の章で扱うIntersection Observerの方が向いていることが多いです。

resizeイベント

resizeは、画面サイズが変わった時に発火します。

resizeイベント
window.addEventListener("resize", () => {
    console.log(window.innerWidth);
});

PCとスマホで処理を変えたい時に使うことがあります。

ただし、リサイズ中も何度も発火するため、重い処理には注意が必要です。

matchMedia

画面幅に応じて処理を分けるなら、matchMediaを使うとCSSのメディアクエリに近い考え方で書けます。

matchMedia
const mediaQuery = window.matchMedia("(max-width: 768px)");

if (mediaQuery.matches) {
    console.log("スマホ幅です");
} else {
    console.log("PC幅です");
}

画面幅が変わったタイミングを監視することもできます。

matchMediaのchangeイベント
const mediaQuery = window.matchMedia("(max-width: 768px)");

mediaQuery.addEventListener("change", (event) => {
    if (event.matches) {
        console.log("スマホ幅になりました");
    } else {
        console.log("PC幅になりました");
    }
});

debounceの考え方

scrollresizeのように何度も発火するイベントでは、処理の回数を減らしたいことがあります。

その時に使う考え方がdebounceです。

debounceは、連続して起きるイベントに対して、最後のイベントから少し待ってから処理する考え方です。

debounceの簡易例
let timerId;

window.addEventListener("resize", () => {
    clearTimeout(timerId);

    timerId = setTimeout(() => {
        console.log("リサイズが落ち着きました");
    }, 300);
});

このコードでは、リサイズ中はタイマーをリセットし続け、最後のリサイズから300ミリ秒後に処理します。

最初から完全に理解できなくても大丈夫です。

まずは、scrollresizeは何度も発火するため、重い処理を直接入れすぎない、という意識を持ちましょう。

小さな実装例: ページトップボタン

イベント処理を使って、一定スクロールしたらページトップボタンを表示する例を見てみます。

HTML
<button class="page-top js-page-top" type="button" hidden>ページトップへ</button>
CSS
.page-top {
    position: fixed;
    right: 24px;
    bottom: 24px;
}

.page-top.is-visible {
    animation: fadeIn 0.2s ease-out;
}
JavaScript
const pageTopButton = document.querySelector(".js-page-top");

if (pageTopButton) {
    window.addEventListener("scroll", () => {
        const shouldShow = window.scrollY > 300;

        pageTopButton.hidden = !shouldShow;
        pageTopButton.classList.toggle("is-visible", shouldShow);
    });

    pageTopButton.addEventListener("click", () => {
        window.scrollTo({
            top: 0,
            behavior: "smooth",
        });
    });
}

このコードでは、2つのイベントを使っています。

  • scroll: 一定以上スクロールしたらボタンを表示する
  • click: ボタンを押したらページ上部へ戻る

よくある失敗

イベント処理でよくある失敗を整理しておきます。

  • 対象要素がないのにaddEventListenerしてエラーになる
  • 関数をイベント時ではなく、読み込み時に実行してしまう
  • event.targetを使ったら、ボタン内のspanが取れて想定とずれる
  • preventDefaultを忘れてフォームが送信されてしまう
  • 逆に、必要ない場面でpreventDefaultしてリンクやフォームの標準動作を壊す
  • scrollresizeに重い処理を入れてページが重くなる
  • 複数要素のうち最初の1つにしかイベントが付いていない

特に、複数要素にイベントを付ける処理は実務でよく間違えます。

1つだけ動いたら終わりではなく、同じUIを2つ、3つ置いても動くか確認しましょう。

この章のまとめ

この章では、イベント処理の基本を学びました。

  • イベントは、クリック、入力、送信、スクロールなどブラウザ上で起きる出来事
  • addEventListenerでイベント発生時の処理を登録する
  • clickは定番UIの開閉処理でよく使う
  • inputchangeはフォーム操作で使う
  • submitでは、必要に応じてpreventDefaultで送信を止める
  • event.targetevent.currentTargetは違う
  • イベント委譲を使うと、親要素で子要素のイベントを扱える
  • scrollresizeは発火回数が多いので、重い処理に注意する

次の章では、ここまで学んだ基礎文法、DOM操作、イベント処理を使って、Web制作でよく使う定番UIを実装していきます。