質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.30%
JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

解決済

3回答

572閲覧

ボクの考えたさいきょうのJavaScriptイベントバインダー

munekun

総合スコア116

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

0グッド

2クリップ

投稿2025/05/30 13:27

ボクの考えたさいきょうの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 というのは触ったことがありません。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

イベント設置されている要素がhtmlから追いやすいことが重要で、複雑な操作を必要としないのであれば Alpine.js で事足りるかもしれません。

ネイティブのJavaScriptだと煩雑な処理にはなるので、ある程度の規模のコードになるのであれば
何らかのライブラリ・フレームワークを導入するか、自作することは良い選択肢とは思います。

コードの方針に一貫性をもたせないと分かりづらくなってくるため、イベントの設置方法や実行コードの場所などについて、様々な方法を混ぜたりしないようには気をつけた方が良いかと思います。

投稿2025/05/30 16:46

Eggpan

総合スコア3287

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

munekun

2025/05/31 13:59

ありがとうございます。 > Alpine.js これすごいですね。HTMLにこんなに多くの情報を持たせていいんだ、とびっくりしました。 > 様々な方法を混ぜたりしないように まさにご指摘の通り問題になっていて、質問の方針で進めたはいいものの、外部のライブラリによって付与されるイベントについてはHTMLの data-js に値がなく、統一感に欠けるところが難点だったりします。
guest

0

ベストアンサー

こんにちは。

前提としてどういう問題があるのか、どういうことを解決したいのかは明らかにした方が回答がしやすいと思います。

注釈

・また、独自フレームワーク的なのを使うのがキモイ(キモさを上回るほどのメリットがない)というご意見をここのみんさんから頂戴するようでしたら使うのをやめたいと思っています。

おそらく、私の以前の意見が足を引っ張っているのだと思うのですが、イベントをバインドするという発想はかなり良いと私は思っていますので、全然いいテーマだと思います。

前提

どういう問題があるのか、というのを勝手に推測するのですが1点はjavascriptでイベントを追加してしまうと、html側から動作が分からない、どこで何が起きるのか分からないという問題に出くわすという所が一つと、
そもそもバニラjsでNodeをコピーしてくるとイベントが追加されていない。。。という問題がある。という話なのかなと思っています。

考え方として

解決方法は様々あるもののどれも「好み」の問題ですので、質問にあるようなやり方も問題はないと思います。
そういうモジュールとして使うと考えると別に問題はないのではないと思います。

悪いかもしれない点(ほかの解決策)

(1)Nodeをコピーした際にイベントがはついてこないという問題について
これはjQueryを使えば気にしないでもイベントが発火できるので、使ってみるのもありだと思います。
バニラjsで届かない部分はjQueryや関連のプラグインでよいこともあります。

(2)html側を見ただけで何が起きるか予測できない問題について
もしかすると、質問者さんの構想にあるページがそもそもとしてでかすぎるかもしれません。
その場合ReactやVue.jsを使って制御したほうが無難だと思います。
HTMLにもHTML側でjsの関数を呼び出すためのonClick的な属性がありますが、ReactやVue.jsでもDOMにイベントを埋め込んだり、styleを埋め込んだりする手法が一般的ですのであなたの考えにあっているかもしれません。
(自分はReact、Vue.jsともに使ったことないのですが)

なので、HTML側に要素を持たせるという考え方はかなりモダンな考え方といえるでしょう。
実際にはHTMLというよりもJSX記法でDOMを構築するという話なのですが。

ただ、個人的にReactなどに思うのはそもそもとしてエントリポイントがjavascriptのコードではないはずなので、htmlがエントリポイントでReactのコードが呼び出されるというのが若干わかりづらいんじゃないか?と個人的に思っています。
例えば、JavaScriptが構造を表すHTMLを呼び出す、またJavaScriptに定義されているメソッドを呼び出す、イベントをバインドするという方向ならわかりやすいと思うのですが。

(3)独自フレームワークがメンテナンス上の大きなデメリットになる可能性
注釈の部分を完全にひっくり返ししますが、Reactでいいじゃん的なことをあなたが開発したフレームワークでやってしまうと、同じチームの開発コストが格段にあがりませんか?別に個人開発ならそれでもいいし、完全にあなたの自由なのですが、信頼性も既存の製品に比べると低くなっちゃいませんか。
Reactを一度学べば、ほかの人が書いたコードでもあなたが書いたコードでもReactが分かる人なら読めるけれど、
あなたの作ったフレームワークを使うと、そのフレームワークが分かる人しかメンテナンスができなくなってしまいます。(わかる人が世界にあなた一人だけかも!?)

また、フレームワーク自体のメンテナンスを別個しないといけません(そもそもフレームワークを作るほうが主目的なのかもしれませんが)。

そもそもの問題点

構想しているページがでかすぎるという話をしましたが、そもそもとして構造化しすぎなのかもしれません。
それによって変な袋小路に入っていませんか?
フレームワークを作ること自体が目的なのかもしれませんが、一度本当に必要なこと、必要な理由を書き出してみて整理することが必要なのかもしれません。

