Chapter 03

DOM操作の基本

querySelector, textContent, classList, createElement を使って JavaScript から HTML を操作する方法を学びます。

30 min

このチャプターで学ぶこと

  • querySelector / querySelectorAll で要素を取得できる
  • textContent / innerHTML で要素の中身を変更できる
  • classList で CSS クラスを操作できる
  • createElement / appendChild で要素を動的に追加できる
  • setAttribute / style で属性やスタイルを変更できる

DOM操作の基本

前のチャプターではJavaScriptの変数、関数、配列といった基礎を学びました。このチャプターでは、いよいよJavaScriptを使ってHTMLを動的に操作する方法を学びます。これがブラウザにおけるJavaScriptの本来の役割です。

DOMとは何か

DOM(Document Object Model) とは、HTMLドキュメントをJavaScriptから操作するためのインターフェースです。ブラウザがHTMLを読み込むと、その内容をツリー構造のオブジェクトとして内部に保持します。JavaScriptはそのオブジェクトツリーにアクセスして、要素の取得・変更・追加・削除ができます。

<!-- このHTML が... -->
<body>
  <div id="app">
    <h1 class="title">こんにちは</h1>
    <p>テキストです</p>
  </div>
</body>

上のHTMLはブラウザの中では次のようなツリー構造になります:

document
└── body
    └── div#app
        ├── h1.title  ("こんにちは")
        └── p         ("テキストです")

JavaScriptはこのツリーの各ノード(要素)にアクセスして、中身・スタイル・属性を自由に読み書きできます。

PHPとの考え方の違い

PHPとLaravelでは、サーバー側でBladeテンプレートを使ってHTMLを生成してブラウザに送信します。一度ブラウザに届いたHTMLをPHPが後から変えることはできません。

{{-- Laravelのbladeテンプレート(サーバー側で実行) --}}
<h1>{{ $user->name }}</h1>
<ul>
  @foreach($items as $item)
    <li>{{ $item->name }}</li>
  @endforeach
</ul>

JavaScriptはブラウザ上でHTMLが表示された後に、そのHTMLを書き換えます。ページをリロードせずにコンテンツを変えられるのはこのためです。これはフロントエンド開発における根本的なパラダイムシフトです。

PHP/Laravel: サーバー → HTML生成 → ブラウザに送信(静的)
JavaScript:  ブラウザ上でHTMLをリアルタイムに操作(動的)

要素を取得する

DOM操作の第一歩は「操作したい要素をJavaScriptで取得すること」です。

querySelector

document.querySelector() は、CSSセレクターを使って最初にマッチした要素を取得します。現代のJavaScript開発で最も多用されるメソッドです。

// IDで取得(CSSの #id セレクターと同じ書き方)
const app = document.querySelector('#app');

// クラスで取得(CSSの .class セレクターと同じ書き方)
const title = document.querySelector('.title');

// タグ名で取得(最初の h1 だけ取れる)
const heading = document.querySelector('h1');

// data属性で取得(CSSの属性セレクターと同じ書き方)
const card = document.querySelector('[data-id="42"]');

// ネストしたセレクター(CSSと同じ組み合わせが使える)
const btn = document.querySelector('#app .btn-primary');
const firstLi = document.querySelector('ul > li:first-child');
💡 CSSセレクターをそのまま使える

querySelector の強みは、すでに知っているCSSセレクターの書き方をそのまま使えることです。getElementByIdgetElementsByClassName という古いAPIもありますが、querySelector ひとつで全て対応できるので、基本的にはこちらだけ覚えれば十分です。

要素が見つからない場合は null が返ります。操作する前に確認するのが安全です。

const el = document.querySelector('.maybe-missing');

if (el) {
  // 要素が存在する場合だけ操作する
  el.textContent = '見つかりました';
}

querySelectorAll

document.querySelectorAll() は、マッチしたすべての要素NodeList として返します。

// すべての .card 要素を取得
const cards = document.querySelectorAll('.card');

// NodeList は forEach で繰り返せる
cards.forEach((card) => {
  console.log(card.textContent);
});

// インデックスでアクセスもできる
console.log(cards[0]); // 最初の要素
console.log(cards.length); // 要素数
📝 NodeListと配列の違い

querySelectorAll が返す NodeList は配列に似ていますが、mapfilter などの配列メソッドは使えません(forEach は使えます)。配列として扱いたい場合は Array.from(cards)[...cards] で変換できます。

// 配列に変換してmapを使う
const texts = Array.from(cards).map((card) => card.textContent);

要素の中身を変える

要素を取得したら、その中身を読み書きできます。

textContent

textContent は要素内のテキストを取得・設定します。HTMLタグはそのままテキストとして扱われます。

const title = document.querySelector('h1');

