SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

再利用性とカプセル化のためのWeb Componentsを基礎から学ぶ

フレームワークに頼らない! フロントエンド技術「Web Components」のAPIを学ぼう

再利用性とカプセル化のためのWeb Componentsを基礎から学ぶ 第2回


  • このエントリーをはてなブックマークに追加

 Web Componentsは、再利用可能なUI部品を作成するためのAPI群の総称です。大別して3つのジャンルに分けられるAPIは、それぞれに特徴的な役割を持っています。実用する際に柔軟に取捨選択ができるよう、本記事で学んでいきましょう。

  • このエントリーをはてなブックマークに追加

対象読者

  • AngularやReactなどのフレームワークに頼らずに再利用可能なHTMLやCSSを整備したいマークアップエンジニア
  • AngularやReactなどのフレームワークとWeb Componentsを併用する利点を学びたいJavaScriptエンジニア

前提環境

 筆者の検証環境は以下の通りです。

  • macOS Monterey 12.4
  • Google Chrome 102.0.5005.115

Web Componentsを構成する3種のAPI群

 前回は、再利用可能なUI部品があることの嬉しさと、ブラウザで再利用可能なUI部品を作るための技術「Web Components」について概要を解説しました。今回はもう少し解像度を高めて、どんな役割のAPIがあるのかを確認していきましょう。まずは前回の復習です。Web Componentsには大別して次の3つの技術の組み合わせによって成り立っている、という話をしました。

  • カスタム要素(Custom Elements)
  • シャドウDOM(Shadow DOM)
  • HTMLテンプレート(HTML Templates)

 最終的には組み合わせて使うケースが多いものですが、それぞれの特徴を押さえておくことで、必要に応じて柔軟に組み替えられるようにもなります。次項からひとつずつ見ていきましょう。

カスタム要素(Custom Elements)

 まずは、カスタム要素(Custom Elements)に関するAPIについて解説します。

 Web Componentsの大きな特徴の一つは、元々ブラウザに用意されている組み込み要素(通常のHTMLタグ)とは異なるカスタム要素(自作タグ)を作成して、その中にUIや振る舞いを閉じ込められる(カプセル化できる)ということです。このカスタム要素の名前(タグ名)を決めたり、属性に指定した値に対してどのように振る舞うのかを定義するためのAPIが用意されています。

 主要なものについて一つずつ解説していきます。

ページで使えるカスタム要素を登録する

 まずは、カスタム要素を扱うための入口となるAPIについて解説します。

 当たり前のことではありますが、リスト1のように、普通のHTMLファイルに自作のタグ名を書いても、何も表示されません。

[リスト1]ページに登録されていない要素を書く(unknown.html)
    <!DOCTYPE html>
    <html lang="ja">
    <body>
      <div>
        
        <my-profile></my-profile>
      </div>
    </body>
    </html>
    

 このタグ名や、このタグによって描画すべきものについて、ページに伝えるためのインターフェースとして用意されているのがCustomElementRegistryです。window.customElementsというプロパティでアクセスできるので、HTMLから呼び出したJavaScript内であればほとんどの場所で手軽に利用することができます。

 おもに使うメソッドはCustomElementRegistry.define()です。第一引数にタグ名、第二引数に振る舞いを表すクラスを渡すことで、カスタム要素を登録できます。第三引数にはオプションとして、{ extends: 'p' }のようにHTMLのタグ名を指定することで、既存の組み込み要素を拡張する形でのカスタム要素を登録することもできます。

 それでは、ページに対して「というカスタム要素があり、その振る舞いはMyProfileクラスに定義されている」と伝えるためのコードを書いてみましょう(リスト2)。

[リスト2]最小のカスタム要素を定義する(define.js)
    class MyProfile extends HTMLElement { // (1)
      constructor() {
        super();
      }
    }
    
    customElements.define("my-profile", MyProfile); // (2)
    

 (1)で要素の振る舞い(まだ空っぽですが…)を定義したクラスを、(2)のdefineメソッドでページに登録しています。

 リスト2のJavaScriptコードが実行されることで、ページ内でが使えるようになりました。リスト3はJavaScriptファイルを読み込む処理を追加したHTMLファイルです。

[リスト3]ページが認識しているmy-profile要素を書く(define.html)
    <!DOCTYPE html>
    <html lang="ja">
    <body>
      <div>
        <my-profile></my-profile>
      </div>
      
      <script src="./define.js"></script>
    </body>
    </html>
    

 ブラウザで表示してもやはり何も表示されないので、一見するとリスト1とリスト3では何も変わっていないように感じられますが、リスト3ではしっかりと表示可能な要素として認知されてた状態になっている点で違っています。

 カスタム要素でUIを表示するサンプルはシャドウDOMの領分になってしまうため、本項では割愛します。

ライフサイクルコールバック

 カスタム要素を作り込んでいくと、カスタム要素がメイン文書(window.document)のツリーに追加されたタイミングで初期化処理をしたくなったり、属性が変わったタイミングで色を割り当て直す処理をしたくなったりすることがあります。そういった処理を行うために、カスタム要素へ何らかの変更が加えられた際に呼び出されるコールバックが整備されています。カスタム要素の追加から削除までの一生涯における各種イベントを通知することから、ライフサイクルコールバックと呼ばれています。

 カスタム要素のクラス内で利用できるライフサイクルコールバックを表1に示します。

