【脱初心者】JavaScriptでUIをclassで管理する考え方

※本ブログの目的は個人の備忘録であり、コードは参考用として掲載しています。
実際に使用される際は、ご自身の環境で十分に動作確認を行ってください。
コードの利用によって生じたいかなる問題についても責任を負いかねますので、あらかじめご了承ください。

はじめに

JavaScriptでUIの制御を書くとき、
「querySelectorで要素を取得し、addEventListenerでイベントを登録し、classListで見た目を切り替える」
という書き方をしている人は多いと思います。

実際、ちょっとしたUIであればこの方法で何の問題もありません。

1つのボタンをクリックしたら見た目が変わる、といった程度であれば十分です。

ただ、ヘッダーやメニュー、アコーディオンのように少しずつ仕様が増えてくると、コードがどんどん読みにくくなることがあります。

この記事では、JavaScriptのclassを使ってUIを管理する考え方を、できるだけシンプルな例から説明していきます。

classを使わない、よくある書き方

まずは、よくある簡単な例を見てみましょう。

<button id="btn">ボタン</button>
const btn = document.getElementById("btn");

btn.addEventListener("click", () => {
  btn.classList.toggle("is-active");
});

この書き方自体は間違っていません。1つのボタンを切り替えるだけなら十分です。

ただ、次のような状況になってくると少しずつ扱いづらくなります。

  • 同じボタンが複数ある
  • 開いているかどうかをJS側でも把握したい
  • 後から仕様が追加される

こうした変更が重なると、処理があちこちに散らばり、結果的にどこがどう動いているのか分かりにくくなってしまいます。

そこで出てくる選択肢が「classを使う」という考え方です。

classを使うと何が変わるのか

classを使う最大のメリットは、状態と処理をひとまとめにして管理できることです。

単に見た目のclassを付け替えるだけでなく、

  • このUIが今どんな状態なのか
  • どの要素を対象にしているのか
  • どんな役割を持った処理なのか

といった情報を、1つのまとまりとして扱えるようになります。

最小限のclassの例

同じ「ボタンを押したらactiveを切り替える」処理を、classで書くとこうなります。

<button class="js-toggle">ボタン</button>
class ToggleButton {
  constructor(el) {
    this.el = el;
    this.isActive = false;

    this.el.addEventListener("click", this.toggle.bind(this));
  }

  toggle() {
    this.isActive = !this.isActive;
    this.el.classList.toggle("is-active", this.isActive);
  }
}

document.querySelectorAll(".js-toggle").forEach((el) => {
  new ToggleButton(el);
});

一見すると何を書いているかわからないと思ってしまうかもしれませんが、やっていること自体はとてもシンプルです。

ここから順に見ていきます。

constructorとは

classを書くと、必ずと言っていいほど出てくるのが constructor です。

constructor「このclassを使い始めるときに1回だけ実行される初期化処理」を書く場所です。

言い換えると、このUIを使うための準備をする場所になります。

constructor(el) {
  this.el = el;
  this.isActive = false;

  this.el.addEventListener("click", this.toggle.bind(this));
}

この例では、constructorの中で大きく3つのことを行っています。

まず、el という引数で、管理したいDOM要素を受け取っています。new ToggleButton(el) と書いたときに渡された要素が、ここに入ります。

次に、this.el = el; で、その要素をこのclass専用のものとして保存しています。

これにより、他のメソッド(toggleなど)からも同じ要素を参照できるようになります。

続いて、this.isActive = false; で初期状態を設定しています。このボタンは、最初はアクティブではない、というルールをここで明示しています。

最後に、クリックイベントを登録しています。

this.el.addEventListener("click", this.toggle.bind(this));

ここでは、クリックされたときに toggle メソッドが実行されるように設定しています。(toggle メソッドについては後述しています。)

constructorの中でイベントを登録することで、「このUIはどう動くか」という仕様を初期化の段階でまとめて定義できます。

bind(this) は、イベントの中でも正しい this を使うためのものです。

細かい仕組みをすべて理解する必要はなく、classのメソッドをイベントで使う場合は必要になる、と覚えておけば問題ありません。

bindとは何か(補足)

class内のメソッドをイベントに渡すとき、this が意図したオブジェクトを指さないことがあります。

例えば、クリックイベント内で this.isActivethis.el を使いたくても、this がボタン要素を指してしまい、正しく動かなくなることがあります。

そこで bind(this) を使います。

bind(this) を付けると、その関数の中の this は常に その class のインスタンス を指すように固定されます。

constructorは準備だけを書く場所

constructorは便利ですが、処理を書きすぎないことも大切です。
実際の動作や状態の切り替えは、toggleのようなメソッドに任せます。

constructorは、

  • 管理する要素を受け取る
  • 初期状態を決める
  • イベントを登録する

といった準備だけを書く場所、と考えると分かりやすくなります。

