Custom Elementsの属性を変化させた時に、その描画が変化しない


はじめに

 Web Componentsで作成するCustom Elementsを動的に配置した時に、それぞれのElementの値が更新されないという問題に直面しました。解決するのが少しだけ大変でしたので、解決策を共有したいと思います。英語が読める人は「参考」の先のリンクを参照してもらえると、この内容について詳しく知ることが出来ると思います(日本語のページもたくさんあります)。

やりたいこと

 自分で作成したCustom Elementsを、ウェブページに動的に配置する際に、Elementの属性(attribute)に値を入れると、Elementの描画の挙動が変わって欲しい。ざっくりいうと以下のコードがその下に貼り付ける画像のように動いて欲しい。

コード(こんな感じで動いて欲しい!)

index.html
<!DOCTYPE html>
<html lang='en'>

<head>
  <meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
  <script type="text/javascript" src="./example-template.js" defer></script>
  <script type="text/javascript" src="./index.js" defer></script>
  </script>
  <!-- Custom Elementを引っ張ってくる "defer" を書かないと動かない  -->
  <title>custom elements example</title>
</head>

<body>
  <div id="parent">
    <!-- ここに動的にcustom elementsを入れたい -->
  </div>
  <!-- Custom Elementの利用。nameTitleとplaceHolderは作成した新しい属性(Attribute) -->
  <example-template name_title="Text form" plac="Please type your name"></example-template>

</body>

</html>
index.js
var parentEle = document.getElementById("parent");
var childElement = document.createElement('example-template');
childElement.setAttribute('name_title', 'Dynamic Insert');
childElement.setAttribute('plac', "値が入って欲しいのだ。");
parentEle.appendChild(childElement);

動作イメージ(こんな感じで動いて欲しい!)

ぶち当たった問題

 ところが、index.htmlのjavascriptを書いただけでは、このように動かないんですね、実際にどうなっているかというと、以下みたいな感じです。Text formという最初からbodyに入っているElementは正しく想定どおりに描画されていますが、動的に入れたElementは正しく描画されません。具体的には、属性に入れた値がきちんと入っていないですね。

動作結果(ちゃんと動かないよ!)

ちなみに、この時の生成されているHTMLはこんな感じです。Chromeの検証で引っ張ってきています。

解決方法

 解決するには、実はcustom elementの方にコードを書かないといけません。何が問題かと言いますと、作成したcustom elemetがattributeの変更を検出して、値を入れるということができていないんですね。そのため、attributeが変化したことを検出して(attributeChangedCallback)、値を入れないといけないんです。実際に書いたコードは以下です。コメントに説明を入れています。

example-template.js


class ExampleTemplate extends HTMLElement {
    constructor() {
        // superは必ず一番最初に呼び出される。
        super();
        // これより下にCustom Elementに関する記述を行う

        //shadow rootの作成。これが今回の一番上のノード(ルート)
        const shadow = this.attachShadow({ mode: 'open' });

        // 今回はひとまとまりをsectionで包む
        const wrapper = document.createElement('section');

        //* Sectionの下に並べるElementsの情報を入れていく *//
        // h2タグ(タイトル)
        const subtitle = document.createElement('h2');
        subtitle.setAttribute('class', 'subTitle');
        const titleSub = this.getAttribute('name_title');    // nameTitleというAttributeの値を取得
        subtitle.textContent = titleSub;                    // nameTitleの値をh2タグで表示

        // form & input (テキストフィールド)
        const formF = document.createElement('form');
        const textField = document.createElement('input');

        textField.setAttribute('type', 'text');             // inputのtype指定
        textField.setAttribute('class', 'textField');       // classをtextFieldに
        const contents = this.getAttribute('plac');  // placというAttributeの値を取得
        textField.setAttribute('placeholder', contents);    // input(type=text)のplaceholderにplacの値を代入
        formF.appendChild(textField);                       // formの下にtextFieldをネストする

        // Shadow domにCSSを適用する
        const style = document.createElement('style');
        console.log(style.isConnected);
        style.textContent = `
        .textField {
            width: 500px;
            height: 100px;
        }
        `;                                                  //今回はtextFieldの幅と高さを指定した。

        // shadow domを一番上にして、上で作ったものをネストしていく
        // shadow |- style
        //        |- wrapper |- subtitle
        //                   |- formF
        shadow.appendChild(style);
        shadow.appendChild(wrapper);
        wrapper.appendChild(subtitle);
        wrapper.appendChild(formF);
    }

    // Attributeが変更された時に行う処理の定義
    static get observedAttributes() { return ["name_title", "plac"] };    //"nameTitle"と"plac"が変わることを監視させる

    // observedAttributesで監視しているattributeが変わったら呼ばれる関数、attrに呼ばれたattribute名、newValには新しく入った値が入る
    attributeChangedCallback(attr, oldVal, newVal) {

        //このconsoleはデバッグ用
        console.log('my-el attribute changed', attr);
        console.log('new value is ', newVal);

        //attrの値によって分ける
        if (attr === 'name_title') {            // attrがname_titleのとき
            // Create title holder
            this.shadowRoot.querySelector('.subTitle').textContent = newVal;    //.subTitleのtextContentにnewVal(入力値)を入れる
        } else if (attr === 'plac') {
            // Create description holder
            this.shadowRoot.querySelector('.textField').setAttribute('placeholder', newVal);    //.textField(input element)にnewVal(入力値)を入れる
        }
    }
}
// 新しいElementの定義
customElements.define('example-template', ExampleTemplate);
 

参考

[1] Using custom elements, MDN Web Doc
[2] Eric Bidelman, カスタム要素 v1: 再利用可能なウェブ コンポーネント, Google Developers
[3] Custom elements, javascript.info
[4] HTML Living Standard

問合せ先

[email protected]