ボクの考えたさいきょうのJavaScriptイベントバインダーの仕組みを批評して頂けませんでしょうか?
知りたいこと
JavaScriptのイベントのバインドに際して、DOM挿入時に自動でイベントをバインドしてくれる仕組みにしたら便利かもと考えました。
しかし初心者なので不安が残ります。
・これで実装を進めると不便なことや将来困りそうなことがあれば知りたいです。(というかあるからこそこういう仕組みが使われていないのでしょうし。)
・また、独自フレームワーク的なのを使うのがキモイ(キモさを上回るほどのメリットがない)というご意見をここのみんさんから頂戴するようでしたら使うのをやめたいと思っています。
よろしくお願い致します。
仕組み
概要
以下のように、HTMLに data-js=【モジュールファイルまでのパス#イベント名#関数名】
という値を持たせ、自動でこの値を分析し、関数を addEventListener()
してくれる仕組みです。
こう書くだけで <button>
を click
したら onClickFunction()
関数が発火するのです。
html
1<button data-js="path/to/module/#click#onClickFunction">ボタン</button>
想定しているメリット
(1) いちいち addEventListener()
を明示的、自覚的に記述する必要がない。
(2) 開発ツールでHTMLの data-js
を見ればどのモジュールのどの関数がイベントとしてバインドされているのかが一目瞭然になる。(ユーザにとってノイズだというなら、addEventListener()
した後で data-js
を削除してもよい。)
従来のイベントのバインドの流れ
従来の私は以下の様に event-binders.js
によってイベントをバインドしていました。
つまりイベントが必要な要素があるとき、毎回 event-binders.js
ファイルを作る必要があり、そしてDOM挿入に際していちいち bindFunction()
を実行して // DOM挿入直後にイベントのバインド
が必要だったのです。
以下は componentA というコンポーネントについてですが、他にも componentB などで毎回これが必要だったということです。(そして componentA や componentB は400個くらいあって大変です。)
html-templates.js
JavaScript
1/* assets/js/components/componentA/html-templates.js */ 2 3export const htmlButton = () => { 4 return `<button>ボタン</button>`; 5}
dom-setters.js
JavaScript
1/* assets/js/components/componentA/dom-setters.js */ 2 3import { htmlButton } from "./html-templates.js"; 4import { bindFunction } from "./event-binders.js"; 5 6export const insertButtonHtml = () => { 7 const main = document.querySelector("main"); 8 // DOM挿入 9 main.insertAdjacentElement("beforeend", htmlButton()); 10 // DOM挿入直後にイベントのバインド 11 bindFunction(main); 12}
event-listeners.js
JavaScript
1/* assets/js/components/componentA/event-listeners.js */ 2 3export const onClickFunction = (event) => { 4 console.log("onClickFunctionを実行します"); 5}
event-binders.js
JavaScript
1/* assets/js/components/componentA/event-binders.js */ 2 3import { onClickFunction } from "./event-listeners.js"; 4 5export const bindFunction = (scope) => { 6 const button = scope.querySelector("button"); 7 button.addEventListener("click", onClickFunction); 8}
今回の案によるイベントのバインドの流れ
上記に対して今回の案ならば、DOMの挿入とイベントのバインドを汎用にこなす最強ファイル (saikyou-dom-setters.js
および saikyou-event-binder.js
) を置けば、HTML を見て data-js
の指定に従って自動でイベントをバインドしてくれます。
上記のように毎回 event-binders.js
ファイルを作る必要がなくなる上に、// 要素のセット直後にイベントのバインド
も不要になるのです。
html-templates.js
JavaScript
1/* assets/js/components/componentA/html-templates.js */ 2 3export const htmlButton = () => { 4 return `<button data-js="/path/to/event-listeners/#click#onClickFunction">functionAの発火</button>`; 5}
dom-setters.js
JavaScript
1/* assets/js/components/componentA/dom-setters.js */ 2 3import { htmlButton } from "./html-templates.js"; 4import { saikyouInsertHTML } from "/assets/js/dom-utils/saikyou-dom-setters.js"; 5 6export const insertButtonHtml = () => { 7 const main = document.querySelector("main"); 8 saikyouInsertHTML(main, "beforeend", htmlButton()); 9}
event-listeners.js (変更なし)
JavaScript
1/* assets/js/components/componentA/event-listeners.js */ 2 3export const onClickFunction = (event) => { 4 console.log("onClickFunctionを実行します"); 5}
saikyou-dom-setters.js
JavaScript
1/* assets/js/dom-utils/saikyou-dom-setters.js */ 2 3import { saikyouEventBinders } from "./saikyou-event-binder.js"; 4 5/*-------------------------------------- 6 挿入 7--------------------------------------*/ 8 9// 単一の HTML Template を特定の位置に追加し, イベントリスナーを設定 10// 単一の とは <div></div> のように最上位で一つにまとまっているもの のこと 11export const saikyouInsertHTML = (element, position, htmlTemplate, test = null) => { 12 if (!element) { 13 console.error("Element is not defined. saikyouInsertHTML aborted.", { 14 element, 15 position, 16 htmlTemplate, 17 }); 18 return null; 19 } 20 21 // テンプレートを作成 22 const template = document.createElement("template"); 23 template.innerHTML = htmlTemplate.trim(); 24 25 // 挿入する要素をクローン作成 26 const fragment = document.createDocumentFragment(); 27 while (template.content.firstChild) { 28 fragment.appendChild(template.content.firstChild); 29 } 30 31 // 挿入位置に要素を一つずつ挿入 32 let insertedElements = []; 33 let child; 34 while ((child = fragment.firstChild)) { 35 if (child.nodeType === Node.ELEMENT_NODE) { 36 element.insertAdjacentElement(position, child); 37 insertedElements.push(child); 38 } else { 39 throw new Error("A non-element node was found. If htmlTemplate contains multiple elements, use saikyouInsertHTMLMany()."); 40 } 41 } 42 43 // [data-js] 属性の要素にイベントリスナーを設定 44 insertedElements.forEach((newElement) => { 45 saikyouEventBinders(newElement); 46 }); 47 48 // 挿入された要素を返す 49 return insertedElements[0]; 50} 51 52// 複数の HTML Template を特定の位置に追加し, イベントリスナーを設定 53// 複数の とは <div></div><div></div> のように最上位に複数並んでいるもの のこと 54export const saikyouInsertHTMLMany = (element, position, htmlTemplateMany, test = null) => { 55 // テンプレートを作成 56 const template = document.createElement("template"); 57 template.innerHTML = htmlTemplateMany.trim(); 58 59 // 挿入する要素をクローン作成 60 const fragment = document.createDocumentFragment(); 61 while (template.content.firstChild) { 62 fragment.appendChild(template.content.firstChild); 63 } 64 65 // 新しい要素を挿入 66 element.insertAdjacentHTML(position, htmlTemplateMany); 67 68 // 挿入された要素を取得 69 const insertedElements = []; 70 const nodes = element.querySelectorAll(":scope > *"); 71 nodes.forEach((node) => insertedElements.push(node)); 72 73 // [data-js] 属性の要素にイベントリスナーを設定 74 insertedElements.forEach((newElement) => { 75 saikyouEventBinders(newElement); 76 }); 77 78 // 挿入された要素の配列を返す 79 return insertedElements; 80}
saikyou-event-binder.js
JavaScript
1/* assets/js/dom-utils/saikyou-event-binder.js */ 2 3/*-------------------------------------- 4 イベントリスナーの設定 5--------------------------------------*/ 6 7// [data-js] にイベントリスナーを登録する 8export const saikyouEventBinders = (jsElement, addEvent = true) => { 9 // 対象要素の子要素にイベントをセット 10 jsElement.querySelectorAll("[data-js]").forEach((jsElement) => { 11 saikyouEventBinder(jsElement, addEvent); 12 }); 13 14 // 対象要素にイベントをセット 15 if (jsElement.getAttribute("data-js")) { 16 saikyouEventBinder(jsElement, addEvent); 17 } 18}; 19 20// [data-js] にイベントリスナーを登録する 21export const saikyouEventBinder = async (jsElement, addEvent = true) => { 22 // data-js を持たぬ要素には何もしない 23 const dataJsValue = jsElement.getAttribute("data-js"); 24 if (!dataJsValue) return; 25 26 // 下記例の構造の data-js を解析 27 // 例: data-js="/path/to/module#click#functionA, /path/to/module#mouseleave#functionB" 28 const eventInfos = dataJsValue.split(",").map((entry) => { 29 const [modulePath, eventType, fnName] = entry 30 .split("#") 31 .map((err) => err.trim()); 32 return { modulePath: `${modulePath}.js`, eventType, fnName }; 33 }); 34 35 for (const eventInfo of eventInfos) { 36 try { 37 // モジュールファイルのロード 38 const module = await import(eventInfo.modulePath); 39 40 const fn = getFn(module, eventInfo); 41 42 // 関数の存在チェックとイベント取り外しの実行 43 if (typeof fn === "function") { 44 if (addEvent) { 45 jsElement.addEventListener(eventInfo.eventType, fn); 46 } else { 47 jsElement.removeEventListener(eventInfo.eventType, fn); 48 } 49 } else { 50 // 関数が見つからない場合 51 console.warn( 52 `Function ${eventInfo.fnName} not found in ${eventInfo.modulePath}`, 53 { eventInfo, dataJsValue } 54 ); 55 } 56 } catch (error) { 57 // import() のエラー (モジュールファイルのロードエラー) 58 const errorMessage = error.message.toLowerCase(); 59 if ( 60 errorMessage.includes("failed to fetch") || 61 errorMessage.includes("not found") || 62 errorMessage.includes("404") 63 ) { 64 console.warn( 65 `Error loading module: ${error.message}:`, 66 { eventInfo, dataJsValue } 67 ); 68 } 69 70 // その他のエラーハンドリング (例えば関数の呼び出しミスなど) 71 else { 72 console.warn( 73 `Error uncategorized: ${error.message}:`, 74 { eventInfo, dataJsValue } 75 ); 76 } 77 } 78 } 79}; 80 81/*-------------------------------------- 82 ヘルパー関数 83--------------------------------------*/ 84 85const getFn = (module, eventInfo) => { 86 return ( 87 module[eventInfo.fnName] || 88 (module.default && 89 (typeof module.default === "function" 90 ? module.default.name === eventInfo.fnName 91 ? module.default 92 : null 93 : module.default[eventInfo.fnName])) 94 ); 95};
補足
・WEBサイト制作未経験者です。
・React とか Vue というのは触ったことがありません。

回答3件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2025/05/31 13:59