表1:ライフサイクルコールバック
コールバック名 概要
connectedCallback 文書にカスタム要素が接続されたときに呼び出される
disconnectedCallback カスタム要素が文書から切断されたときに呼び出される
adoptedCallback カスタム要素が別の文書に移動すると呼び出される
attributeChangedCallback カスタム要素の属性が追加・変更・削除されると呼び出される

 attributeChangedCallbackは少し動きが想像しづらいので、サンプルを見てみましょう。リスト4は、に名前(fullname)と年齢(age)の属性を定義して、のように書けるようにします。

[リスト4]lifecycle.js
    class MyProfile extends HTMLElement {
    
      static get observedAttributes() {
        return ['fullname', 'age']; // (5)
      }
    
      constructor() {
        super();
    
        // カスタム要素の中にシャドウDOMを接続する
        this.attachShadow({ mode: "open" });
    
        // HTMLファイルで指定された属性を取り出し、初期表示に使用する
        const fullname = this.getAttribute('fullname') || 'John Doe';
        const age = this.getAttribute('age') || 'unknown';
    
        // (1) このカスタム要素の文書構造を定義する
        this.shadowRoot.innerHTML = `
          <div>
            <h1>My Profile</h1>
            <div id="fullname">full name: <span class="value">${fullname}<span></div>
            <div id="age">age: <span class="value">${age}</span></div>
          </div>
        `;
      }
    
      connectedCallback() {
        console.log("connectedCallback", { isConnected: this.isConnected });
      }
    
      disconnectedCallback() {
        console.log("disconnectedCallback");
      }
    
      adoptedCallback() {
        console.log("adoptedCallback");
      }
    
      attributeChangedCallback(name, oldValue, newValue) { // (2)
        console.log('attributeChangedCallback', { name, oldValue, newValue })
    
        if (name === 'fullname') { // (3)
          this.shadowRoot.querySelector('#fullname .value').textContent = newValue;
        }
    
        if (name === 'age') { // (4)
          this.shadowRoot.querySelector('#age .value').textContent = newValue;
        }
      }
    }
    
    customElements.define("my-profile", MyProfile);
    

 何かUIがあったほうがわかりやすいので、コンストラクタでシャドウDOMを割り当てつつ、(1)で名前と年齢を表示する文書構造を定義しました。

 属性に変更があると、(2)のattributeChangedCallbackが呼び出されます。第一引数(name)には属性の名前(今回はfullnameage)が渡され、第二引数(oldValue)には更新前の値、第三引数(newValue)には更新後の値が渡されます。実用上は第一引数と第三引数を使うことが多くなりそうです。

 処理内容を記述する場合は、(3)や(4)のように属性の名前で条件分岐をすることになります。今回は属性の変更内容に応じてUIの表示を更新したかったので、(1)で作った文書構造に基づいてシャドウDOMのツリー内を検索し、表示内容を更新しました。

 なお、属性に書いた内容ならなんでもattributeChangedCallbackが呼び出されるわけではありません。(5)でobservedAttributesというメソッドに定義した属性名の一覧にある属性だけが、attributeChangedCallbackの呼び出し対象になります。

 それでは、HTMLから実際に呼び出して、属性を変更した際の挙動を見てみましょう(リスト5)。

[リスト5]lifecycle.html
    <!DOCTYPE html>
    <html lang="ja">
    <body>
      <div>
        
        <my-profile fullname="Taro Suzuki" age="1"></my-profile>
      </div>
      
      <script src="./lifecycle.js"></script>
      
      <script<
        let intervalId;
        intervalId = setInterval((() => { // (3)
          const profile = document.querySelector('my-profile'); // (4) カスタム要素を取得
          let age = Number(profile.getAttribute('age')); // 文字列を数値に変換
          age += 1;
          profile.setAttribute('age', age); // (5) 属性を更新する
    
          if (age >= 10) {
            // 10歳以上になったら、インターバルを終了する
            clearInterval(intervalId);
          }
        }), 1000);
      </script>
    </body>
    </html>
    

 何はともあれ、まずは(1)でリスト4のJavaScriptファイルを読み込み、この文書内でが使えるようにします。それから(2)にタグを記載して、初期値となる属性を指定しました。

 さて、このサンプルでは属性が変わっていくたびにコールバックが呼び出される様子を見せたいわけですが、妥当なUIが思い浮かばなかったので、(3)のsetIntervalで1秒に1つずつ年齢を加算していくという味気ない例をご紹介することにします。

 毎秒どんな処理を行うのか見ていきましょう。(3)では通常の要素と同じように、要素名を使ってquerySelectorでDOMツリー内のカスタム要素を参照しています。属性を更新する方法も通常の要素と同じなので、(5)で年齢の値を一つ大きくしてage属性にセットしています。

 では、実際にこのHTMLファイルを表示してみましょう(図1)。

図1:属性の更新に合わせてUIが変わる
図1:属性の更新に合わせてUIが変わる

 1秒ごとにage:の数字が大きくなっていきますね。どうやら属性の変更によってカスタム要素の中で起きたattributeChangedCallbackの呼び出しを上手く扱えたようです。

 このように、カスタム要素に関するAPIでは、カスタム要素の内側と外側の世界を繋ぐ役割をおもに担っています。

会員登録無料すると、続きをお読みいただけます

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

次のページ
シャドウDOM(Shadow DOM)

この記事は参考になりましたか?

  • このエントリーをはてなブックマークに追加
再利用性とカプセル化のためのWeb Componentsを基礎から学ぶ連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 中川幸哉(ナカガワユキヤ)

WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS Twitter: @yyamada(公式)、@yyamada/wings(メンバーリスト) Facebook

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/16337 2022/09/13 12:03

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング