SHOEISHA iD

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

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

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

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

進化し続けるフロントエンド技術「Web Components」はライブラリ不要!――気軽に始めて開発の生産性を高めよう

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

カスタム要素を作ってみる

 それでは今回の締めくくりとして、ここまで例示してきた <my-great-beautiful-button> の内部実装を解説しましょう。main.jsというファイル名で、リスト2の内容を実装します。

[リスト2]main.js
// (1) カスタム要素の振る舞いはクラス構文で実装する
class MyGreatBeautifulButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" }); // (3) カスタム要素に空のシャドウDOMを紐付ける

    // HTML側で指定されたbackgroundColor属性やcolor属性の値を参照する
    const backgroundColorAttr = this.getAttribute('backgroundColor') || '#eee';
    const colorAttr = this.getAttribute('color') || '#000';

    // (4) シャドウDOMの文書構造を定義する
    this.shadowRoot.innerHTML = `
      <style>
        button {
          border: none;
          border-radius: 8px;
          padding: 10px 20px;
          background-color: ${backgroundColorAttr};
          color: ${colorAttr};
        }
      </style>
      <button type="submit">
        <slot name="label" /><!-- (5) 利用側のパラメータを差し込む -->
      </button>
    `;
  }
}

// (2) カスタム要素を登録する
customElements.define("my-great-beautiful-button", MyGreatBeautifulButton);

 カスタム要素の定義は、

  • (1)の要素本体を表すクラスの定義
  • (2)(1)のクラスを登録する処理

 から構成されます。これによって <my-great-beautiful-button> をHTMLとして読み込まれた際に(2)で登録した(1)のクラスが実行されます。

 ただし、それだけでは空の要素ができるだけなので、(3)で作成したシャドウDOMツリーに(4)で文書構造を与えています。

 ボタンのテキストは利用する側から差し込みたいので、(5)に <slot> 要素を定義しました。 name="label" という属性で名前を定義しておいたのが、後ほどHTML側から利用する際に役立ちます。

 次は、登録したカスタム要素をHTMLの中で使ってみましょう(リスト3)。

[リスト3]index.html
<!DOCTYPE html>
<html lang="ja">
<body>
  <div>
    <my-great-beautiful-button><!-- (2) -->
      <span slot="label">submit now!</span><!-- (3) -->
    </my-great-beautiful-button>
  </div>
  <div style="margin-top: 8px;">
    <my-great-beautiful-button backgroundColor="#008CCF" color="#FFFFFF" >
      <span slot="label">submit now!</span>
    </my-great-beautiful-button>
  </div>
  <script src="./main.js"></script><!-- (1) -->
