POS開発 フロントエンド チートコード — DOM

5. DOM 操作

要素の取得

// 単一要素の取得
const el = document.querySelector('.class-name');    // CSSセレクタで最初の1つ
const el = document.getElementById('myId');          // IDで取得(#不要)

// 複数要素の取得(NodeList → forEachが使える)
const items = document.querySelectorAll('.item');
items.forEach(item => { /* ... */ });

// NodeListを配列に変換(map/filterを使いたい場合)
const arr = [...document.querySelectorAll('.item')];
const arr = Array.from(document.querySelectorAll('.item'));

// 親・子・兄弟の参照
el.parentElement;            // 親要素
el.children;                 // 子要素一覧(HTMLCollection)
el.firstElementChild;        // 最初の子要素
el.lastElementChild;         // 最後の子要素
el.nextElementSibling;       // 次の兄弟要素
el.previousElementSibling;   // 前の兄弟要素
el.closest('.wrapper');       // 祖先を遡って検索

要素の作成・追加・削除

// 要素の作成
const div = document.createElement('div');
div.textContent = "テキスト";                // テキストのみ設定(安全)
div.innerHTML = '<span>HTML</span>';       // HTMLとして設定(XSS注意)

// 追加
parent.appendChild(div);             // 末尾に追加
parent.prepend(div);                  // 先頭に追加
parent.insertBefore(div, reference);  // 指定要素の前に追加
el.after(newEl);                       // 要素の直後に追加
el.before(newEl);                      // 要素の直前に追加

// insertAdjacentHTML(文字列HTMLを位置指定で挿入)
// 位置: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'
el.insertAdjacentHTML('beforeend', `
  <tr>
    <td>${name}</td>
    <td>¥${price.toLocaleString()}</td>
  </tr>
`);

// 削除
el.remove();                          // 自身を削除
parent.removeChild(child);             // 子要素を削除
parent.innerHTML = '';                // 子要素を全削除

// 要素の置換
parent.replaceChild(newEl, oldEl);

クラス操作

el.classList.add('active');         // 追加
el.classList.add('a', 'b');         // 複数追加
el.classList.remove('active');      // 削除
el.classList.toggle('active');      // 切替(あれば削除、なければ追加)
el.classList.contains('active');    // 存在チェック → true/false
el.classList.replace('old', 'new'); // 置換

スタイル・属性操作

// インラインスタイル
el.style.color = 'red';
el.style.display = 'none';
el.style.backgroundColor = '#f0f0f0';  // ケバブ→キャメルケース

// 計算済みスタイルの取得
const styles = getComputedStyle(el);
styles.width;  // "200px"

// 属性の取得・設定
el.getAttribute('data-id');
el.setAttribute('disabled', 'true');
el.removeAttribute('disabled');
el.hasAttribute('disabled');    // true/false

// フォーム値の取得・設定
const input = document.getElementById('price');
input.value;            // 取得(文字列として)
input.value = "1000";   // 設定
input.disabled = true;  // プロパティとして直接設定も可

フォーム要素の値取得(チェック・ラジオ・セレクト)

// ── チェックボックス ──
// HTML: <input type="checkbox" id="taxExempt"> 免税対象
const taxExempt = document.getElementById('taxExempt');

taxExempt.checked;            // true or false(ON/OFFの状態)
taxExempt.checked = true;     // プログラムでONにする

// 条件分岐で使う
if (taxExempt.checked) {
  console.log("免税処理を適用");
}

// 変更を検知
taxExempt.addEventListener('change', (e) => {
  console.log(e.target.checked);  // true / false
});

// 複数チェックボックスの選択値をまとめて取得
// HTML: <input type="checkbox" name="options" value="袋"> 袋
//       <input type="checkbox" name="options" value="箸"> 箸
const checked = [...document.querySelectorAll('input[name="options"]:checked')]
  .map(el => el.value);
// 例: ["袋"](チェックされたものだけ配列になる)

// ── ラジオボタン ──
// HTML: <input type="radio" name="payment" value="cash"> 現金
//       <input type="radio" name="payment" value="card"> カード
//       <input type="radio" name="payment" value="qr"> QR決済
const selected = document.querySelector('input[name="payment"]:checked');
const paymentMethod = selected ? selected.value : null;
// "cash" / "card" / "qr" / null(未選択時)

// 変更を検知(name属性でグルーピング)
document.querySelectorAll('input[name="payment"]').forEach(radio => {
  radio.addEventListener('change', (e) => {
    console.log(`決済方法: ${e.target.value}`);
  });
});

// ── セレクトボックス ──
// HTML: <select id="category">
//         <option value="">カテゴリを選択</option>
//         <option value="food">食品</option>
//         <option value="drink">飲料</option>
//       </select>
const select = document.getElementById('category');