状態を変数として持てる

this.isActive = false;

ここが重要なポイントですが、CSSのclassだけに頼らず、「今どういう状態か」をJavaScript側で把握できます。

this.isActive は変数のように扱える「インスタンスのプロパティ」です。

ローカル変数と違って、class内のどのメソッドからもアクセスでき、インスタンスごとに独立した状態を持たせられます。

後から「初回だけ処理を変えたいとき」や、「開いているときだけ別の処理をしたい」といった要件が出ても対応しやすくなります。

見た目の状態と、ロジック上の状態を分けて考えられるのが、classで管理する大きなメリットです。

toggleメソッド=「役割がはっきりした関数」

constructorで初期設定が終わったら、実際に動作を担当する関数を用意します。

toggle() {
  this.isActive = !this.isActive;
  this.el.classList.toggle("is-active", this.isActive);
}

このように処理をメソッドとして切り出しておくと、何をする関数なのかが名前だけで分かりますし、処理も1か所にまとまります。

イベントの中に直接処理を書くより、後から読み返したときに理解しやすくなります。

実際に要素に登録する

document.querySelectorAll(".js-toggle").forEach((el) => {
  new ToggleButton(el);
});

classの準備ができたら、実際に使いたい要素に対してインスタンスを作成します。

この書き方にしておくと、要素が複数あっても同じ処理を簡単に適用できます。

HTML側でclassを付けるだけで済むため、実務でも扱いやすくなります。

実務に近い例:アコーディオン

次は、よくあるアコーディオンの例です。

アコーディオンは、よくあるUIの1つで、クリックすると中身が開閉する構造です。
少し仕様が増えても、classを使えば状態と処理を整理しやすくなります。

簡単な仕様ですが、どのようにclassを利用するかイメージしてみましょう。

<div class="js-accordion">
  <button class="js-trigger">開く</button>
  <div class="js-content">中身</div>
</div>
class Accordion {
  constructor(root) {
    this.root = root;
    this.trigger = root.querySelector(".js-trigger");
    this.content = root.querySelector(".js-content");
    this.isOpen = false;

    this.trigger.addEventListener("click", () => this.toggle());
  }

  toggle() {
    this.isOpen = !this.isOpen;
    this.root.classList.toggle("is-open", this.isOpen);
  }
}

document.querySelectorAll(".js-accordion").forEach((el) => {
  new Accordion(el);
});

どのように動いているのか

  1. root要素を渡す
    constructorに .js-accordion の要素を渡すことで、classはその中の構造だけを管理します。
  2. rootの中の要素だけを操作できる
    this.trigger = root.querySelector(".js-trigger");
    this.content = root.querySelector(".js-content");

    このように、rootを基準に絞ることで、ページ内に複数のアコーディオンや同じ構造のUIがあっても、他の要素に影響を与えずに個別に制御できるようになります。
    もし document.querySelector だけで要素を取得すると、ページ内の最初の要素しか取得できなかったり、複数要素が干渉したりする可能性があります。
    rootを基準にすることで、class単位で「部品ごとの完結した管理」が可能になるのです。

  3. 状態を管理する
    this.isOpen で開閉の状態を保持しています。
    CSSのclassだけに頼るのではなく、JavaScript側でも「今開いているか」を把握できます。
  4. イベントとメソッドを分ける
    constructorではクリックイベントを登録し、toggleメソッドで開閉処理を行います。
    こうすることで、UIの挙動と初期設定を分離でき、コードが読みやすくなります。
  5. 複数要素でも同じ処理を適用
    document.querySelectorAll(".js-accordion").forEach(...) と書くことで、HTMLにアコーディオンを追加するだけで自動的に同じ動作が適用されます。

補足:Arrow関数 () => ... と this

this.trigger.addEventListener("click", () => this.toggle());

最初の簡単な例では、.bindを利用しましたが、ここで使っているのはアロー関数です。アロー関数には特殊な特徴があります。

通常の関数と違い、アロー関数の中の this外側(定義時)の this をそのまま使います。

つまり、constructor内の this(このAccordionインスタンス)をそのまま参照できます。

そのため、bind(this) を使わなくても this.toggle() 内の this は正しくインスタンスを指します。

classは難しい書き方ではない

classを書くと、最初は少し難しく感じるかもしれません。

ただ実際には、UIを部品として整理するための仕組みです。

小さなtoggleや開閉処理から使い始めるだけでも、「このUIはclassで書いた方が楽だな」と感じられるようになります。

まとめ

  • classはUIを部品として管理するための仕組み
  • 状態を変数で持てるのが最大の強み
  • 小さなUIから使えば問題ない

classはUIを部品として管理するための考え方です。

状態を変数として持てることが、最大の強みになります。

JavaScriptでのUI制御に慣れてきたら、ぜひclassを使う書き方も選択肢に入れてみてください。