概要
この記事では、ライブラリやフレームワークに依存せず、純粋なJavaScriptのみを使用してデータバインディングを実装する方法について詳しく説明します。効率的なWebアプリケーションの開発に欠かせない基本的な技術を、具体的な例を交えて紹介します。JavaScriptのさらなる可能性に興味がある初心者から中級者の方々にとって、理想的な内容です。
検証方法
データバインディングの検証を行うために、次の機能を持つツールを作成します。
- クリック回数を表示するテキストエリアを用意します。
- ボタンをクリックするごとに、表示されているクリック回数を1増やします。
実装案
- 純粋なJavaScriptを使用して、データバインディングの制御を担うモジュールを開発します。
- このデータバインディング制御モジュールを使用して、クリック回数表示ツールを実装します。
データバインディング制御モジュールの設計
独自に開発したイベント管理ツールを用いて、データバインディングの設定とその変更を監視します。
イベントツール
ソース
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のイベント処理パターンを実装しています。
データバンディングモジュール
ソース
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要素の内容をデータと同期させるようにしています。このパターンは、双方向データバインディングを実現するための基礎となります。
検証用ツール
<!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>