ベーシック アドベントカレンダー 3日目です。

Web Components というか Custom Element へのカスタムイベントの登録方法とメソッド呼び出しの方法を解説します。コンポーネント自身の振る舞いは当然コンポーネント内に定義されますが、使う側から独自の振る舞いを注入したくなるときがあります。例えば、モーダルコンポーネントで「閉じる」ボタンを押した後にイベントを発火させたり、フォトギャラリーコンポーネントで次の写真に移動させたりなどです。

カスタムイベントの設定

Custom Element だからといって特別なことはなく Event CustomEvent を使うだけです。

class MyCustomEvent extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
  }

  connectedCallback() {
    this.render()
  }

  render() {
    const button = document.createElement('button')
    button.addEventListener('click', this.fireClickedEvent)
    button.innerText = 'button'
    this.shadowRoot.appendChild(button)
  }

  fireClickedEvent() {
    const event = new CustomEvent('clicked', {
      bubbles: true,
      composed: true,
      detail: { hoge: 'piyo' }
    })
    this.dispatchEvent(event)
  }
}

customElements.define('my-custom-event', MyCustomEvent)

今回は独自のパラメータを渡したかったので new CustomEvent しました。detail key で自由にデータ定義ができます。また、 composedtrue にする必要があります(デフォルトだと false )。この理由は後で解説します。

<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>CustomEvent</title>
  <script type="module" src="./my-custom-event.js"></script>
</head>
<body>
<my-custom-event></my-custom-event>
<script>
  window.addEventListener('DOMContentLoaded', function() {
    const ce = document.querySelector('my-custom-event')
    ce.addEventListener('clicked', e => { console.log(e.detail.hoge) })
  })
</script>
</body>
</html>

そして呼び出す側は、予め定義されているカスタムイベント名で addEventListener すると発火イベントを受け取れます。ちなみに Custom Element が評価される前にイベント設定をしても発火しないので DOMContentLoaded で Web Components のモジュールファイルが読み込まれた後に設定をする必要があります。

これだけでコンポーネントの外部から振る舞いを注入することができます。

Shadow DOM のイベントバブリング境界線

composedtrue にする必要があると上で書きましたが、それは Shadow DOM は標準だと Shadow DOM 境界を超えた外部にイベントはバブリングしないという仕様になっているからです。つまり composedfalse のままだと呼び出す側リッスンできなくなります。

const event = new CustomEvent('clicked', {
  detail: { hoge: 'piyo' }
})
this.dispatchEvent(event)
const ce = document.querySelector('my-custom-event')
ce.addEventListener('clicked', e => { console.log(e.detail.hoge) }) // 発火しない

ちなみに Shadow DOM でなければバブリングするので、イベントリスナーの this を Custom Element 自身にすると発火したりします。ただ、あまり使うようなケースはないと思いますが…。

render() {
  const button = document.createElement('button')
  // button 自身ではなく Custom Element Class 自身を this として渡す
  // 無名関数ではなく bind でもよい
  button.addEventListener('click', event => this.fireClickedEvent(event))
  button.innerText = 'button'
  this.shadowRoot.appendChild(button)
}

fireClickedEvent() {
  const event = new CustomEvent('clicked', {
    detail: { hoge: 'piyo' }
  })
  // ここの this は <button> ではなく Custom Element Class
  this.dispatchEvent(event)
}

メソッド呼び出し

Custom Element は普通の Class でもあるので定義したメソッドは普通に呼び出すことができます。

class MyCustomEvent extends HTMLElement {
  constructor() {
    super()
  }

  hoge() {
    console.log('call hoge() method')
  }
}

customElements.define('my-custom-event', MyCustomEvent)
<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>CustomEvent</title>
  <script type="module" src="./my-custom-event.js"></script>
</head>
<body>
<my-custom-event></my-custom-event>
<script>
  window.addEventListener('DOMContentLoaded', function() {
    const ce = document.querySelector('my-custom-event')
    ce.hoge()
  })
</script>
</body>
</html>

これらカスタムイベントとメソッドを活用して組み立てていくと使い勝手の良いコンポーネントが作れると思います。