目次
1. Web Components
Web Components というのは、以下の仕様をまとめた仕様のことです。
- カスタム要素(の仕様)
- Shadow DOM(の仕様)
- HTML テンプレート(の仕様)
- ES Module(の仕様)
これらの仕様により、次の特徴を持った HTMLの部品を作成することができます。
- テンプレート内の CSS の効果をカスタムタグ内に限定することができる(カプセル化)
- 再利用しやすい
本記事では、Web Components を使ったサンプルコードを紹介します。
2. Web Components を使うための雛形
その1:時間を表示する
HTML
<style>
my-clock {
color: blue;
}
</style>
<my-clock></my-clock>
<my-clock>
というカスタムタグに色を設定しています。これは、たとえば<h2>
に対して色を設定するのと同じです。<h2>
は標準で用意されているタグであり、<my-clock>
は独自に作成したタグであるという違いでしかありません。Shadow DOM の中でもCSSを記述することはできますが、Shadow DOM の外側での設定が優先されて適用されます。
JavaScript
customElements.define('my-clock',
class extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
:host {
display: block;
contain: content;
font-size: 2em;
font-weight: bold;
font-family: monospace;
background-color: #aaffcc;
text-align: center;
}
</style>
<p id="dateOnScreen"></p>
`;
this._elmDateOnPage = this.shadowRoot.querySelector('#dateOnScreen');
}
connectedCallback() {
setInterval(() => {
this._elmDateOnPage.textContent = moment().format("YYYY-MM-DD HH:mm:ss");
}, 1000);
}
})
- テンプレート・リテラルを使って HTML と CSS を記述し、それを ShadowRoot にセットしています。
- カスタムタグ内の要素を取得するには、ShadowRoot の
querySelector()
メソッドを使います。 - カスタムタグがDOMに挿入されたときに、
connectedCallback()
メソッドが実行されます。DOM 上にカスタムタグが存在していないとできない初期化処理などは、ここに記述します。 :host
は、カスタムタグ自身を指しています。
デモページ
? シンプル時計 (Web Components)class を export する場合の書き方
class 定義を export した JavaScript ファイルを、HTML側から import して使う場合の書き方です。
HTML
<style>
my-clock {
color: blue;
}
</style>
<my-clock></my-clock>
<script type="module">
import MyClock from './my-clock.js'
customElements.define('my-clock', MyClock);
</script>
JavaScript
export default class extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
:host {
display: block;
contain: content;
font-size: 2em;
font-weight: bold;
font-family: monospace;
background-color: #aaffcc;
text-align: center;
}
</style>
<p id="dateOnScreen"></p>
`;
this._elmDateOnPage = this.shadowRoot.querySelector('#dateOnScreen');
}
connectedCallback() {
setInterval(() => {
this._elmDateOnPage.textContent = moment().format("YYYY-MM-DD HH:mm:ss");
}, 1000);
}
}
その2:数値を増減する
HTML
<my-elm interval="1"></my-elm>
JavaScript
class MyElm extends HTMLElement {
//static get observedAttributes() {
// return ['interval'];
//}
constructor() {
super();
// メソッド内で this を使う場合は必要
this._onIncClick = this._onIncClick.bind(this);
this._onDecClick = this._onDecClick.bind(this);
this._onIntervalIncClick = this._onIntervalIncClick.bind(this);
this._onIntervalDecClick = this._onIntervalDecClick.bind(this);
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
:host {
display: block;
contain: content;
}
p.title {
font-size: 1.2em;
font-weight: bold;
margin-top: 0;
}
p[name=output] {
color: blue;
margin-left: 1.5em;
}
span[name=interval] {
display: inline-block;
margin-right: 1em;
}
button {
cursor: pointer;
}
</style>
<p class="title">数字を増減させる</p>
<p name="output"></p>
<button name="inc">増</button> <button name="dec">減</button>
<hr>
Interval 属性値: <span name="interval"></span><button name="incInterval">増</button> <button name="decInterval">減</bu
tton>
`;
this._count = 0;
this._output = this.shadowRoot.querySelector('p[name=output]');
this._btnInc = this.shadowRoot.querySelector('button[name=inc]');
this._btnDec = this.shadowRoot.querySelector('button[name=dec]');
this._intervalElm = this.shadowRoot.querySelector('span[name=interval]');
this._btnIntervalInc = this.shadowRoot.querySelector('button[name=incInterval]');
this._btnIntervalDec = this.shadowRoot.querySelector('button[name=decInterval]');
this._btnInc.addEventListener('click', this._onIncClick);
this._btnDec.addEventListener('click', this._onDecClick);
this._btnIntervalInc.addEventListener('click', this._onIntervalIncClick);
this._btnIntervalDec.addEventListener('click', this._onIntervalDecClick);
}
connectedCallback() {
this._output.textContent = `0`;
if (!Number.isInteger(this.interval)) {
this.interval = 1;
}
this._intervalElm.textContent = this.interval;
}
disconnectedCallback() {
if (this._btnInc) {
this._btnInc.removeEventListener('click', this._onIncClick);
}
if (this._btnDec) {
this._btnDec.removeEventListener('click', this._onDecClick);
}
if (this._btnIntervalInc) {
this._btnIntervalInc.removeEventListener('click', this._onIntervalIncClick);
}
if (this._btnIntervalDec) {
this._btnIntervalDec.removeEventListener('click', this._onIntervalDecClick);
}
}
set interval(value) {
this.setAttribute('interval', value);
this._intervalElm.textContent = this.interval;
}
get interval() {
return parseInt(this.getAttribute('interval'));
}
_onIncClick() {
this._count += this.interval;
this._output.textContent = `${this._count}`;
}
_onDecClick() {
this._count -= this.interval;
this._output.textContent = `${this._count}`;
}
_onIntervalIncClick() {
this.interval += 1;
}
_onIntervalIncClick() {
this.interval += 1;
}
_onIntervalDecClick() {
this.interval -= 1;
}
//attributeChangedCallback(name, oldValue, newValue) {
// console.log('attributeChangedCallback()', name, oldValue, newValue);
// //this[name] = newValue;
//}
}
window.customElements.define('my-elm', MyElm);
デモページ
? Web Components: サンプルその13. メモ
- Web Components のサンプルコードをいろいろ探してみましたが、「
<template>
タグを使って Shadow DOM に HTML と CSS をセットする」サンプルはあまり見付かりませんでした。Web Components から HTML Import の仕様が削られました(?)が、<template>
タグを使うことすら推奨されていない印象を受けます。ということで、本記事では素直に JavaScript の テンプレート・リテラル を使っています。
4. おわりに
他のサンプルも追加していく予定です。