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);
// React: JSX で宣言的に描画。createElement / appendChild / remove は不要 function Cart() { const [items, setItems] = useState([ { id: 1, name: "コーヒー", price: 400 }, ]); // 追加:配列に push せず、新しい配列を作って setState const addItem = () => setItems([...items, { id: Date.now(), name: "新商品", price: 500 }]); // 削除:filter で除外した新しい配列を setState const removeItem = (id) => setItems(items.filter(i => i.id !== id)); // 全削除 const clearAll = () => setItems([]); return ( <table> <tbody> {items.map(item => ( {/* key 必須:差分更新の識別子 */} <tr key={item.id}> <td>{item.name}</td> <td>¥{item.price.toLocaleString()}</td> <td> <button onClick={() => removeItem(item.id)}>削除</button> </td> </tr> ))} </tbody> </table> ); }
クラス操作
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'); // 置換
// React: className に文字列を組み立てる。state から導出するのが基本 function TabButton({ label, isActive, onClick }) { // 条件分岐で文字列を組む(テンプレリテラル or 三項演算子) const className = `tab ${isActive ? 'active' : ''}`; return ( <button className={className} onClick={onClick}> {label} </button> ); } // 複数クラスは配列で組み立てて join(clsx ライブラリもよく使う) function Item({ selected, disabled }) { const className = [ 'item', selected && 'selected', disabled && 'disabled', ].filter(Boolean).join(' '); return <div className={className}/>; }
スタイル・属性操作
// インラインスタイル 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; // プロパティとして直接設定も可
// React: style は object、属性は prop として直接渡す function PriceInput({ disabled }) { const [price, setPrice] = useState(''); // インラインスタイルは object(キーはキャメルケース) const style = { color: price === '' ? '#999' : 'red', backgroundColor: '#f0f0f0', }; return ( <div data-id="price-row"> <input value={price} onChange={(e) => setPrice(e.target.value)} disabled={disabled} {/* false を渡すと属性ごと消える */} style={style} placeholder="金額" /> </div> ); } // 計算済みスタイルが欲しい場合は useRef + useEffect で DOM 参照 const ref = useRef(null); useEffect(() => { const w = getComputedStyle(ref.current).width; }, []);
フォーム要素の値取得(チェック・ラジオ・セレクト)
// ── チェックボックス ── // 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: 食品" });
// React: 制御コンポーネント(value/checked を state で保持 + onChange で更新) function OrderForm() { const [taxExempt, setTaxExempt] = useState(false); const [options, setOptions] = useState([]); // チェック複数選択 const [payment, setPayment] = useState('cash'); // ラジオ const [category, setCategory] = useState(''); // セレクト const toggleOption = (value) => setOptions(options.includes(value) ? options.filter(o => o !== value) : [...options, value]); return ( <form> {/* チェックボックス(単独)*/} <label> <input type="checkbox" checked={taxExempt} onChange={(e) => setTaxExempt(e.target.checked)} /> 免税対象 </label> {/* チェックボックス(複数、配列管理)*/} {['袋', '箸'].map(v => ( <label key={v}> <input type="checkbox" checked={options.includes(v)} onChange={() => toggleOption(v)} /> {v} </label> ))} {/* ラジオ(name属性でグルーピングは不要、state が真実)*/} {['cash', 'card', 'qr'].map(v => ( <label key={v}> <input type="radio" checked={payment === v} onChange={() => setPayment(v)} /> {v} </label> ))} {/* セレクトボックス */} <select value={category} onChange={(e) => setCategory(e.target.value)}> <option value="">カテゴリを選択</option> <option value="food">食品</option> <option value="drink">飲料</option> </select> </form> ); }
イベントリスナー
// クリック 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 });
// React: onXxx prop でハンドラを渡す。要素にバインドは不要 function Form() { const [value, setValue] = useState(''); // フォーム送信(preventDefault は自動ではない) const handleSubmit = (e) => { e.preventDefault(); console.log({ value }); }; // グローバル keydown は useEffect 内で登録 + cleanup useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') { /* モーダル閉じる */ } if (e.key === 'F2') { /* ファンクションキー */ } if (e.ctrlKey && e.key === 's') { e.preventDefault(); saveData(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); return ( <form onSubmit={handleSubmit}> <input value={value} onChange={(e) => setValue(e.target.value)} {/* input相当 */} onBlur={(e) => console.log('確定:', e.target.value)} /> <button type="button" onClick={() => console.log('クリック')}> ボタン </button> </form> ); }
イベント委譲
// 親要素にリスナーを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); } });
// React: map で描画するので、ハンドラは item を直接クロージャに閉じ込められる // closest や dataset で DOM から辿り直す必要がない function CartTable({ items, onRemove, onChangeQty }) { return ( <tbody> {items.map(item => ( <tr key={item.id}> <td>{item.name}</td> <td> <button onClick={() => onChangeQty(item.id, 'decrease')}>−</button> {item.qty} <button onClick={() => onChangeQty(item.id, 'increase')}>+</button> </td> <td> <button onClick={() => onRemove(item.id)}>削除</button> </td> </tr> ))} </tbody> ); }
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);
// React: useRef で sentinel 参照、useEffect で observer 作成 + cleanup function InfiniteProductList() { const [products, setProducts] = useState([]); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const sentinelRef = useRef(null); // page が変わるたびにデータ取得 useEffect(() => { let cancelled = false; (async () => { const res = await fetch(`/api/products?page=${page}`); const data = await res.json(); if (cancelled) return; if (data.length === 0) setHasMore(false); else setProducts(prev => [...prev, ...data]); })(); return () => { cancelled = true; }; }, [page]); // sentinel を observe、unmount / hasMore変化で disconnect useEffect(() => { if (!hasMore) return; const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) setPage(p => p + 1); }, { rootMargin: '300px' }); observer.observe(sentinelRef.current); return () => observer.disconnect(); // cleanup 必須 }, [hasMore]); return ( <> {products.map(p => <div key={p.id}>{p.name}</div>)} <div ref={sentinelRef}>{hasMore ? '読込中...' : '全表示'}</div> </> ); }