逆に言うとですが、あなたがあなたのためにアプリ開発をするとしたら、バニラの状態で最小限で簡単な構成でも別によいわけです。
構造化するというのは未来の自分か、ほかの開発者との連携を取りやすくするメリットがあるかもしれませんが、構造化しすぎることでその部分は逆にわかりづらくなっており、目的を果たせていない可能性がありませんか?
物事を単純化しメンテナンス可能にするために、「このシンプルなやり方でわからない人は、もうわからなくていい。切り捨てる」という考えも必要です。
無理に複雑化して誰も手を付けられないようにしたいのであれば好きにしてくださいとしか言えないんじゃないかとも思います。

必要な部分を部品化して使いまわしたいと考えるかもしれませんが、将来の自分が必要な時にすればいいかもしれません。
作ってからリファクタリング(リファクタリングとは動作を変えずにソースのみを変えることです)したほうがいいのではないでしょうか。
読解不可能なレベルで作りこんだコードはリファクタリングすらできませんよ。
で、使い捨てならやっぱり単純な構成でいいですし。


蛇足
モジュールの作成や仕組みの解説が主であれば、質問ではなく
Qiitaに記事を書いてみる方が楽しいかもしれません。

また、使うのをやめるというよりはより良い形でブラッシュアップできれば良いと思います。
もっとシンプルで単純な構成であればバニラjsでそのモジュールを使う意義が出てきます。

例えば、最近困っているのですが、
AjaxでWebAPIを用いて、値をバインドしたいと思っています。(Stateのバインドといったほうが一部にはわかりやすいか。)

HTMLの方にAjaxのURLやAPIのキー(セッションなど?)、発火のタイミングなどを記述し、
埋め込みたい要素に対して取り出したいjsonのキー
(js-bind="{meta: count: {}}"とかjs-bind="meta.count"みたいなイメージ)を書けば、jsが解析してくれるとかHTMLをトランスパイルするような何かが解析してファイル出力してくれるとか、
そういうのしてくれるモジュールないかなぁ。とか思っていますよ。
そうすればバックエンドでなんの言語を使っているか関係なく、例えば通知とかコメントとかの部分を更新できますので。

投稿2025/05/30 14:47

utm.

総合スコア860

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

munekun

2025/05/31 14:15

ありがとうございます。以下、ご返信を書いているうちに長くなってしまいましたが、ご回答へのただの感想なので読まずにスルーで大丈夫です。笑 先にお礼を。丁寧にありがとうございます。大変参考になりました。 > イベントをバインドするという発想はかなり良い > 質問にあるようなやり方も問題はないと > HTML側に要素を持たせるという考え方はかなりモダン ありがとうございます。考え方の方向性としては間違っていなそうだと、utm.さんクラスの方に仰っていただけるのは励みになります。 > javascriptでイベントを追加してしまうと、html側から動作が分からない、どこで何が起きるのか分からない そうなのです。開発ツールの右サイドの Event Listeners タブから見れなくもないですが、見にくいですよね。 > Nodeをコピーしてくるとイベントが追加されていない なるほど。そういえばイベントも含めてのコピーという点についても困ることがありますね。 > htmlがエントリポイントでReactのコードが呼び出されるというのが若干わかりづらいんじゃないか? > JavaScriptが構造を表すHTMLを呼び出す、またJavaScriptに定義されているメソッドを呼び出す、イベントをバインドするという方向 エントリポイントもReactもわからないので、この辺りはちょっと理論に追いつけませんでした・・。(説明を追記してくれという意味ではありません。知識不足すぎて追記頂いてもおそらく理解できません。) > 独自フレームワークがメンテナンス上の大きなデメリットになる可能性 > 読解不可能なレベルで作りこんだコードはリファクタリングすらできません 仰る通りで、めっちゃ大いに不安です。質問の内容だけでも私には結構重めで、自分なりに何度かブラッシュアップした結果のものなのですが、その過程で毎回過去の自分の記述を問いなおす羽目にすでになっています。 > Reactを一度学べば、ほかの人が書いたコードでもあなたが書いたコードでもReactが分かる人なら読める これはつまり、質問で想定しているメリットとして挙げている2点(さらに挙げて頂いたイベントも含めてのコピーという点)は、Reactを使えば足りるだろうということなのでしょうか? 学習コストに鑑みて二の足を踏んでいたのですが、ちょっと見てみます。 > Qiitaに記事を書いてみる方が楽しいかも よく見かけるサイトですが登録したことはまだありません。たしかにコメントなどで指摘を受けて改善が進んでいる様子をよく見ます。今回はteratailにちょっと不適切な投稿だったかもですね。 > 例えば、最近困っているのですが、 この辺りもちょっと追いつけませんでした。ただHTMLの方に色々埋め込むという質問のようは方針は、他の方のご回答にもあるようにナシというわけではない様子、少し安心できました。
utm.

2025/05/31 14:53

シンプルに言うなら「そんなに複雑にする必要はあるのか」という回答でした
guest

0

document.addEventListenerすれば解決しそうな気がします

投稿2025/06/02 04:55

yambejp

総合スコア117780

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.30%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問