Skip to Content
Lecture第9回:DOM操作とイベント処理

第9回:DOM操作とイベント処理

🎯 学習目標

  • DOMの概念を理解する
  • 要素の取得・操作ができる
  • イベント処理を実装できる
  • ToDoリストアプリを作成できる

📚 導入(5分)

静的HTMLから動的Webページへ

これまで作成してきたWebページは「静的」でした。JavaScriptのDOM操作により、ユーザーの操作に応じて内容が変化する「動的」なWebページを作成できます。

<!-- 静的:常に同じ内容 --> <p>こんにちは</p> <!-- 動的:JavaScriptで内容を変更 --> <p id="greeting">こんにちは</p> <script> document.getElementById('greeting').textContent = '動的に変更されました!'; </script>

ユーザーインタラクションの重要性

現代のWebアプリケーションでは、以下のような双方向性が重要です:

  • フォームの入力検証:リアルタイムなエラー表示
  • 動的なコンテンツ更新:ページ全体を再読み込みせずに更新
  • レスポンシブな操作感:即座の視覚的フィードバック

💡 理論学習(30分)

DOM(Document Object Model)(15分)

DOMの概念とHTML要素へのアクセス方法

DOMは、HTMLドキュメントをJavaScriptで操作するためのインターフェースです。

<!DOCTYPE html> <html> <!-- document.documentElement --> <head> <!-- document.head --> <title>サンプル</title> </head> <body> <!-- document.body --> <div id="container"> <!-- 要素ノード --> <p class="text"> <!-- 子要素 --> テキスト <!-- テキストノード --> </p> </div> </body> </html>

要素の取得方法

// IDで取得(最も高速) const element = document.getElementById('myId'); // クラス名で取得(複数の要素を返す) const elements = document.getElementsByClassName('myClass'); console.log(elements[0]); // 最初の要素 console.log(elements.length); // 要素数 // タグ名で取得(複数の要素を返す) const paragraphs = document.getElementsByTagName('p'); for (let p of paragraphs) { console.log(p.textContent); } // CSSセレクタで取得(最初の1つ) const firstButton = document.querySelector('.button'); const firstParagraph = document.querySelector('p'); const complexSelector = document.querySelector('.container > p:first-child'); // CSSセレクタで取得(すべて) const allButtons = document.querySelectorAll('.button'); const allParagraphs = document.querySelectorAll('p'); // 現代的な推奨方法 allButtons.forEach(button => { console.log(button.textContent); });

要素の操作方法

内容の変更
const element = document.getElementById('myElement'); // テキストのみを変更(HTMLタグは無効化) element.textContent = '新しいテキスト'; element.textContent = '<strong>太字</strong>'; // タグは文字として表示 // HTMLを含む内容を変更(セキュリティ注意) element.innerHTML = '<strong>太字</strong>'; // HTMLとして解釈される element.innerHTML = ` <p>段落1</p> <p>段落2</p> `; // 安全なHTML挿入(DOM要素を作成) const newParagraph = document.createElement('p'); newParagraph.textContent = '新しい段落'; element.appendChild(newParagraph);
スタイルの変更
const element = document.getElementById('myElement'); // 個別のスタイル変更 element.style.color = 'red'; element.style.backgroundColor = 'yellow'; element.style.fontSize = '20px'; element.style.marginTop = '10px'; // 複数のスタイルを一度に変更 element.style.cssText = 'color: red; background-color: yellow; font-size: 20px;'; // CSSクラスの操作(推奨) element.className = 'new-class'; // クラスを置き換え element.classList.add('active'); // クラスを追加 element.classList.remove('inactive'); // クラスを削除 element.classList.toggle('visible'); // クラスの切り替え element.classList.contains('hidden'); // クラスの存在確認 // 複数クラスの操作 element.classList.add('class1', 'class2', 'class3'); element.classList.remove('old-class1', 'old-class2');
属性の操作
const link = document.querySelector('a'); const input = document.querySelector('input'); // 属性の取得 const href = link.getAttribute('href'); const id = link.getAttribute('id'); // 属性の設定 link.setAttribute('href', 'https://example.com'); link.setAttribute('target', '_blank'); input.setAttribute('placeholder', '入力してください'); // 属性の削除 link.removeAttribute('target'); // 真偽値属性の操作 input.disabled = true; // disabled属性を追加 input.disabled = false; // disabled属性を削除 input.checked = true; // チェックボックス・ラジオボタン input.hidden = true; // 要素を非表示 // データ属性の操作 const element = document.querySelector('[data-user-id]'); element.dataset.userId = '123'; // data-user-id="123" element.dataset.userName = 'タロウ'; // data-user-name="タロウ" console.log(element.dataset.userId); // "123"
要素の作成・追加・削除
// 新しい要素の作成 const newDiv = document.createElement('div'); const newParagraph = document.createElement('p'); const newButton = document.createElement('button'); // 内容とスタイルの設定 newParagraph.textContent = '新しい段落です'; newParagraph.className = 'paragraph'; newButton.textContent = 'クリック'; newButton.id = 'new-button'; // 要素の追加 const container = document.getElementById('container'); container.appendChild(newParagraph); // 最後に追加 container.insertBefore(newDiv, newParagraph); // 指定要素の前に挿入 // より現代的な挿入方法 container.append(newButton); // 最後に追加(文字列も可) container.prepend(newDiv); // 最初に追加 newParagraph.after(newButton); // 要素の後に挿入 newParagraph.before(newDiv); // 要素の前に挿入 // 要素の削除 const elementToRemove = document.getElementById('remove-me'); elementToRemove.remove(); // 現代的な方法 // elementToRemove.parentNode.removeChild(elementToRemove); // 古い方法 // 要素の置換 const oldElement = document.getElementById('old'); const newElement = document.createElement('div'); newElement.textContent = '新しい要素'; oldElement.replaceWith(newElement);

