独自のメソッド、プロパティ、イベントなどを持つ、クラスで記述されたカスタムHTML要素を作成できます。
カスタム要素が定義されると、組み込みのHTML要素と同等に使用できます。
HTMLの辞書は豊富ですが、無限ではありません。<easy-tabs>
, <sliding-carousel>
, <beautiful-upload>
のようなものはありません。必要なその他のタグを考えてみてください。
特別なクラスで定義してから、HTMLの一部であるかのように使用できます。
カスタム要素には2つの種類があります
- 自律カスタム要素 – 抽象クラス
HTMLElement
を拡張した「完全に新しい」要素。 - カスタマイズされた組み込み要素 –
HTMLButtonElement
などに基づいて、カスタマイズされたボタンのような、組み込み要素を拡張する要素。
最初は自律カスタム要素について説明し、その後、カスタマイズされた組み込み要素に移ります。
カスタム要素を作成するには、ブラウザに、その表示方法、要素がページに追加または削除されたときに何をすべきかなど、いくつかの詳細を伝える必要があります。
これは特別なメソッドを持つクラスを作成することで行われます。メソッドはわずかしかなく、すべてオプションであるため、簡単です。
以下は、メソッドの完全なリストを含むスケッチです
class MyElement extends HTMLElement {
constructor() {
super();
// element created
}
connectedCallback() {
// browser calls this method when the element is added to the document
// (can be called many times if an element is repeatedly added/removed)
}
disconnectedCallback() {
// browser calls this method when the element is removed from the document
// (can be called many times if an element is repeatedly added/removed)
}
static get observedAttributes() {
return [/* array of attribute names to monitor for changes */];
}
attributeChangedCallback(name, oldValue, newValue) {
// called when one of attributes listed above is modified
}
adoptedCallback() {
// called when the element is moved to a new document
// (happens in document.adoptNode, very rarely used)
}
// there can be other element methods and properties
}
その後、要素を登録する必要があります
// let the browser know that <my-element> is served by our new class
customElements.define("my-element", MyElement);
これで、タグ <my-element>
を持つ任意のHTML要素について、MyElement
のインスタンスが作成され、前述のメソッドが呼び出されます。JavaScriptで document.createElement('my-element')
も実行できます。
-
を含める必要がありますカスタム要素名にはハイフン -
を含める必要があります。例えば、my-element
および super-button
は有効な名前ですが、myelement
は有効ではありません。
これは、組み込みのHTML要素とカスタムHTML要素の間で名前の競合が発生しないようにするためです。
例: “time-formatted”
たとえば、HTMLにはすでに日付/時刻用の <time>
要素があります。ただし、それ自体ではフォーマットは行いません。
時刻を適切に、言語対応の形式で表示する <time-formatted>
要素を作成しましょう
<script>
class TimeFormatted extends HTMLElement { // (1)
connectedCallback() {
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
}
customElements.define("time-formatted", TimeFormatted); // (2)
</script>
<!-- (3) -->
<time-formatted datetime="2019-12-01"
year="numeric" month="long" day="numeric"
hour="numeric" minute="numeric" second="numeric"
time-zone-name="short"
></time-formatted>
- クラスには
connectedCallback()
という1つのメソッドしかありません。ブラウザは、<time-formatted>
要素がページに追加されたとき(またはHTMLパーサーがそれを検出したとき)にそれを呼び出し、組み込みの Intl.DateTimeFormat データフォーマッタを使用します。これはブラウザ全体で十分にサポートされており、適切にフォーマットされた時間を表示します。 - 新しい要素を
customElements.define(tag, class)
で登録する必要があります。 - そして、どこでも使用できます。
ブラウザが customElements.define
の前に <time-formatted>
要素を検出した場合、エラーにはなりません。ただし、要素はまだ不明で、標準外のタグと同じです。
このような「未定義」の要素は、CSSセレクター :not(:defined)
でスタイルを設定できます。
customElement.define
が呼び出されると、「アップグレード」されます。それぞれに TimeFormatted
の新しいインスタンスが作成され、connectedCallback
が呼び出されます。それらは :defined
になります。
カスタム要素に関する情報を取得するには、次のメソッドがあります
customElements.get(name)
– 指定されたname
を持つカスタム要素のクラスを返します。customElements.whenDefined(name)
– 指定されたname
を持つカスタム要素が定義されると解決する(値なしで)プロミスを返します。
constructor
ではなく connectedCallback
でのレンダリング上記の例では、要素の内容は connectedCallback
でレンダリング(作成)されます。
なぜ constructor
でないのですか?
理由は簡単です。constructor
が呼び出されるとき、それはまだ時期尚早です。要素は作成されていますが、ブラウザはこの段階で属性をまだ処理/割り当てていません。getAttribute
の呼び出しは null
を返します。そのため、そこでは実際にレンダリングできません。
さらに、考えてみれば、本当に必要になるまで作業を遅らせる方がパフォーマンスの点で優れています。
connectedCallback
は、要素がドキュメントに追加されたときにトリガーされます。別の要素に子として追加されただけでなく、実際にページの一部になったときです。したがって、デタッチされたDOMを構築し、要素を作成して、後で使用するために準備できます。それらはページに表示されたときにのみ実際にレンダリングされます。
属性の監視
<time-formatted>
の現在の実装では、要素がレンダリングされた後、それ以上の属性の変更は効果がありません。これはHTML要素としては奇妙です。通常、a.href
のように属性を変更する場合、変更がすぐに表示されることを期待します。したがって、これを修正しましょう。
属性を監視するには、observedAttributes()
静的ゲッターでそのリストを提供します。このような属性の場合、attributeChangedCallback
はそれらが変更されたときに呼び出されます。他の、リストされていない属性についてはトリガーされません(これはパフォーマンス上の理由からです)。
以下は、属性が変更されたときに自動更新される新しい <time-formatted>
です
<script>
class TimeFormatted extends HTMLElement {
render() { // (1)
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
connectedCallback() { // (2)
if (!this.rendered) {
this.render();
this.rendered = true;
}
}
static get observedAttributes() { // (3)
return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name'];
}
attributeChangedCallback(name, oldValue, newValue) { // (4)
this.render();
}
}
customElements.define("time-formatted", TimeFormatted);
</script>
<time-formatted id="elem" hour="numeric" minute="numeric" second="numeric"></time-formatted>
<script>
setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
</script>
- レンダリングロジックは
render()
ヘルパーメソッドに移動されます。 - 要素がページに挿入されるときに1回呼び出します。
observedAttributes()
にリストされた属性の変更については、attributeChangedCallback
がトリガーされます。- …そして要素を再レンダリングします。
- 最後に、ライブタイマーを簡単に作成できます。
レンダリング順序
HTMLパーサーがDOMを構築するとき、要素は次々と、親が子よりも先に処理されます。例えば、<outer><inner></inner></outer>
がある場合、<outer>
要素が最初に作成されDOMに接続され、次に <inner>
が接続されます。
これはカスタム要素にとって重要な結果につながります。
たとえば、カスタム要素が connectedCallback
で innerHTML
にアクセスしようとすると、何も取得されません
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
alert(this.innerHTML); // empty (*)
}
});
</script>
<user-info>John</user-info>
実行すると、alert
は空になります。
これはまさに、その段階では子がなく、DOMが未完成であるためです。HTMLパーサーはカスタム要素 <user-info>
を接続し、その子に進もうとしていますが、まだ完了していません。
カスタム要素に情報を渡したい場合は、属性を使用できます。それらはすぐに利用できます。
または、本当に子が必要な場合は、ゼロ遅延 setTimeout
でそれらへのアクセスを延期できます。
これは機能します
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
setTimeout(() => alert(this.innerHTML)); // John (*)
}
});
</script>
<user-info>John</user-info>
これで、HTML解析が完了した後に非同期で実行するため、行 (*)
の alert
は「John」と表示されます。必要に応じて子を処理し、初期化を完了できます。
一方、この解決策も完璧ではありません。ネストされたカスタム要素も setTimeout
を使用して自身を初期化する場合、それらはキューに登録されます。外側の setTimeout
が最初にトリガーされ、次に内側の setTimeout
がトリガーされます。
したがって、外側の要素は内側の要素よりも前に初期化を完了します。
例でそれをデモンストレーションしましょう
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
alert(`${this.id} connected.`);
setTimeout(() => alert(`${this.id} initialized.`));
}
});
</script>
<user-info id="outer">
<user-info id="inner"></user-info>
</user-info>
出力順序
- outer connected.
- inner connected.
- outer initialized.
- inner initialized.
外側の要素が内側の要素 (4)
よりも前に初期化 (3)
を完了していることを明確に確認できます。
ネストされた要素の準備ができた後にトリガーされる組み込みのコールバックはありません。必要な場合は、自分で実装できます。たとえば、内側の要素は initialized
のようなイベントをディスパッチでき、外側の要素はそれらを聞いて反応できます。
カスタマイズされた組み込み要素
<time-formatted>
のように、作成した新しい要素には、関連付けられたセマンティクスがありません。それらは検索エンジンには不明であり、アクセシビリティデバイスはそれらを処理できません。
しかし、そのようなことは重要になる可能性があります。例えば、検索エンジンは、実際に時刻を表示していることを知りたいでしょう。また、特別な種類のボタンを作成する場合、既存の <button>
機能を再利用してみませんか?
組み込みHTML要素を、そのクラスから継承することで拡張およびカスタマイズできます。
たとえば、ボタンは HTMLButtonElement
のインスタンスなので、それをベースに構築しましょう。
-
クラスで
HTMLButtonElement
を拡張するclass HelloButton extends HTMLButtonElement { /* custom element methods */ }
-
customElements.define
に3番目の引数を提供し、タグを指定しますcustomElements.define('hello-button', HelloButton, {extends: 'button'});
同じDOMクラスを共有する異なるタグが存在する可能性があるため、
extends
を指定する必要があります。 -
最後に、カスタム要素を使用するには、通常の
<button>
タグを挿入しますが、それにis="hello-button"
を追加します<button is="hello-button">...</button>
以下は完全な例です
<script>
// The button that says "hello" on click
class HelloButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => alert("Hello!"));
}
}
customElements.define('hello-button', HelloButton, {extends: 'button'});
</script>
<button is="hello-button">Click me</button>
<button is="hello-button" disabled>Disabled</button>
新しいボタンは組み込みボタンを拡張しています。したがって、disabled
属性のような同じスタイルと標準機能を保持します。
参照
- HTML Living Standard: https://html.spec.whatwg.org/#custom-elements.
- 互換性: https://caniuse.dokyumento.jp/#feat=custom-elementsv1.
まとめ
カスタム要素には2つのタイプがあります
-
「自律型」-
HTMLElement
を拡張する新しいタグ。定義スキーム
class MyElement extends HTMLElement { constructor() { super(); /* ... */ } connectedCallback() { /* ... */ } disconnectedCallback() { /* ... */ } static get observedAttributes() { return [/* ... */]; } attributeChangedCallback(name, oldValue, newValue) { /* ... */ } adoptedCallback() { /* ... */ } } customElements.define('my-element', MyElement); /* <my-element> */
-
「カスタマイズされた組み込み要素」- 既存の要素の拡張機能。
.define
の引数がもう1つ必要で、HTML でis="..."
が必要です。class MyButton extends HTMLButtonElement { /*...*/ } customElements.define('my-button', MyElement, {extends: 'button'}); /* <button is="my-button"> */
カスタム要素はブラウザ間で十分にサポートされています。polyfill が https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs にあります。
コメント
<code>
タグを使用し、数行の場合は<pre>
タグで囲み、10 行以上の場合にはサンドボックス (plnkr, jsbin, codepen…) を使用してください。