select.value;                  // 選択中のoptionのvalue(例: "food")
select.selectedIndex;          // 選択中のインデックス(0始まり)
select.options;                // 全optionのHTMLCollection
select.options[select.selectedIndex].text;  // 表示テキスト(例: "食品")

// プログラムで選択を変更
select.value = "drink";

// 変更を検知
select.addEventListener('change', (e) => {
  const text = e.target.options[e.target.selectedIndex].text;
  console.log(`${e.target.value}: ${text}`);  // "food: 食品"
});

イベントリスナー

// クリック
btn.addEventListener('click', () => {
  console.log('クリックされた');
});

// 入力変更(リアルタイム、キー入力ごとに発火)
input.addEventListener('input', (e) => {
  console.log(e.target.value);
});

// 値の確定(フォーカスが外れた時、Enterキーなど)
input.addEventListener('change', (e) => {
  console.log(e.target.value);
});

// フォーム送信
form.addEventListener('submit', (e) => {
  e.preventDefault();  // ページリロードを防止
  const formData = new FormData(form);
  console.log(Object.fromEntries(formData));
});

// キーボード
document.addEventListener('keydown', (e) => {
  if (e.key === 'Enter')    { /* Enter処理 */ }
  if (e.key === 'Escape')   { /* モーダル閉じるなど */ }
  if (e.key === 'F2')       { /* ファンクションキー */ }
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();  // ブラウザのデフォルトを防止
    saveData();
  }
});

// リスナーの削除(参照を保持しておく必要がある)
const handler = () => console.log('click');
btn.addEventListener('click', handler);
btn.removeEventListener('click', handler);

// 一度だけ実行するリスナー
btn.addEventListener('click', handler, { once: true });

イベント委譲

// 親要素にリスナーを1つだけ設定(動的に追加された子要素にも対応)
document.getElementById('cartBody').addEventListener('click', (e) => {
  // 削除ボタン
  const deleteBtn = e.target.closest('.delete-btn');
  if (deleteBtn) {
    const row = deleteBtn.closest('tr');
    row.remove();
    return;
  }

  // 数量変更ボタン
  const qtyBtn = e.target.closest('.qty-btn');
  if (qtyBtn) {
    const action = qtyBtn.dataset.action;  // "increase" or "decrease"
    updateQuantity(qtyBtn.closest('tr'), action);
  }
});

DOMContentLoaded / ページ読み込み

// DOM構築完了後に実行(画像読み込みを待たない)
document.addEventListener('DOMContentLoaded', () => {
  init();
});

// 全リソース読み込み完了後に実行
window.addEventListener('load', () => {
  hideLoadingScreen();
});

// scriptタグにdefer属性を付ければDOMContentLoadedは不要
// <script src="app.js" defer></script>

16. 高度なDOM操作 中級

カスタムイベント(コンポーネント間通信)

// カスタムイベントを発火
function addToCart(product) {
  cart.push(product);

  // detail にデータを乗せてイベント発火
  document.dispatchEvent(new CustomEvent('cart:updated', {
    detail: {
      items: [...cart],
      total: cart.reduce((s, i) => s + i.price * i.qty, 0),
    },
  }));
}

// 別の場所でリッスン
document.addEventListener('cart:updated', (e) => {
  const { items, total } = e.detail;
  document.getElementById('itemCount').textContent = items.length;
  document.getElementById('total').textContent = `¥${total.toLocaleString()}`;
});

// 特定要素でバブルさせたい場合
element.dispatchEvent(new CustomEvent('item:selected', {
  detail: { id: 42 },
  bubbles: true,  // 親要素にも伝搬
}));

MutationObserver(DOM変更の監視)

// テーブルの行数変化を監視
const tbody = document.querySelector('#cartTable tbody');

const observer = new MutationObserver((mutations) => {
  mutations.forEach(m => {
    if (m.type === 'childList') {
      const rowCount = tbody.rows.length;
      document.getElementById('itemCount').textContent = rowCount;
      console.log(`行数変化: ${rowCount}行`);
    }
  });
});

// 監視を開始
observer.observe(tbody, {
  childList: true,   // 子要素の追加・削除
  subtree: false,    // 孫要素は監視しない
});

// 監視を停止
// observer.disconnect();

IntersectionObserver(無限スクロール)

// 商品カタログの無限スクロール
let page = 1;
let loading = false;

const sentinel = document.getElementById('loadMore');

const observer = new IntersectionObserver(async (entries) => {
  const entry = entries[0];
  if (!entry.isIntersecting || loading) return;

  loading = true;
  sentinel.textContent = '読み込み中...';

  try {
    const res = await fetch(`/api/products?page=${page}`);
    const products = await res.json();

    if (products.length === 0) {
      observer.disconnect();
      sentinel.textContent = '全商品を表示しました';
      return;
    }

    renderProducts(products);
    page++;
  } finally {
    loading = false;
    sentinel.textContent = '';
  }
}, { rootMargin: '300px' });

observer.observe(sentinel);