HTML・CSSとJavaScriptを連携する:メニュー・テーマ切り替え・入力確認を実装
HTMLとCSSにJavaScriptを連携。レスポンシブメニュー、ダークモード、保存失敗への対応、送信しない入力確認フォームを実装します。
第9回では、HTMLとCSSで作ったサイトをJavaScriptで動かします。HTMLは意味と入力部品、CSSは見た目、JavaScriptは状態と操作という役割を保ちます。目標は、クリックのたびに画面全体を作り直さず、必要な属性や文字だけを変更できるようになることです。
今回のJavaScriptはブラウザーで実行する
script.jsを読み込む
index.htmlと同じフォルダーにscript.jsを作り、HTMLのhead内でCSSの読み込み行の後へ次を追加します。defer付きの外部スクリプトは、HTMLの解析後に実行されるため、本文の要素を取得できます。
<script src="./script.js" defer></script>
今回使うdocumentや画面のイベントはブラウザーの機能です。node script.jsでこのページを操作することはできません。文法に不安がある場合はJavaScript入門と関数・スコープを先に確認してください。
DOMとイベントの関係
ブラウザーがHTMLから作った要素の構造をDOMと呼びます。querySelector()で要素を取得し、addEventListener()でクリックや入力時に呼ぶ処理を登録します。指定したIDがHTMLにない場合は取得結果がnullになり、そのまま操作するとエラーになるので、ファイルを組み合わせる際はIDを一致させます。
操作ごとの状態を設計する
メニューの開閉
ボタンのaria-expandedに開閉状態を入れ、ナビゲーションのhiddenへ表示状態を反映します。matchMedia()でCSSと同じ48remの境界を確認し、PC幅ではナビゲーションを常に表示します。幅を変えたときにも状態を同期し、スマートフォンで閉じたままPCへ広げてもリンクが消えないようにします。
Escapeキーで閉じたら、操作元のボタンへフォーカスを戻します。これは通常のページ内ナビゲーションなので、ダイアログのようなフォーカスの閉じ込めは行いません。リンクは標準のa要素のままです。
テーマと保存の失敗
data-themeを変えると、第8回で定義した色の変数が切り替わります。aria-pressedはダークモードが選択されているかを表します。保存にはlocalStorageを使いますが、ブラウザー設定などで利用できない場合もあるため、try...catchで切り替え機能まで停止しないようにします。
file:で直接開いた場合の保存動作は環境差があるため、保存の継続はHTTP/HTTPSで配信した状態でも確認します。この例で保存するのは配色の選択だけです。名前やフォームの入力値は保存しません。
フォームは入力確認だけを行う
submitイベントでpreventDefault()を呼び、通常のページ送信を止めます。requiredのほかに空白だけの名前を確認し、setCustomValidity()でエラーを付けます。入力し直したらエラーを空文字列で解除する処理も必要です。解除しないと、正しい入力に直しても送信イベントへ進めない場合があります。
画面への表示にはtextContentを使います。入力文字列をinnerHTMLへ直接入れると、入力をHTMLとして解釈する問題につながります。実際の送信を導入する段階では、サーバー側の検証、送信失敗、重複送信、情報の取り扱いも別途設計します。
script.jsの完成コード
コードを保存する
HTMLは第3回の内容に上記のscriptを追加し、CSSは第8回の完成版を使用してください。次がscript.js全体です。
const menuButton = document.querySelector('#menu-toggle');
const navigation = document.querySelector('#site-nav');
const wideScreen = window.matchMedia('(min-width: 48rem)');
function setMenu(open) {
menuButton.setAttribute('aria-expanded', String(open));
menuButton.textContent = open ? '閉じる' : 'メニュー';
navigation.hidden = !wideScreen.matches && !open;
}
function syncMenu() {
const focusWillHide = !wideScreen.matches && navigation.contains(document.activeElement);
menuButton.hidden = wideScreen.matches;
setMenu(false);
if (focusWillHide) menuButton.focus();
}
menuButton.addEventListener('click', () => {
setMenu(menuButton.getAttribute('aria-expanded') !== 'true');
});
navigation.addEventListener('click', event => {
if (event.target.closest('a')) setMenu(false);
});
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && !wideScreen.matches && menuButton.getAttribute('aria-expanded') === 'true') {
setMenu(false);
menuButton.focus();
}
});
wideScreen.addEventListener('change', syncMenu);
syncMenu();
const themeButton = document.querySelector('#theme-toggle');
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
themeButton.setAttribute('aria-pressed', String(theme === 'dark'));
}
let initialTheme = 'light';
try {
if (localStorage.getItem('atelier-theme') === 'dark') initialTheme = 'dark';
} catch {
// 保存が許可されない環境でも、表示の切り替えは使えるようにする。
}
applyTheme(initialTheme);
themeButton.hidden = false;
themeButton.addEventListener('click', () => {
const nextTheme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
applyTheme(nextTheme);
try { localStorage.setItem('atelier-theme', nextTheme); } catch {}
});
const form = document.querySelector('#contact-form');
const nameInput = document.querySelector('#name');
const result = document.querySelector('#form-result');
nameInput.addEventListener('input', () => {
nameInput.setCustomValidity('');
result.textContent = '';
});
form.addEventListener('submit', event => {
event.preventDefault();
const name = nameInput.value.trim();
nameInput.setCustomValidity(name ? '' : '空白だけではなく、名前を入力してください。');
if (!form.reportValidity()) return;
const course = new FormData(form).get('course');
result.textContent = `${name}さん、${course}を選択しました。これは入力確認のみで、送信はしていません。`;
});
document.querySelector('#confirm-button').disabled = false;
ブラウザーで操作を確認する
狭い画面ではメニューボタンが表示され、押すとリンクが開きます。PC幅ではリンクが常時表示されます。ダークモードを押すと配色と押下状態が変わります。架空の名前を入力して「入力を確認する」を押すと、選んだ学習内容が画面に表示されます。実際の申し込みは行われません。
正常時以外も試す
空欄・空白・文字列を試す
名前が空欄なら必須チェック、空白だけなら独自のメッセージが出ます。名前に<b>テスト</b>と入力しても太字にはならず、その文字がそのまま表示されることを確認します。入力欄を修正したらエラーが解除されるかも試してください。
JavaScriptなしでも内容を読めるか
JavaScriptを無効にして読み込むと、ナビゲーションは表示されたままで、入力確認ボタンは無効です。学習内容やページ内リンクは使えます。このように基本の内容をHTMLで提供し、JavaScriptで機能を加えると、スクリプトが使えない状況でも読み進められます。
参考資料
この回の完成版と学習の続き
第9回の見本をブラウザーで開く · 第9回の完成コードをダウンロード
ZIPを展開してindex.htmlを開けば、この回の完成時点から試せます。コードを変更する場合は、ZIP内のファイルをテキストエディターで編集して保存してください。