JavaScript コンポーネント設計の実践:スライダーを例にしたカプセル化手法

1. コンポーネントとは

Web ページ上で HTML・CSS・JavaScript をひとまとめにした再利用可能な部品を「コンポーネント」と呼ぶ。優れたコンポーネントは以下の四要素を満たす。

  • カプセル化:内部実装を隠蔽し、外部からは最小限のインターフェースのみを公開
  • 正確性:仕様通りに動作し、副作用がない
  • 拡張性:機能追加が容易
  • 再利用性:異なるプロジェクトでも流用可能

2. スライダーコンポーネントの基本形

2.1 HTML(構造)

<div id="carousel" class="carousel">
  <ul>
    <li class="carousel__item carousel__item--active">
      <img src="img1.jpg">
    </li>
    <li class="carousel__item">
      <img src="img2.jpg">
    </li>
  </ul>
</div>

2.2 CSS(表現)

.carousel { position: relative; width: 100%; overflow: hidden; }
.carousel__item {
  position: absolute; top: 0; left: 0;
  opacity: 0; transition: opacity 1s;
}
.carousel__item--active { opacity: 1; }

2.3 JavaScript(振る舞い)- API 編

class Carousel {
  constructor(selector) {
    this.root = document.querySelector(selector);
    this.slides = [...this.root.querySelectorAll('.carousel__item')];
  }
  get activeIndex() {
    return this.slides.findIndex(s => s.classList.contains('carousel__item--active'));
  }
  goTo(index) {
    this.slides.forEach(s => s.classList.remove('carousel__item--active'));
    this.slides[index]?.classList.add('carousel__item--active');
  }
  next() { this.goTo((this.activeIndex + 1) % this.slides.length); }
  prev() { this.goTo((this.activeIndex - 1 + this.slides.length) % this.slides.length); }
}

3. インタラクション実装(イベント駆動)

矢印・インジケータなどを追加し、ユーザーの操作に対応する。

class InteractiveCarousel extends Carousel {
  constructor(selector, { autoplay = 3000 } = {}) {
    super(selector);
    this.autoplay = autoplay;
    this.buildControls();
    this.bindEvents();
    if (autoplay) this.start();
  }

  buildControls() {
    this.root.insertAdjacentHTML('beforeend', `
      <button class="carousel__arrow carousel__arrow--prev"></button>
      <button class="carousel__arrow carousel__arrow--next"></button>
      <nav class="carousel__dots">
        ${this.slides.map((_, i) => `<span data-index="${i}"></span>`).join('')}
      </nav>
    `);
  }

  bindEvents() {
    this.root.addEventListener('click', e => {
      if (e.target.matches('.carousel__arrow--next')) this.next();
      if (e.target.matches('.carousel__arrow--prev')) this.prev();
      if (e.target.dataset.index != null) this.goTo(+e.target.dataset.index);
      this.resetTimer();
    });
    this.root.addEventListener('slide', e => {
      const idx = e.detail.index;
      this.root.querySelectorAll('.carousel__dots span')
        .forEach((dot, i) => dot.classList.toggle('is-active', i === idx));
    });
  }

  start() {
    this.timer = setInterval(() => this.next(), this.autoplay);
  }
  stop()  { clearInterval(this.timer); }
  resetTimer() { this.stop(); if (this.autoplay) this.start(); }

  goTo(index) {
    super.goTo(index);
    this.root.dispatchEvent(new CustomEvent('slide', { detail: { index } }));
  }
}

4. プラグイン化による拡張

コアクラスを肥大化させず、機能をプラグインとして外付けする。

const pluginDots = carousel => {
  carousel.on('slide', e => {
    const idx = e.detail.index;
    carousel.root.querySelectorAll('.carousel__dots span')
      .forEach((dot, i) => dot.classList.toggle('is-active', i === idx));
  });
};

const pluginArrow = carousel => {
  carousel.root.addEventListener('click', e => {
    if (e.target.matches('.carousel__arrow--next')) carousel.next();
    if (e.target.matches('.carousel__arrow--prev')) carousel.prev();
  });
};

class Carousel {
  ...
  use(...plugins) { plugins.forEach(p => p(this)); }
}

const c = new Carousel('#carousel');
c.use(pluginDots, pluginArrow);

5. テンプレート化で HTML 依存を排除

JavaScript 側で HTML を生成し、画像リストだけを受け取る構成に変更。

class Carousel {
  constructor(selector, { images = [], autoplay = 3000 } = {}) {
    this.root = document.querySelector(selector);
    this.images = images;
    this.autoplay = autoplay;
    this.root.innerHTML = this.render();
    this.slides = [...this.root.querySelectorAll('.carousel__item')];
    this.goTo(0);
    if (autoplay) this.start();
  }

  render() {
    return `
      <ul>
        ${this.images.map(src => `<li class="carousel__item"><img src="${src}"></li>`).join('')}
      </ul>`;
  }
}

6. 抽象化:汎用コンポーネントフレームワーク

class Component {
  constructor(selector, opts) {
    this.root = document.querySelector(selector);
    this.opts = opts;
    this.root.innerHTML = this.render(opts.data);
  }
  render(data) { /* 抽象メソッド */ return ''; }
  use(...plugins) { plugins.forEach(p => p(this)); }
}

class Carousel extends Component {
  render(images) {
    return `<ul>${images.map(src => `<li>...</li>`).join('')}</ul>`;
  }
}

まとめ

  • コンポーネント設計は「構造・表現・振る舞い」を明確に分離する
  • カプセル化 → 正確性 → プラグイン化 → テンプレート化 → 抽象化 のステップで再利用性を高める
  • 独自フレームワークを作ることで、外部ライブラリに頼らない柔軟な開発が可能になる

タグ: javascript component-design Carousel bem custom-events

6月25日 00:20 投稿