イベント処理(15分)

addEventListenerの使用方法

const button = document.getElementById('myButton'); // 基本的な使用方法 button.addEventListener('click', function() { console.log('ボタンがクリックされました'); }); // アロー関数での記述 button.addEventListener('click', () => { console.log('ボタンがクリックされました'); }); // 名前付き関数での実装(推奨) function handleClick(event) { console.log('クリックされた要素:', event.target); console.log('イベントの種類:', event.type); } button.addEventListener('click', handleClick); // 複数のイベントリスナー button.addEventListener('click', handleClick); button.addEventListener('mouseenter', handleMouseEnter); button.addEventListener('mouseleave', handleMouseLeave); // イベントリスナーの削除 button.removeEventListener('click', handleClick);

イベントオブジェクト

function handleEvent(event) { // イベントの基本情報 console.log('イベント種類:', event.type); console.log('発生元要素:', event.target); console.log('リスナーが登録された要素:', event.currentTarget); // マウスイベントの場合 if (event.type === 'click') { console.log('クリック位置:', event.clientX, event.clientY); console.log('Shiftキー:', event.shiftKey); console.log('Ctrlキー:', event.ctrlKey); } // デフォルト動作の阻止 event.preventDefault(); // リンクの遷移やフォーム送信を阻止 // イベント伝播の停止 event.stopPropagation(); // 親要素へのイベント伝播を阻止 }

主要なイベントタイプ

// マウスイベント element.addEventListener('click', handleClick); // クリック element.addEventListener('dblclick', handleDblClick); // ダブルクリック element.addEventListener('mousedown', handleMouseDown); // マウスボタン押下 element.addEventListener('mouseup', handleMouseUp); // マウスボタン離上 element.addEventListener('mousemove', handleMouseMove); // マウス移動 element.addEventListener('mouseenter', handleMouseEnter); // マウス侵入 element.addEventListener('mouseleave', handleMouseLeave); // マウス離脱 // キーボードイベント document.addEventListener('keydown', handleKeyDown); // キー押下 document.addEventListener('keyup', handleKeyUp); // キー離上 input.addEventListener('keypress', handleKeyPress); // 文字キー入力 // フォームイベント form.addEventListener('submit', handleSubmit); // フォーム送信 input.addEventListener('change', handleChange); // 値変更(フォーカス離脱時) input.addEventListener('input', handleInput); // 値変更(リアルタイム) input.addEventListener('focus', handleFocus); // フォーカス取得 input.addEventListener('blur', handleBlur); // フォーカス離脱 // ページイベント window.addEventListener('load', handleLoad); // ページ読み込み完了 document.addEventListener('DOMContentLoaded', handleDOMReady); // DOM構築完了 window.addEventListener('resize', handleResize); // ウィンドウリサイズ window.addEventListener('scroll', handleScroll); // スクロール // カスタムイベント const customEvent = new CustomEvent('myCustomEvent', { detail: { message: 'カスタムデータ' } }); element.dispatchEvent(customEvent); element.addEventListener('myCustomEvent', function(e) { console.log(e.detail.message); });

イベント委譲(Event Delegation)

// 効率的なイベント処理(動的に追加される要素にも対応) const container = document.getElementById('container'); container.addEventListener('click', function(event) { // クリックされた要素がボタンかチェック if (event.target.matches('button')) { console.log('ボタンがクリックされました:', event.target.textContent); } // クラス名でのチェック if (event.target.classList.contains('delete-btn')) { const item = event.target.closest('.item'); item.remove(); } }); // 動的に追加されるボタンも自動的に対応される const newButton = document.createElement('button'); newButton.textContent = '新しいボタン'; container.appendChild(newButton); // 自動的にクリックイベントが有効

🛠️ 実習(50分)

ToDoリストアプリ作成(45分)

HTML構造

<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ToDoリスト</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div class="container"> <header class="header"> <h1 class="title">📝 ToDoリスト</h1> <div class="stats" id="stats"> <span class="stat">全体: <span id="totalCount">0</span></span> <span class="stat">完了: <span id="completedCount">0</span></span> <span class="stat">未完了: <span id="pendingCount">0</span></span> </div> </header> <main class="main"> <!-- ToDo入力フォーム --> <form class="todo-form" id="todoForm"> <div class="input-group"> <input type="text" id="todoInput" class="todo-input" placeholder="新しいタスクを入力..." required autocomplete="off" > <button type="submit" class="add-btn">追加</button> </div> </form> <!-- フィルター --> <div class="filters"> <button class="filter-btn active" data-filter="all">すべて</button> <button class="filter-btn" data-filter="pending">未完了</button> <button class="filter-btn" data-filter="completed">完了済み</button> </div> <!-- ToDoリスト --> <ul class="todo-list" id="todoList"> <!-- 動的に生成される --> </ul> <!-- 空の状態 --> <div class="empty-state" id="emptyState"> <p>📋 タスクがありません</p> <p>上記の入力欄から新しいタスクを追加してください</p> </div> </main> <!-- 確認モーダル --> <div class="modal" id="confirmModal"> <div class="modal-content"> <h3>確認</h3> <p id="confirmMessage">この操作を実行しますか?</p> <div class="modal-actions"> <button class="btn btn-secondary" id="cancelBtn">キャンセル</button> <button class="btn btn-danger" id="confirmBtn">実行</button> </div> </div> </div> <div class="modal-overlay" id="modalOverlay"></div> </div> <script src="js/script.js"></script> </body> </html>

CSS(css/style.css)

/* リセットとベーススタイル */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 600px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); overflow: hidden; } /* ヘッダー */ .header { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 30px; text-align: center; } .title { font-size: 2rem; margin-bottom: 15px; } .stats { display: flex; justify-content: center; gap: 20px; font-size: 0.9rem; opacity: 0.9; } .stat { background: rgba(255, 255, 255, 0.2); padding: 5px 12px; border-radius: 15px; } /* メイン */ .main { padding: 30px; } /* フォーム */ .todo-form { margin-bottom: 25px; } .input-group { display: flex; gap: 10px; } .todo-input { flex: 1; padding: 15px; border: 2px solid #e0e0e0; border-radius: 10px; font-size: 16px; transition: all 0.3s ease; } .todo-input:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } .add-btn { padding: 15px 20px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; border-radius: 10px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } .add-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); } /* フィルター */ .filters { display: flex; gap: 5px; margin-bottom: 25px; } .filter-btn { padding: 8px 16px; background: #f5f5f5; border: none; border-radius: 20px; cursor: pointer; transition: all 0.3s ease; font-size: 14px; } .filter-btn.active { background: #667eea; color: white; } .filter-btn:hover:not(.active) { background: #e0e0e0; } /* ToDoリスト */ .todo-list { list-style: none; display: flex; flex-direction: column; gap: 10px; } .todo-item { background: #f8f9fa; border: 2px solid transparent; border-radius: 10px; padding: 15px; display: flex; align-items: center; gap: 12px; transition: all 0.3s ease; animation: slideIn 0.3s ease; } @keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .todo-item:hover { border-color: #667eea; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); } .todo-item.completed { background: #e8f5e8; border-color: #28a745; } .todo-item.completed .todo-text { text-decoration: line-through; opacity: 0.6; } .todo-checkbox { width: 20px; height: 20px; cursor: pointer; } .todo-text { flex: 1; font-size: 16px; word-break: break-word; } .todo-actions { display: flex; gap: 5px; } .btn { padding: 6px 12px; border: none; border-radius: 5px; cursor: pointer; font-size: 12px; transition: all 0.3s ease; } .btn-edit { background: #ffc107; color: #000; } .btn-delete { background: #dc3545; color: white; } .btn:hover { transform: translateY(-1px); box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); } /* 空の状態 */ .empty-state { text-align: center; padding: 40px; color: #666; } .empty-state.hidden { display: none; } /* モーダル */ .modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border-radius: 10px; padding: 30px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); z-index: 1001; opacity: 0; visibility: hidden; transition: all 0.3s ease; } .modal.active { opacity: 1; visibility: visible; } .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; opacity: 0; visibility: hidden; transition: all 0.3s ease; } .modal-overlay.active { opacity: 1; visibility: visible; } .modal h3 { margin-bottom: 15px; color: #333; } .modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; } .btn-secondary { background: #6c757d; color: white; } .btn-danger { background: #dc3545; color: white; } /* レスポンシブ */ @media (max-width: 600px) { .container { margin: 10px; border-radius: 10px; } .header, .main { padding: 20px; } .stats { flex-direction: column; gap: 10px; } .input-group { flex-direction: column; } .filters { flex-wrap: wrap; } }

JavaScript(js/script.js)

// アプリケーション状態管理 let todos = []; let currentFilter = 'all'; let editingId = null; // DOM要素の取得 const todoForm = document.getElementById('todoForm'); const todoInput = document.getElementById('todoInput'); const todoList = document.getElementById('todoList'); const emptyState = document.getElementById('emptyState'); const filterBtns = document.querySelectorAll('.filter-btn'); const confirmModal = document.getElementById('confirmModal'); const modalOverlay = document.getElementById('modalOverlay'); const confirmBtn = document.getElementById('confirmBtn'); const cancelBtn = document.getElementById('cancelBtn'); const confirmMessage = document.getElementById('confirmMessage'); // 統計要素 const totalCount = document.getElementById('totalCount'); const completedCount = document.getElementById('completedCount'); const pendingCount = document.getElementById('pendingCount'); // ユニークIDの生成 function generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } // ToDoの追加 function addTodo(text) { const todo = { id: generateId(), text: text.trim(), completed: false, createdAt: new Date() }; todos.unshift(todo); // 配列の先頭に追加 updateDisplay(); updateStats(); // アニメーション効果 const todoElement = document.querySelector(`[data-id="${todo.id}"]`); if (todoElement) { todoElement.style.animation = 'slideIn 0.3s ease'; } } // ToDoの削除 function deleteTodo(id) { todos = todos.filter(todo => todo.id !== id); updateDisplay(); updateStats(); } // ToDoの完了状態の切り替え function toggleTodo(id) { todos = todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ); updateDisplay(); updateStats(); } // ToDoの編集 function editTodo(id, newText) { todos = todos.map(todo => todo.id === id ? { ...todo, text: newText.trim() } : todo ); updateDisplay(); updateStats(); } // フィルタリング function getFilteredTodos() { switch (currentFilter) { case 'completed': return todos.filter(todo => todo.completed); case 'pending': return todos.filter(todo => !todo.completed); default: return todos; } } // 表示の更新 function updateDisplay() { const filteredTodos = getFilteredTodos(); if (filteredTodos.length === 0) { todoList.innerHTML = ''; emptyState.classList.remove('hidden'); return; } emptyState.classList.add('hidden'); todoList.innerHTML = filteredTodos.map(todo => ` <li class="todo-item ${todo.completed ? 'completed' : ''}" data-id="${todo.id}"> <input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''} onchange="toggleTodo('${todo.id}')" > <span class="todo-text" ondblclick="startEdit('${todo.id}')">${escapeHtml(todo.text)}</span> <div class="todo-actions"> <button class="btn btn-edit" onclick="startEdit('${todo.id}')">編集</button> <button class="btn btn-delete" onclick="confirmDelete('${todo.id}')">削除</button> </div> </li> `).join(''); } // 統計の更新 function updateStats() { const total = todos.length; const completed = todos.filter(todo => todo.completed).length; const pending = total - completed; totalCount.textContent = total; completedCount.textContent = completed; pendingCount.textContent = pending; } // HTMLエスケープ(XSS対策) function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 編集開始 function startEdit(id) { const todo = todos.find(t => t.id === id); if (!todo) return; const todoElement = document.querySelector(`[data-id="${id}"] .todo-text`); const originalText = todo.text; todoElement.innerHTML = ` <input type="text" class="edit-input" value="${escapeHtml(originalText)}" style="width: 100%; padding: 5px; border: 1px solid #ddd; border-radius: 3px;" > `; const input = todoElement.querySelector('.edit-input'); input.focus(); input.select(); // Enterキーで保存 input.addEventListener('keypress', function(e) { if (e.key === 'Enter') { finishEdit(id, input.value); } }); // Escapeキーでキャンセル input.addEventListener('keyup', function(e) { if (e.key === 'Escape') { cancelEdit(id, originalText); } }); // フォーカスが外れたら保存 input.addEventListener('blur', function() { finishEdit(id, input.value); }); } // 編集完了 function finishEdit(id, newText) { if (newText.trim() === '') { deleteTodo(id); } else { editTodo(id, newText); } } // 編集キャンセル function cancelEdit(id, originalText) { const todoElement = document.querySelector(`[data-id="${id}"] .todo-text`); todoElement.textContent = originalText; } // 削除確認 function confirmDelete(id) { const todo = todos.find(t => t.id === id); if (!todo) return; confirmMessage.textContent = `"${todo.text}" を削除しますか?`; showModal(); // 確認ボタンのイベントを設定 confirmBtn.onclick = function() { deleteTodo(id); hideModal(); }; } // モーダル表示 function showModal() { confirmModal.classList.add('active'); modalOverlay.classList.add('active'); document.body.style.overflow = 'hidden'; } // モーダル非表示 function hideModal() { confirmModal.classList.remove('active'); modalOverlay.classList.remove('active'); document.body.style.overflow = ''; } // イベントリスナーの設定 todoForm.addEventListener('submit', function(e) { e.preventDefault(); const text = todoInput.value.trim(); if (text === '') return; addTodo(text); todoInput.value = ''; todoInput.focus(); }); // フィルターボタン filterBtns.forEach(btn => { btn.addEventListener('click', function() { // アクティブ状態の更新 filterBtns.forEach(b => b.classList.remove('active')); this.classList.add('active'); // フィルター更新 currentFilter = this.dataset.filter; updateDisplay(); }); }); // モーダル関連 cancelBtn.addEventListener('click', hideModal); modalOverlay.addEventListener('click', hideModal); // Escapeキーでモーダルを閉じる document.addEventListener('keyup', function(e) { if (e.key === 'Escape') { hideModal(); } }); // 初期化 document.addEventListener('DOMContentLoaded', function() { updateDisplay(); updateStats(); todoInput.focus(); }); // キーボードショートカット document.addEventListener('keydown', function(e) { // Ctrl+Enter で新しいToDo追加にフォーカス if (e.ctrlKey && e.key === 'Enter') { todoInput.focus(); } }); // LocalStorageでの永続化(オプション) function saveTodos() { localStorage.setItem('todos', JSON.stringify(todos)); } function loadTodos() { const saved = localStorage.getItem('todos'); if (saved) { todos = JSON.parse(saved); updateDisplay(); updateStats(); } } // ページ読み込み時にデータを復元 window.addEventListener('load', loadTodos); // データ変更時に自動保存(すべての変更関数を拡張) const originalAddTodo = addTodo; const originalDeleteTodo = deleteTodo; const originalToggleTodo = toggleTodo; const originalEditTodo = editTodo; addTodo = function(...args) { originalAddTodo.apply(this, args); saveTodos(); }; deleteTodo = function(...args) { originalDeleteTodo.apply(this, args); saveTodos(); }; toggleTodo = function(...args) { originalToggleTodo.apply(this, args); saveTodos(); }; editTodo = function(...args) { originalEditTodo.apply(this, args); saveTodos(); };

📝 まとめ・質疑応答(5分)

DOM操作とイベント処理の確認

✅ マスターしたスキル

  • 要素の取得(getElementById, querySelector等)
  • 要素の内容変更(textContent, innerHTML)
  • スタイル変更(style, classList)
  • 要素の作成・追加・削除
  • イベントリスナーの設定
  • イベントオブジェクトの活用

次回予告:JavaScript応用とAPI

次回学習する内容:

  • 配列とオブジェクトの高度な操作
  • 非同期処理(Promise、async/await)
  • API連携(fetch)
  • JSONデータの扱い

🏠 宿題

  1. ToDoリストの機能拡張

    • ドラッグ&ドロップでの並び替え
    • 期限設定機能
    • カテゴリ分類機能
  2. 電卓アプリの作成

    • 四則演算機能
    • 計算履歴表示
    • キーボード操作対応
  3. 画像ギャラリーの作成

    • 画像のクリックで拡大表示
    • スライドショー機能
    • フィルター機能

📚 参考リソース


次回もお楽しみに! 🚀

Last updated on