Madogiwa Blog

主に技術系の学習メモに使っていきます。

JavaScript: CustomEventを使って密結合な親子要素をイベント通じた疎結合な実装にするMEMO

以下のような親子関係のような仕様を持つCheckBoxの機能を作りたいときに、

  • 全選択のCheckBoxをチェックすると、各要素のCheckBoxをすべてON/OFFにする
  • 各要素のCheckBoxがすべてONの時は全選択のCheckBoxはON、それ以外の場合はOFFになる

イメージ

f:id:madogiwa0124:20201004122134g:plain

各レコードの項目はサーバーサイド側のライブラリが自動でHTMLのコードを生成しているので、 すべてJavaScriptで扱ってしまうと大変なケースがあり、、、

そういうわけで、ちょっとニッチですが今回はサーバーサイドが生成したHTMLを活かしつつ、 ピュアなJavaScriptでこのへんを実装していくような形で考えていきます。

またそれを考えていくなかで、タイトルにあるようなJavaScriptののCustomEventを使うと、 親子関係の密結合な要素をイベントを通じた疎結合な実装に出来るかなと思ったので、 そのへんを書いていきます。

developer.mozilla.org

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を通じて連携するようにすると疎結合に実装出来そうだなという話でした🙇‍♂️

参考

developer.mozilla.org