この章では、イベント処理を学びます。
イベントとは、ユーザーの操作やブラウザ上で起きる出来事のことです。クリック、入力、フォーム送信、キー操作、スクロール、画面サイズの変更などがイベントです。
Web制作のJavaScriptは、「何かが起きたら、画面の状態を変える」という形で書くことが多いです。その「何かが起きたら」を扱うのがイベント処理です。
イベントとは
イベントは、ブラウザ上で発生する出来事です。
たとえば、次のようなものがあります。
| イベント | 起きるタイミング |
|---|---|
click | クリック、タップされた時 |
input | 入力欄の値が変わった時 |
change | 選択肢やチェック状態が変わった時 |
submit | フォームが送信された時 |
keydown | キーボードのキーが押された時 |
scroll | スクロールされた時 |
resize | 画面サイズが変わった時 |
DOMContentLoaded | HTMLの読み込みが終わった時 |
Web制作では、これらのイベントをきっかけに、クラスや属性を切り替えます。
addEventListener
イベントを登録するには、addEventListenerを使います。
<button class="js-button" type="button">クリック</button>const button = document.querySelector(".js-button");
if (button) {
button.addEventListener("click", () => {
console.log("クリックされました");
});
}addEventListenerは、次の形で書きます。
element.addEventListener("イベント名", () => {
// イベントが起きた時の処理
});この形は何度も使います。
clickイベント
clickは、ボタンやリンクなどがクリック、タップされた時に発火します。
Web制作で最もよく使うイベントの1つです。
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は、入力欄の値が変わるたびに発火します。
<input class="js-name-input" type="text" />
<p class="js-count-text">0文字</p>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は、入力値や選択状態が確定して変わった時に発火します。
チェックボックス、ラジオボタン、セレクトボックスでよく使います。
<label>
<input class="js-agree-checkbox" type="checkbox" />
利用規約に同意する
</label>
<button class="js-submit-button" type="submit" disabled>送信する</button>const checkbox = document.querySelector(".js-agree-checkbox");
const submitButton = document.querySelector(".js-submit-button");
if (checkbox && submitButton) {
checkbox.addEventListener("change", () => {
submitButton.disabled = !checkbox.checked;
});
}inputとchangeは似ていますが、フォーム部品によって使い分けます。
| イベント | よく使う場面 |
|---|---|
input | テキスト入力の変化をすぐ反映したい時 |
change | 選択やチェック状態の変更を扱う時 |
submitイベント
submitは、フォームが送信される時に発火します。
フォームの入力チェックではとても重要です。
<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>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キーを扱う、といった場面で使います。
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の読み込み後に処理します。
document.addEventListener("DOMContentLoaded", () => {
const button = document.querySelector(".js-button");
if (button) {
button.addEventListener("click", () => {
console.log("クリックされました");
});
}
});ただし、実務ではscriptタグにdeferを付けて読み込むことも多いです。
<script src="./assets/js/main.js" defer></script>deferを付けると、HTMLの解析が終わってからJavaScriptが実行されます。
イベントオブジェクト
イベントが発生すると、イベントに関する情報が入ったオブジェクトを受け取れます。
button.addEventListener("click", (event) => {
console.log(event);
});このeventの中には、どの要素で起きたか、どのキーが押されたか、標準動作を止めるためのメソッドなどが入っています。
event.target
event.targetは、実際にイベントが発生した要素です。
<button class="js-button" type="button">
<span>開く</span>
</button>button.addEventListener("click", (event) => {
console.log(event.target);
});上の例で、ボタンの中のspan部分をクリックすると、event.targetはspanになることがあります。
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は、イベントが親要素へ伝わるのを止めます。
たとえば、モーダルの背景をクリックしたら閉じるが、モーダル本体をクリックしても閉じたくない場合があります。
<div class="modal js-modal">
<div class="modal-body js-modal-body">
<p>モーダルの中身</p>
</div>
</div>modal.addEventListener("click", () => {
modal.hidden = true;
});
modalBody.addEventListener("click", (event) => {
event.stopPropagation();
});ただし、stopPropagationを多用するとイベントの流れが追いにくくなります。
イベント委譲と組み合わせる時も注意が必要です。
イベント委譲
イベント委譲は、子要素それぞれにイベントを付けるのではなく、親要素にイベントを付けて、クリックされた子要素を判定する考え方です。
たとえば、FAQの項目がたくさんある場合を考えます。
<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は、画面サイズが変わった時に発火します。
window.addEventListener("resize", () => {
console.log(window.innerWidth);
});PCとスマホで処理を変えたい時に使うことがあります。
ただし、リサイズ中も何度も発火するため、重い処理には注意が必要です。
matchMedia
画面幅に応じて処理を分けるなら、matchMediaを使うとCSSのメディアクエリに近い考え方で書けます。
const mediaQuery = window.matchMedia("(max-width: 768px)");
if (mediaQuery.matches) {
console.log("スマホ幅です");
} else {
console.log("PC幅です");
}画面幅が変わったタイミングを監視することもできます。
const mediaQuery = window.matchMedia("(max-width: 768px)");
mediaQuery.addEventListener("change", (event) => {
if (event.matches) {
console.log("スマホ幅になりました");
} else {
console.log("PC幅になりました");
}
});debounceの考え方
scrollやresizeのように何度も発火するイベントでは、処理の回数を減らしたいことがあります。
その時に使う考え方がdebounceです。
debounceは、連続して起きるイベントに対して、最後のイベントから少し待ってから処理する考え方です。
let timerId;
window.addEventListener("resize", () => {
clearTimeout(timerId);
timerId = setTimeout(() => {
console.log("リサイズが落ち着きました");
}, 300);
});このコードでは、リサイズ中はタイマーをリセットし続け、最後のリサイズから300ミリ秒後に処理します。
最初から完全に理解できなくても大丈夫です。
まずは、scrollやresizeは何度も発火するため、重い処理を直接入れすぎない、という意識を持ちましょう。
小さな実装例: ページトップボタン
イベント処理を使って、一定スクロールしたらページトップボタンを表示する例を見てみます。
<button class="page-top js-page-top" type="button" hidden>ページトップへ</button>.page-top {
position: fixed;
right: 24px;
bottom: 24px;
}
.page-top.is-visible {
animation: fadeIn 0.2s ease-out;
}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してリンクやフォームの標準動作を壊す scrollやresizeに重い処理を入れてページが重くなる- 複数要素のうち最初の1つにしかイベントが付いていない
特に、複数要素にイベントを付ける処理は実務でよく間違えます。
1つだけ動いたら終わりではなく、同じUIを2つ、3つ置いても動くか確認しましょう。
この章のまとめ
この章では、イベント処理の基本を学びました。
- イベントは、クリック、入力、送信、スクロールなどブラウザ上で起きる出来事
addEventListenerでイベント発生時の処理を登録するclickは定番UIの開閉処理でよく使うinputやchangeはフォーム操作で使うsubmitでは、必要に応じてpreventDefaultで送信を止めるevent.targetとevent.currentTargetは違う- イベント委譲を使うと、親要素で子要素のイベントを扱える
scrollやresizeは発火回数が多いので、重い処理に注意する
次の章では、ここまで学んだ基礎文法、DOM操作、イベント処理を使って、Web制作でよく使う定番UIを実装していきます。