// テキストを読む
console.log(title.textContent); // "こんにちは"

// テキストを書き換える
title.textContent = 'はじめまして';

// 数値や変数も文字列に変換して設定できる
const count = 42;
document.querySelector('#count').textContent = count;

innerHTML

innerHTML は要素内のHTML文字列を取得・設定します。HTMLとして解釈されるため、タグを含むコンテンツを設定できます。

const container = document.querySelector('#container');

// HTMLを設定する
container.innerHTML = '<strong>太字のテキスト</strong>';

// 複数の要素もまとめて設定できる
container.innerHTML = `
  <h2>タイトル</h2>
  <p>本文テキストです。</p>
`;
⚠️ innerHTML とXSSのリスク

innerHTML にユーザーが入力した文字列をそのまま設定すると、XSS(クロスサイトスクリプティング)攻撃の脆弱性につながります。

これはLaravelのBladeテンプレートでいう {{ }}{!! !!} の違いと同じです。

{{-- Laravel: {{ }} は自動エスケープ(安全) --}}
{{ $userInput }}

{{-- Laravel: {!! !!} はエスケープなし(危険) --}}
{!! $userInput !!}
// textContent は自動エスケープ(安全)
el.textContent = userInput; // HTMLタグがそのまま文字として表示される

// innerHTML はエスケープなし(ユーザー入力には使わない)
el.innerHTML = userInput; // <script>タグなども実行される可能性がある

ルール:ユーザーが入力したデータを表示するときは必ず textContent を使う。innerHTML は固定のHTMLテンプレートを設定するときだけ使う。

CSSクラスを操作する

classList を使うと、要素のCSSクラスを動的に追加・削除・切り替えできます。JavaScriptでUIの状態を視覚的に表現するときに頻繁に使います。

const button = document.querySelector('#my-button');

// クラスを追加
button.classList.add('active');

// クラスを削除
button.classList.remove('active');

// クラスがあれば削除、なければ追加(トグル)
button.classList.toggle('open');

// クラスが存在するか確認(true/false)
if (button.classList.contains('disabled')) {
  console.log('このボタンは無効です');
}

// クラスを別のクラスに置き換える
button.classList.replace('btn-primary', 'btn-secondary');

実践例:表示・非表示の切り替え

よくあるパターンとして、クリックで要素の表示・非表示を切り替える実装を見てみましょう。

<!-- HTML -->
<button id="toggle-btn">詳細を表示</button>
<div id="detail" class="hidden">
  <p>ここに詳細コンテンツが表示されます。</p>
</div>
/* CSS */
.hidden {
  display: none;
}
// JavaScript
const btn = document.querySelector('#toggle-btn');
const detail = document.querySelector('#detail');

btn.addEventListener('click', () => {
  // .hidden クラスをトグルする
  detail.classList.toggle('hidden');

  // ボタンのテキストも変える
  if (detail.classList.contains('hidden')) {
    btn.textContent = '詳細を表示';
  } else {
    btn.textContent = '詳細を隠す';
  }
});

addEventListener の詳細は次のチャプターで学びます。ここでは「クリックしたときに何かをする」という書き方として見ておいてください。

要素を作って追加する

document.createElement() を使うと、JavaScriptでHTML要素を一から作成してDOMに追加できます。サーバーからデータを取得してリストを動的に構築するようなケースで使います。

// 1. 要素を作る
const li = document.createElement('li');

// 2. 中身とクラスを設定する
li.textContent = '新しいリストアイテム';
li.classList.add('list-item');

// 3. 親要素に追加する(末尾)
const ul = document.querySelector('#my-list');
ul.appendChild(li);

よく使う追加・削除メソッド

const parent = document.querySelector('#container');
const newEl = document.createElement('div');
newEl.textContent = '新しい要素';

// 末尾に追加
parent.appendChild(newEl);

// 先頭に追加
parent.prepend(newEl);

// 要素を削除
newEl.remove();

// 特定の要素の前に挿入
const referenceEl = document.querySelector('#reference');
parent.insertBefore(newEl, referenceEl);

実践例:配列からリストを構築する

データの配列を受け取って、DOM要素を動的に生成するパターンです。

const fruits = ['りんご', 'バナナ', 'オレンジ', 'ぶどう'];

const ul = document.querySelector('#fruit-list');

// forEach で各アイテムのDOM要素を作って追加
fruits.forEach((fruit) => {
  const li = document.createElement('li');
  li.textContent = fruit;
  ul.appendChild(li);
});

Laravelで言えば、@foreach でBladeテンプレートを展開するのと同じ発想です。ただしこちらはサーバーではなくブラウザ上でHTML要素を生成しています。

属性とスタイルを操作する

setAttribute / getAttribute

const img = document.querySelector('img');