</body>
</html>

 リスト2で作成したJavaScriptファイルは、HTML内のカスタム要素を利用している箇所よりも下、body要素の下部(1)で読み込んでおきましょう。

 カスタム要素は通常のHTMLと同じように(2)のような使い方をします。少し見慣れない点としては、(3)のslot属性でしょうか。(3)でslotを定義した場合、リスト2の<slot>要素が(今回の例では<span>要素に置き換わります。これはHTMLテンプレートの機能の一部です。

HTMLテンプレートで文書構造を再利用する

 リスト2の例ではカスタム要素の初期化処理(constructor())に文書構造を直接埋め込んでいましたが、HTMLファイルではなくJavaScriptの文字列として定義していたため、エディタで書いている際にも補完などの入力支援を受けられず、心細い開発体験になってしまったことと思います。やはりHTMLは .html 拡張子のHTMLファイルに書いたほうが気分がいいかと思いますので、次の例では、文書構造を再利用可能なHTMLテンプレートとして記述してみましょう。

 新たにHTMLファイルを作り、 <template>要素の中にカスタム要素の文書構造となるHTMLを記述します(リスト4)。

[リスト4]index2.html
<!DOCTYPE html>
<html lang="ja">
<body>
  <!-- HTMLテンプレートを定義する -->
  <template id="my-great-beautiful-button">
    <style>
      button {
        border: none;
        border-radius: 8px;
        padding: 10px 20px;
        background-color: brown;
        color: yellow;
      }
    </style>
    <button type="submit">
      <slot name="label"></slot>
    </button>
  </template>
</body>
</html>

 HTMLテンプレートは単独では表示の対象にならないため、このまま表示しても真っ白な画面が出るだけです。次は、<template>要素に付与したidを使用して、カスタム要素を定義してみましょう(リスト5)。

[リスト5]main2.js
class MyGreatBeautifulButtonWithTemplate extends HTMLElement {
  constructor() {
    super();

    // (1) HTMLテンプレートを呼び出す
    const template = document.getElementById('my-great-beautiful-button');
    // (2) テンプレート内の構造をコピーする(操作しても元のテンプレートを壊さないようにする)
    const templateContent = template.content.cloneNode(true);

    // (3) attachした戻り値でシャドウDOMのルート要素を入手する
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(templateContent); // (4) テンプレートをシャドウDOMのツリーに追加する
  }
}

customElements.define(
  "my-great-beautiful-button-with-template",
  MyGreatBeautifulButtonWithTemplate
);

 リスト2でinnerHTMLに文字列を与えていた処理の代わりに、リスト4のHTMLテンプレートを当てはめる処理を行っています。

 まずは(1)でidを利用して<template>要素を呼び出します。<template>要素の中身は contentというプロパティに格納されていますが、これをそのまま使ってしまうと元のテンプレートを壊してしまって他の再利用先に迷惑がかかるので、(2)のようにcloneNodeメソッドを使ってコピーしたものを利用します。引数にtrueを与えることでcontent以下の全要素をコピーします。(3)はリスト2と同じようにattachShadowメソッドを呼び出してカスタム要素にシャドウDOMを紐づけていますが、この戻り値でシャドウDOMのルート要素が得られるためshadowRoot変数に保持しておきます。最後に、(4)でシャドウDOMに(2)でテンプレートから作成した要素を追加することで、このカスタム要素にHTMLテンプレート由来の文書構造が登録されます。

 それでは、リスト5のmain2.jsで作成した<my-great-beautiful-button-with-template>要素を、index2.htmlから呼び出しましょう。カスタム要素を追加したり、テンプレートの閉じタグ(</template>)の下に<script>要素を配置します(リスト6)。

[リスト6]カスタム要素をindex2.htmlの文書構造に組み込む
<!DOCTYPE html>
<html lang="ja">
<body>
  <!-- 追加:ここから -->
  <div>
    <my-great-beautiful-button-with-template>
      <span slot="label">submit now! (with template)</span>
    </my-great-beautiful-button-with-template>
  </div>
  <!-- 追加:ここまで -->
  <template id="my-great-beautiful-button">
    <!-- 省略 -->
  </template>
  <script src="./main2.js"></script><!-- 追加 -->
</body>
</html>

 この状態でブラウザに表示すると、図5のようになります。

図5:HTMLテンプレートを利用した例

 テンプレートの文書構造を利用したカスタム要素を作ることができました。

 このままだと背景色や文字色がカスタマイズできないので、もう少し手を加えてみましょう。といっても、リスト2のように文字列として色指定を埋め込むことはできないので、CSSカスタムプロパティの仕組みを利用して、パラメータを外側から与えることにします。カプセル化が崩れたように感じられるかもしれませんが、外部から操作できる限られた方法を提供するのはカプセル化の要素の一つなので、問題ありません。

 JavaScriptには手を加える必要がないので、HTMLのみを編集します(リスト7)。

[リスト7]色指定ができるようにindex2.htmlを改造する
<!DOCTYPE html>
<html lang="ja">
<head>
  <!-- (2) 追加:ここから -->
  <style>
    my-great-beautiful-button-with-template.bright {
      --background-color: yellow;
      --color: gray;
    }
  </style>
  <!-- (2) 追加:ここまで -->
</head>
<body>
  <div>
    <my-great-beautiful-button-with-template>
      <span slot="label">submit now! (with template)</span>
    </my-great-beautiful-button-with-template>
  </div>

  <!-- (3) 追加:ここから -->
  <div style="margin-top: 8px;">
    <my-great-beautiful-button-with-template class="bright">
      <span slot="label">submit now! (with template)</span>
    </my-great-beautiful-button-with-template>
  </div>
  <!-- (3) 追加:ここまで -->

  <template id="my-great-beautiful-button">
    <style>
      button {
        border: none;
        border-radius: 8px;
        padding: 10px 20px;
        background-color: var(--background-color, brown); /* (1) */
        color: var(--color, azure); /* (1) */
      }
    </style>
    <button type="submit">
      <slot name="label"></slot>
    </button>
  </template>
  <script src="./main2.js"></script>
</body>
</html>

 まずは<template>要素内でCSSカスタムプロパティを受け取れるようにします。(1)でCSSの var()関数を利用して--background-color--colorを受け取れるようにし、未指定の場合のデフォルト値として第二引数に従来の値を入れておきました。次に、(2)でCSSとしてカスタムプロパティを定義しています。

 <my-great-beautiful-button-with-template>要素に特定のクラスを付けた場合のみ適用されるスタイルなので、おそらく問題は起こりづらいはずです。最後に、(3)で<my-great-beautiful-button-with-template>要素に(2)で定義したbrightクラスを付与して、スタイルが当たるようにしました。

 リスト7を実行すると、図6のようになります。

図6:テンプレート由来のカスタム要素に外部から色指定できた

 カスタマイズできました。スタイル定義の大半はHTMLテンプレートやカスタム要素の中に隠蔽しつつ、カスタマイズしたい小数の項目だけを通常のCSSに定義すればよいので、従来の方法よりもよいアプローチになっています。

まとめ

  Node.jsの力を借りず、ブラウザの力だけでUIの部品化をできる技術として、Web Componentsを解説していきます。今回はバックグラウンドの思想と簡単な使い方を解説しました。次回は利用できるAPIを細かく解説していきます。

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

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
再利用性とカプセル化のためのWeb Componentsを基礎から学ぶ連載記事一覧

もっと読む

この記事の著者

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

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。 2026年時点での登録メンバは約50名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS X: @WingsPro_info(公式)、@WingsPro_info/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本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

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

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

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/15921 2022/06/03 11:00

イベント

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

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

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

メールバックナンバー