飴屋

JavaScript/Webコンポーネント/ライフサイクルコールバック

customElements.define('fan-menu', FanMenu);

前回、こんな感じで独自のタグとその実体となるクラスを結びつけました。この段階で独自のタグが誕生したわけです。誕生するものはいずれ死ぬのは、生物もプログラムも一緒です。ここでライフサイクルって言葉がでてきます。Webコンポーネントの場合は、ライフサイクルの段階に応じて、そのクラスのメソッドが呼び出されるよってことでライフサイクルコールバックって呼ぶようです。JavaでAndroidアプリを作ってたときなんかは、ライフサイクルイベントって呼んでたかな。

とりあえず、さっきのタグ誕生のタイミングではconnectedCallbackメソッドが呼び出されるようです。main関数的な、initialize的な、OnCreate的な、とにかく物事の始点となりそうなメソッドです。カスタム要素の設定はできる限り、クラスのconstructorではなく、connectedCallbackメソッド内で行うことが推奨されてるようです。要素が文書に追加される度に呼び出されるせいですかね。インスタンスができるタイミングと、DOM(文書)の表舞台に立つタイミングは必ずしも一致しないせいでしょう。タグは二度生まれる。誕生と登場は違う。

この反対の操作の時に呼ばれるのがdisconnectedCallbackメソッドです。文書から削除される度に呼び出されるそうです。

connectedMoveCallbackメソッドが定義されていると、文書内で要素が移動するタイミングで呼ばれるそうです。定義されてない場合はdisconnectedCallback(退場)とconnectedCallback(登場)が順番に起こって、実質移動したことになるのかな。

同じ文書ではなく別の文書に移動するときは、adoptedCallbackが呼び出されます。まぁ、あんまり複数文書間をまたぐような作業はしないから、あんまり用がないかも。

attributeChangedCallbackは属性値(アトリビュート)が変更、追加、削除、置換されたときのコールバックです。これは使うことになりそう。

connectedCallback

では実際問題、constructorではなく、connectedCallbackに書くべきことってなんでしょう?

イベントリスナー登録

イベント関係は文書に登場中にリスナーを用意した方がいいみたいです。逆にdisconnectedCallbackでremoveEventListenerしないとメモリリークしやすくなるみたいです。

子要素への参照取得 (querySelector)

DOMが確実に存在しているタイミングに行う方が安全です。この辺はShadow DOMについて、もっと知る必要がありそう。

属性値に応じた初期化

constructor時点では属性がまだ反映されていない場合があるので、connectedCallbackで行うべきでしょう。

fetch / WebSocket / observable購読

不要な通信を避けるため、登場する段(要素が実際に表示されてから)になって初めてやればいいでしょう。

実践

それでは、constructorのDOMを書き換えてみましょう。

constructor() {
  super();
  this.attachShadow({ mode: 'open' })
    .innerHTML = `
<button class="trigger" aria-label="メニュー">+</button>
<div class="items">
  <a href="#" class="item">🏠</a>
  <a href="#" class="item">⭐</a>
  <a href="#" class="item">⚙️</a>
  <a href="#" class="item">📷</a>
  <a href="#" class="item">💬</a>
</div>`;
}

buttonをトリガーにメニューの絵文字アイコンが登場するコンポーネントを作るので、buttonとメニューのリスト(items)をShadow DOMとやらにつけました。

connectedCallback() {
  this.trigger = this.querySelector('.trigger');
  this.items = [...this.querySelectorAll('.item')];
  this.addEventListener('pointerenter', openMenu);
  this.addEventListener('pointerleave', closeMenu);
  this.trigger.addEventListener('pointerdown', (e) => {
    e.preventDefault();
    openMenu();
  });
}

connectedCallbackでは、DOMに乗ったトリガーボタンと5つのメニューアイテムへの参照を取得してみました。connectedCallbackなので、要素が登場する度にthis.triggerとthis.itemsの値は最新の状態に更新されます。
メニューを開いたり閉じたりする関数を、fan-menuタグのイベントリスナーに登録し、this.triggerも押されたら開く関数が作動するようになりました。(openMenu / closeMenu の詳細は割愛)