// 属性を設定する
img.setAttribute('src', '/images/photo.jpg');
img.setAttribute('alt', '写真の説明');

// 属性を読む
const src = img.getAttribute('src');
console.log(src); // "/images/photo.jpg"

// 属性を削除する
img.removeAttribute('alt');

// 真偽値属性(disabled, checked など)
const input = document.querySelector('input');
input.setAttribute('disabled', '');  // 無効化
input.removeAttribute('disabled');   // 有効化

data-* 属性(dataset)

HTMLの data-* カスタム属性は、JavaScript側から dataset プロパティで簡単にアクセスできます。

<!-- HTMLでdata属性を設定 -->
<button data-user-id="123" data-action="delete">削除</button>
const btn = document.querySelector('button');

// ケバブケース(data-user-id)は キャメルケース(userId)に変換される
console.log(btn.dataset.userId);  // "123"
console.log(btn.dataset.action);  // "delete"

// 書き込みもできる
btn.dataset.status = 'pending';
// → <button data-status="pending"> になる

element.style でスタイルを直接指定する

const box = document.querySelector('#box');

// CSSプロパティはキャメルケースで指定する
box.style.backgroundColor = '#3b82f6'; // background-color
box.style.fontSize = '1.5rem';         // font-size
box.style.display = 'none';            // display

// 読み取りもできる
console.log(box.style.backgroundColor);
📝 style より classList を優先する

element.style で直接スタイルを書くと、スタイルがJavaScriptのコードに散らばり管理が難しくなります。基本的にはCSSクラスを classList で付け外しする方法を使い、スタイルはCSSファイルに書くようにしましょう。style の直接設定は、ユーザーの操作によって決まる動的な値(座標、サイズなど)を設定するときだけ使うのが理想です。

✍ やってみよう:プロフィールカードを動的に生成する

JavaScriptのオブジェクトからプロフィールカードを動的に生成して、#app 要素に追加してみましょう。

用意するHTML:

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>プロフィールカード</title>
  <style>
    .card { border: 1px solid #e2e8f0; border-radius: 12px; padding: 1.5rem; max-width: 400px; }
    .card h2 { margin: 0 0 0.25rem; }
    .card .role { color: #64748b; margin: 0 0 0.75rem; font-size: 0.875rem; }
    .card .bio { margin: 0 0 1rem; }
    .skills { display: flex; gap: 0.5rem; flex-wrap: wrap; list-style: none; padding: 0; margin: 0; }
    .skills li { background: #eff6ff; color: #2563eb; padding: 0.2rem 0.75rem; border-radius: 999px; font-size: 0.875rem; }
  </style>
</head>
<body>
  <div id="app"></div>
  <script src="main.js"></script>
</body>
</html>

作成するJavaScript(main.js):

// プロフィールデータ
const profile = {
  name: '山田太郎',
  role: 'フルスタックエンジニア',
  bio: 'LaravelとReactが得意なエンジニアです。最近はTypeScriptの勉強中。',
  skills: ['Laravel', 'React', 'MySQL', 'Docker'],
};

// ここからDOM操作で要素を作る
const app = document.querySelector('#app');

// 1. カードのdivを作る
const card = document.createElement('div');
card.classList.add('card');

// 2. 名前の見出しを作る
const name = document.createElement('h2');
name.textContent = profile.name;

// 3. 役職のpを作る
const role = document.createElement('p');
role.classList.add('role');
role.textContent = profile.role;

// 4. 自己紹介のpを作る
const bio = document.createElement('p');
bio.classList.add('bio');
bio.textContent = profile.bio;

// 5. スキルリストを作る(ulとliの組み合わせ)
const skillList = document.createElement('ul');
skillList.classList.add('skills');

profile.skills.forEach((skill) => {
  const li = document.createElement('li');
  li.textContent = skill;
  skillList.appendChild(li);
});

// 6. カードに全部追加する
card.appendChild(name);
card.appendChild(role);
card.appendChild(bio);
card.appendChild(skillList);

// 7. appに追加する
app.appendChild(card);

発展課題: データを複数人分の配列にして、forEach でカードを複数枚生成してみましょう。

const members = [
  { name: '山田太郎', role: 'バックエンド', bio: 'Laravel歴5年', skills: ['Laravel', 'PHP'] },
  { name: '田中花子', role: 'フロントエンド', bio: 'ReactとVueが得意', skills: ['React', 'Vue'] },
];

members.forEach((member) => {
  // 上記のカード生成ロジックを関数化して呼び出す
});

まとめ

このチャプターで学んだDOM操作の基本をまとめます:

次のチャプターでは、クリックや入力などのイベント処理を学びます。DOM操作とイベントを組み合わせることで、本格的なインタラクティブなUIが作れるようになります。