以下のような親子関係のような仕様を持つCheckBoxの機能を作りたいときに、
- 全選択のCheckBoxをチェックすると、各要素のCheckBoxをすべてON/OFFにする
- 各要素のCheckBoxがすべてONの時は全選択のCheckBoxはON、それ以外の場合はOFFになる
イメージ
各レコードの項目はサーバーサイド側のライブラリが自動でHTMLのコードを生成しているので、 すべてJavaScriptで扱ってしまうと大変なケースがあり、、、
そういうわけで、ちょっとニッチですが今回はサーバーサイドが生成したHTMLを活かしつつ、 ピュアなJavaScriptでこのへんを実装していくような形で考えていきます。
またそれを考えていくなかで、タイトルにあるようなJavaScriptののCustomEventを使うと、 親子関係の密結合な要素をイベントを通じた疎結合な実装に出来るかなと思ったので、 そのへんを書いていきます。
HTML部分
HTMLは以下のような形です、シンプルなTODOリスト的な構成です。
全選択のCheckBoxはcustom-all-checkbox
というclassが設定されていて、
各レコードのCheckBoxはcustom-checkbox
というclassが設定されています。
<!DOCTYPE html> <html> <head> <title>Sample App</title> <meta charset="UTF-8"> <script src="https://unpkg.com/vue@2.5.17"></script> </head> <body> <div id="app"> <table class="todo-list"> <tr> <th><input type="checkbox" class="custom-all-checkbox"></th> <th>title</th> <th>description</th> </tr> <tr> <td><input type="checkbox" class="custom-checkbox"></td> <td>title 1</td> <td>description 1</td> </tr> <tr> <td><input type="checkbox" class="custom-checkbox"></td> <td>title 2</td> <td>description 2</td> </tr> </table> </div> <script src="./sample.js"></script> </body> </html>
最初に考えた普通に実装した案
以下が最初に考えてみた案です、そんなに悪くなさそうに見えますが。。。
allCheckBoxElement.addEventListener
の中で直接checkBoxElements
を呼び出している、checkBoxElements.forEach
の中でallCheckBoxElement
を呼び出しているので、それぞれがそれぞれに依存している密結合な状態になってしまっています。
const allCheckBoxElement = document.querySelector('.custom-all-checkbox') const checkBoxElements = document.querySelectorAll('.custom-checkbox') const allChecked = checkBoxes => { const unChecked = Array.from(checkBoxes).filter(checkBox => !checkBox.checked) return unChecked.length === 0 } allCheckBoxElement.addEventListener('click', () => { checkBoxElements.forEach(checkbox => checkbox.checked = allCheckBoxElement.checked) }) checkBoxElements.forEach(checkBox => { checkBox.addEventListener('click', () => { allCheckBoxElement.checked = allChecked(checkBoxElements) }) })
そのため、処理を分割しずらく、おそらく今後修正を重ねるたびにコードが多くなっていきますし、全選択だけ他で使いたいような要望があっても使うことは出来ないので、同様のコードのコピペを生みやすくなっていそうです。。。
CustomEventを使って疎結合にする
以下がCustomEventを使って疎結合にしてみた実装です。 先程よりもコード自体は増えてしまっていますが、以下の点がメリットかなと。
- 各レコードのチェックボックスと全選択のチェックボックス、それぞれが独立しているので、全選択だけ他で使いたいといったことに対応出来る。
- 処理が各CheckBoxに閉じているのでファイル分割等が容易で、コード量の増加に対応しやすい。
// 各レコードのチェックボックス const checkBoxElements = document.querySelectorAll('.custom-checkbox') const allChecked = checkBoxes => { const unChecked = Array.from(checkBoxes).filter(checkBox => !checkBox.checked) return unChecked.length === 0 } checkBoxElements.forEach(checkBox => { checkBox.addEventListener('click', () => { const updateAllCheckedEvent = new CustomEvent('child-checked', { detail: { allCheked: allChecked(checkBoxElements) } } ) document.dispatchEvent(updateAllCheckedEvent) }) }) document.addEventListener('all-checked', event => { checkBoxElements.forEach(checkbox => checkbox.checked = event.detail.allCheked) }) // 全選択のチェックボックス const allCheckBoxElement = document.querySelector('.custom-all-checkbox') allCheckBoxElement.addEventListener('click', () => { const allCheckedEvent = new CustomEvent('all-checked', { detail: { allCheked: allCheckBoxElement.checked } } ) document.dispatchEvent(allCheckedEvent) }) document.addEventListener('child-checked', event => { allCheckBoxElement.checked = event.detail.allCheked })
デメリットとしては、以下かなと。。。
document
というグローバルな領域にイベントをディスパッチしている- 直接値を変更するよりもイベントの発火、補足を経由している分パフォーマンスが良くない(?)
今回は制約事項をなるべくへらすということでdocument
にイベントをディスパッチしましたが、
多少制約が増やしても良いような場合はdocument
ではなく、
親/子を含んだ上位の要素(今回のケースだと.todo-list
)に、dispatchするような形にして、それを補足するようなコードにすると親/子要素がそれぞれ上位の要素に依存するという制約の中で、疎結合になるかなと思います。
// 各レコードのチェックボックス const dispatchTo = document.querySelector('.todo-list') const checkBoxElements = document.querySelectorAll('.custom-checkbox') const allChecked = checkBoxes => { const unChecked = Array.from(checkBoxes).filter(checkBox => !checkBox.checked) return unChecked.length === 0 } checkBoxElements.forEach(checkBox => { checkBox.addEventListener('click', () => { const updateAllCheckedEvent = new CustomEvent('child-checked', { detail: { allCheked: allChecked(checkBoxElements) } } ) dispatchTo.dispatchEvent(updateAllCheckedEvent) }) }) dispatchTo.addEventListener('all-checked', event => { checkBoxElements.forEach(checkbox => checkbox.checked = event.detail.allCheked) }) // 全選択のチェックボックス const dispatchTo = document.querySelector('.todo-list') const allCheckBoxElement = document.querySelector('.custom-all-checkbox') allCheckBoxElement.addEventListener('click', () => { const allCheckedEvent = new CustomEvent('all-checked', { detail: { allCheked: allCheckBoxElement.checked } } ) dispatchTo.dispatchEvent(allCheckedEvent) }) dispatchTo.addEventListener('child-checked', event => { allCheckBoxElement.checked = event.detail.allCheked })
※パフォーマンスについては測定してないので、なんとも言えない・・・!
おわりに
親子関係を持つような要素をなんかしらの理由から、まとめてJavaScriptで扱えないときにmCustomEventを通じて連携するようにすると疎結合に実装出来そうだなという話でした🙇♂️