純粋なJavaScriptで実現するデータバインディング

## 概要
この記事では、ライブラリやフレームワークに依存せず、純粋なJavaScriptのみを使用してデータバインディングを実装する方法について詳しく説明します。効率的なWebアプリケーションの開発に欠かせない基本的な技術を、具体的な例を交えて紹介します。JavaScriptのさらなる可能性に興味がある初心者から中級者の方々にとって、理想的な内容です。

## 検証方法
データバインディングの検証を行うために、次の機能を持つツールを作成します。
- クリック回数を表示するテキストエリアを用意します。
- ボタンをクリックするごとに、表示されているクリック回数を1増やします。

## 実装案
- 純粋なJavaScriptを使用して、データバインディングの制御を担うモジュールを開発します。
- このデータバインディング制御モジュールを使用して、クリック回数表示ツールを実装します。

## データバインディング制御モジュールの設計
独自に開発したイベント管理ツールを用いて、データバインディングの設定とその変更を監視します。
### イベントツール
#### ソース
```js
class EventEmitter {
  constructor() {
    this.listeners = new Map();
  }

  // イベントリスナーを追加
  on(event, listener, options = {}) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    const listenerObj = { listener, options };
    this.listeners.get(event).push(listenerObj);

    // AbortSignalによるリスナー削除のサポート
    if (options.signal) {
      options.signal.addEventListener('abort', () => {
        this.off(event, listener);
      }, { once: true });
    }
  }

  // イベントを発生させ、リスナーを呼び出す
  emit(event, data) {
    const eventListeners = this.listeners.get(event);
    if (eventListeners) {
      eventListeners.forEach(({ listener }) => {
        listener(data);
      });
    }
  }

  // イベントリスナーを削除
  off(event, listenerToRemove) {
    if (this.listeners.has(event)) {
      const filteredListeners = this.listeners.get(event).filter(({ listener }) => listener !== listenerToRemove);
      this.listeners.set(event, filteredListeners);
    }
  }
}
```
#### ソース説明
このEventEmitterクラスは、カスタムイベントの作成と管理を可能にするための簡単なフレームワークを提供します。クラスの各メソッドは、イベントリスナーの登録、イベントの発生時のリスナー呼び出し、およびリスナーの削除を行うために使用されます。このクラスは、より高度なイベント処理機能、特にAbortSignalを用いたリスナーの削除のサポートを含む、JavaScriptのイベント処理パターンを実装しています。

### データバンディングモジュール
#### ソース
```js
class DataBinder extends EventEmitter {
  constructor(el, data) {
    super();
    this.el = el;
    this.data = new Proxy(data, {
      set: (target, property, value) => {
        const result = Reflect.set(target, property, value);
        this.emit('change', { property }); // 変更イベントを発火
        return result;
      }
    });

    this.bindChildren();
  }

  bindChildren() {
    const elements = this.el.querySelectorAll('[bind-text]');
    elements.forEach(element => {
      const propName = element.getAttribute('bind-text');
      if (this.data[propName] !== undefined) {
        element.innerText = this.getter(this.data[propName]);
        this.on('change', (e) => {
          if (e.property === propName) {
            element.innerText = this.getter(this.data[propName]);
          }
        });
      }
    });
  }

  // getter関数を追加
  getter(value) {
    return `{{ ${value} }}`;
  }
}

```

#### ソース説明
このソースコードは、EventEmitterクラスを継承したDataBinderクラスを実装しています。これにより、特定のデータオブジェクトの変更を監視し、それに応じてDOMを更新する機能を提供します。JavaScriptのProxyを用いてデータオブジェクトに変更があった際にイベントを発火し、DOM要素の内容をデータと同期させるようにしています。このパターンは、双方向データバインディングを実現するための基礎となります。

## 検証用ツール
```js
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>databinding検証</title>
  <script src="./databind/event_emitter.js"></script>
  <script src="./databind/data_bind.js"></script>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      const app = document.getElementById('app');
      let data = { count: 0 }; // countをdataオブジェクトのプロパティとして宣言

      // データバインディングモジュールを使用
      const binder = new DataBinder(app, data);

      // カウントアップイベントの処理
      btn = app.querySelector('button');
      btn.addEventListener('count-up', () => {
        data.count++;
        binder.emit('change', { property: 'count' }); // 変更を通知
      });
    }, false);
  </script>
</head>
<body>
<div id="app">
  <p bind-text="count">カウント: {{count}}</p>
  <button type="button" onclick="this.dispatchEvent(new CustomEvent('count-up'))">カウントアップ</button>
</div>
</body>
</html>  
```