HTML
CSS
JavaScript
WebComponents

Web Componentsに触れる

0. WebComponentsって?

Web Components は、再利用可能なカスタム要素を作成し、ウェブアプリの中で利用するための、一連のテクノロジーです。

引用元:https://developer.mozilla.org/ja/docs/Web/Web_Components

一連のテクノロジーの主な中身
- HTML Templates
- HTML Imports
- Shadow DOM
- Custom Elements

これらを一つづつ、さらっと表面をなぞっていく
※見やすくするためにhtmlでは、html要素とhead要素を使っていません

1. HTML Templates

再利用可能なHTML要素

index.html
<body>
  <template>
    <h1>Hello HTML Templates</h1>
    <img src="">
  </template>
  <script src="main.js"></script>
</body>

この時点では、なにも表示されない

JSでtemplateをクローンしてbodyにappendChildする

main.js
const TEMPLATE = document.querySelector('template');
const CLONE = document.importNode(TEMPLATE.content, true);
document.body.appendChild(CLONE);


template自体を表示させるんじゃなくて、いったんcloneして生成するのがキモ。
img src(unknow)になってるのでsrcをつけてみる

main.js
const TEMPLATE = document.querySelector('template');
TEMPLATE.content.querySelector('img').src = 'https://dummyimage.com/99';
const CLONE = document.importNode(TEMPLATE.content, true);
document.body.appendChild(CLONE);

template要素.contentに対してquerySelectorする

2. HTML Imports

外部HTMLを読み込む

foo.html
<h1>Hello HTML Imports</h1>
index.html
<body>
  <link rel="import" href="foo.html">
  <script src="main.js"></script>
</body>
main.js*
const IMPORT = document.querySelector('link[rel="import"]').import;
const HEADER = IMPORT.querySelector('h1');
document.body.appendChild(HEADER.cloneNode(true));

インポートしたfoo.htmlからh1を取り出してbodyにappendChildしている。

HTML TemplateをHTML Importsする

template.html*
<template>
  <h1>Hello HTML Templates from template.html</h1>
  <img src="">
</template>
index.html
<body>
  <link rel="import" href="template.html">
  <script src="main.js"></script>
</body>
main.js
//HTML imports
const IMPORT = document.querySelector('link[rel="import"]').import;

//HTML template
const TEMPLATE = IMPORT.querySelector('template');
TEMPLATE.content.querySelector('img').src = 'https://dummyimage.com/99';

//クローンを作成してbodyに生やす
const CLONE = document.importNode(TEMPLATE.content, true);
document.body.appendChild(CLONE);

※import元のhtmlの名前空間(idやclass)にはスコープが生成されるが、styleはそうでないので注意

template.html
<template>
  <style>
    h1 {
      color: red;
    }
  </style>
  <h1>Hello from template.html</h1>
</template>
index.html
<body>
  <h1>Hello from index.html</h1>
  <link rel="import" href="template.html">
  <script src="main.js"></script>
</body>
main.js
const IMPORT = document.querySelector('link[rel="import"]').import;
const TEMPLATE = IMPORT.querySelector('template');
const CLONE = document.importNode(TEMPLATE.content, true);
document.body.appendChild(CLONE);

index.htmlにも、template.htmlのスタイルが適用される。

styleの名前空間を汚したくない!
→Shadow DOMで解決

3. Shadow DOM

カプセル化してくれるやつ

index.html
<body>
  <h1>Hello</h1>
  <script src="main.js"></script>
</body>
main.js
const SHADOW = document.querySelector('h1').createShadowRoot();
SHADOW.textContent = 'Hello Shadow DOM';


一見何の変哲もないが、

ShadowDOMは表示だけを担当し、JSから参照する場合は、元々の値が返される。
使いみちは?
→ShadowDOMはstyleを含めたスコープを持つ(カプセル化と言う)
ということで、

HTML TemplateをHTML ImportsしてShadowDOMで出力する

template.html
<template>
  <style>
    h1 {
      color: red;
    }
  </style>
  <h1>Hello from template.html</h1>
</template>
index.html
<body>
  <h1>index.html</h1>
  <div id='for_shadow'></div>
  <link rel="import" href="template.html">
  <script src="main.js"></script>
</body>  
main.js
//HTML imports
const IMPORT = document.querySelector('link[rel="import"]').import;

//HTML template
const TEMPLATE = IMPORT.querySelector('template');
const CLONE = document.importNode(TEMPLATE.content, true);

//Shadow DOM
const SHADOW = document.querySelector('#for_shadow').createShadowRoot();
SHADOW.appendChild(CLONE);

//DOMアクセス
console.log(document.querySelector('#for_shadow').shadowRoot);
console.log(document.querySelector('#for_shadow::shadow h1'));


無事、styleの名前空間を分離することができた

4. Custom Elements

タグを自作

いきなり、全部の合わせ技

template.html
<template>
  <h1>Hello from template.html as x-card</h1>
</template>
index.html
<body>
  <link rel="import" href="template.html">
  <script src="main.js"></script>
</body>
main.js
//Custom Elementsを定義
const card = class extends HTMLElement {
  connectedCallback() { //お約束
    //html import
    const IMPORT = document.querySelector('link[rel="import"]').import;

    //html template
    const TEMPLATE = IMPORT.querySelector('template');
    const CLONE = document.importNode(TEMPLATE.content, true);

    //Shadow DOM
    this.createShadowRoot().appendChild(CLONE);
  }
};

//x-cardタグにcardクラスを登録
window.customElements.define('x-card', card);

//bodyに生成
document.body.appendChild(new card());

5. ブラウザの対応状況

2018/10/14時点ではブラウザの対応状況がマチマチなので注意
HTML ImportsとShadow DOMにいたっては草案(Working Draft: WD)段階

https://caniuse.com/#feat=template

https://caniuse.com/#feat=imports

https://caniuse.com/#feat=shadowdomv1

https://caniuse.com/#feat=custom-elementsv1