WebComponents の使い方
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ CustomElements
◆ タグを自作できる
◆ メソッド・プロパティ・getter/setter など作れる
◆ ShadowDOM
◆ 外側のツリーと切り離す
◆ CSS の影響を受けない
◆ id や class など属性を自由につけれる
◆ template タグ
◆ HTML 構造のテンプレート
◆ 同じ構造のものを複数使うところでテンプレートをクローンして使う
◆ HTMLImports
◆ 外部 HTML を読み込む
◆ template や CustomElement の定義を HTML ファイルにまとめられる
◆ まとめて使う
◆ HTML ファイルをインポートするだけでコンポーネントを使えるように出来る
◆ タグを自作できる
◆ メソッド・プロパティ・getter/setter など作れる
◆ ShadowDOM
◆ 外側のツリーと切り離す
◆ CSS の影響を受けない
◆ id や class など属性を自由につけれる
◆ template タグ
◆ HTML 構造のテンプレート
◆ 同じ構造のものを複数使うところでテンプレートをクローンして使う
◆ HTMLImports
◆ 外部 HTML を読み込む
◆ template や CustomElement の定義を HTML ファイルにまとめられる
◆ まとめて使う
◆ HTML ファイルをインポートするだけでコンポーネントを使えるように出来る
WebComponents
昔見たページが WebComponents をうまくまとめてくれていて分かりやすかったのですが 久々に使おうとしてググってみても見当たらなかったので自分でまとめてみます個々の機能はあちこちに丁寧な説明があるのですが全部まとめた使い方ってあまりないのですよね
特に HTMLImports は見当たらないです
CustomElements と ShadowDOM は使い方が変わって新しいのが v1 です
古いのは v0 です
ここでは v1 のみの紹介です
CustomElements
p タグや div タグのようなタグを自作できる機能ですこれまででも存在しない適当なタグ名を使えました
エラーは出ずに未定義の HTMLUnknownElement という型になります
クラスを customElements に登録
CustomElements ではタグ名とそのタグの動作を定義するクラスを登録できますクラス定義でメソッドやプロパティの追加 イベント時の処理を設定できるようになります
class CustomElement extends HTMLElement {
x(){ console.log(100) }
get y(){ return this.hasAttribute("y") }
set y(v){ this.setAttribute("y", v) }
z(){ this.innerHTML = `<div class="a"><span class="b">c</span></div>` }
}
customElements.define("c-elem", CustomElement)
const ce = document.createElement("c-elem")
ce.x()
// 100
ce.y = "foo"
ce.outerHTML
// <c-elem y="foo"></c-elem>
ce.z()
ce.outerHTML
// <c-elem y="foo"><div class="a"><span class="b">c</span></div></c-elem>
x(){ console.log(100) }
get y(){ return this.hasAttribute("y") }
set y(v){ this.setAttribute("y", v) }
z(){ this.innerHTML = `<div class="a"><span class="b">c</span></div>` }
}
customElements.define("c-elem", CustomElement)
const ce = document.createElement("c-elem")
ce.x()
// 100
ce.y = "foo"
ce.outerHTML
// <c-elem y="foo"></c-elem>
ce.z()
ce.outerHTML
// <c-elem y="foo"><div class="a"><span class="b">c</span></div></c-elem>
CustomElements の登録には customElements.define を使います
タグ名とそのタグの挙動を定義したクラスを引数に渡します
クラスは HTMLElement を継承している必要があります
HTMLElement を継承してないと define でエラーになります
customElements.define で定義したものは document.createElement で作ることができるようになります
直接クラスを new して body などに append しても大丈夫です
document.body.append(new CustomElement())
children
コンストラクタで初期化時に子要素を変更するなどができますしかし 子要素も含めて全体の DOM の一部なので外側から普通にアクセスできます
特別なメソッドやプロパティがある div を別名で作ってるようなものです
タグ名に 「-」 必須
定義するタグ名には 「-」 が含まれていないといけませんないと登録時にエラーが出ます
組み込みのタグには 「-」 が含まれないので将来的に重複しないようにという理由だったと思います
「custom-element」や「libname-customelement」などが登録可能です
組み込みタグでは 2 語になっても fieldset, textarea, datalist のように「-」なしの名前になります
クラス構文使わないとダメ
クラス構文嫌いな人もいるとは思いますが CustomElements の登録ではクラス構文必須です仕様レベルでの決まりではないように思うので将来的にはかわるかもしれません
HTMLElement を継承していないといけないのは決まっているので 継承するようクラス構文ではない方法で実装してみると HTMLElement.apply で new が必要と言われます
function CustomElement(...args){
HTMLElement.apply(this, args)
}
CustomElement.prototype.__proto__ = HTMLElement.prototype
customElements.define("c-elem", CustomElement)
const ce = document.createElement("c-elem")
// Please use the 'new' operator, this DOM object constructor cannot be called as a function.
HTMLElement.apply(this, args)
}
CustomElement.prototype.__proto__ = HTMLElement.prototype
customElements.define("c-elem", CustomElement)
const ce = document.createElement("c-elem")
// Please use the 'new' operator, this DOM object constructor cannot be called as a function.
かと言って new を使っても Illegal constructor になるので CustomElement のインスタンスを作ることができません
無名クラスで定義
要素を作るときにクラス名に対して new を使わないなら 無名クラスでつくってしまえますcustomElements.define("c-elem", class extends HTMLElement {
x(){ console.log(100) }
})
document.createElement("c-elem").x()
// 100
x(){ console.log(100) }
})
document.createElement("c-elem").x()
// 100
こっちのほうが楽でよく見る方法です
この方法でもクラスを取得できないわけではないです
customElements.get でタグ名を指定するとコンストラクタを返してくれます
customElements.define("c-elem", class extends HTMLElement {
x(){ console.log(100) }
})
const CustomElement = customElements.get("c-elem")
new CustomElement().x()
// 100
x(){ console.log(100) }
})
const CustomElement = customElements.get("c-elem")
new CustomElement().x()
// 100
HTMLElement 以外を継承
HTMLElement を継承していれば他の要素を継承することもできますただ 今のところは Chrome ではちゃんと動いていません (→ Chrome 67 で実装されました)
Chrome の開発者向けブログのサンプルをそのまま使ってもダメだったので私の確認時のコードにミスがあったということはないと思います
継承できるものは例えば HTMLButtonElement があります
変更するところは
- class の extends を変更
- define の extends を指定
- createElement の is を指定
の 3 つです
customElements.define("button-elem", class extends HTMLButtonElement {}, {extends: "button"})
document.createElement("button", {is: "button-elem"})
document.createElement("button", {is: "button-elem"})
今の Chrome だとインスタンスは作れるのですがクラスを継承してくれず 自分で定義したメソッドがインスタンスに存在しません
またコンストラクタも実行されません
定義できるだけで使えません
customElements.define("button-elem", class HTMLButtonElement {
constructor(){ console.log("constructor") }
method(){ console.log("method") }
}, {extends: "button"})
const elem = document.createElement("button", {is: "button-elem"})
// no output
elem.method()
// TypeError: elem.method is not a function
constructor(){ console.log("constructor") }
method(){ console.log("method") }
}, {extends: "button"})
const elem = document.createElement("button", {is: "button-elem"})
// no output
elem.method()
// TypeError: elem.method is not a function
もうひとつ Chrome で動かない部分があって HTMLButtonElement などを継承したクラスを new できません
customElements.define("button-elem", class extends HTMLButtonElement {}, {extends: "button"})
new (customElements.get("button-elem"))
// Illegal constructor
new (customElements.get("button-elem"))
// Illegal constructor
仕様的にはできるはずらしいですし 関係しそうな issue はあったので数バージョンの内には直るんじゃないかなと思ってます
getter/setter
hidden や id など一部の属性はプロパティと対応していて 片方を更新するともう一方もされますそういう処理はクラス定義で自分で作る必要があります
customElements.define("c-elem", class HTMLElement {
get customId(){
return this.getAttribute("custom-id")
}
set customId(value){
this.setAttribute("custom-id", value)
}
get customHidden(){
return this.hasAttribute("custom-hidden")
}
set customHidden(value){
if(value){
this.setAttribute("custom-hidden", "")
}else{
this.removeAttribute("custom-hidden")
}
}
})
get customId(){
return this.getAttribute("custom-id")
}
set customId(value){
this.setAttribute("custom-id", value)
}
get customHidden(){
return this.hasAttribute("custom-hidden")
}
set customHidden(value){
if(value){
this.setAttribute("custom-hidden", "")
}else{
this.removeAttribute("custom-hidden")
}
}
})
ShadowDOM
要素の内側に切り離された空間を作ることが出来る機能です外側のクエリセレクタなどでは内側が見えません
通常のアクセスができない隔離された空間なので iframe に近いです
Google 関係の ShadowDOM の解説ではよく video タグは内部で ShadowDOM で実装されていると言われています
ただしページの JavaScript レベルで作るものは video みたいに中が見えないことはなく devtools で ShadowDOM のツリーも見えます
HTML
つくる
要素の attachShadow メソッドを実行すると ShadowDOM がアタッチされますattachShadow の返り値か attachShadow 実行後の要素の shadowRoot プロパティで ShadowRoot を取得できます
ShadowRoot は DocumentFragment の一種なので innerHTML や append で内側に要素を追加できます
const div = document.createElement("div")
div.innerHTML = `<span>300</span>`
div.attachShadow({mode: "open"})
div.shadowRoot.innerHTML = `<p>400</p>`
document.body.append(div)
div.outerHTML
// <div><span>300</span></div>
div.innerHTML = `<span>300</span>`
div.attachShadow({mode: "open"})
div.shadowRoot.innerHTML = `<p>400</p>`
document.body.append(div)
div.outerHTML
// <div><span>300</span></div>
画面表示: 400
devtools:
<div>
#shadow-root (open)
<p>400</p>
<span>300</span>
</div>
devtools:
<div>
#shadow-root (open)
<p>400</p>
<span>300</span>
</div>
slot
div の子要素に 300 が存在しますが 画面上は ShadowDOM の中の 400 が表示されていますShadowDOM があるとそっちのツリーが優先して表示されます
ShadowDOM をアタッチした要素の子要素は slot タグを使って ShadowDOM のツリー内に配置できます
<!-- document tree -->
<div id="div">
<p slot="s1">s1のコンテンツ</p>
<span slot="s2">s2のコンテンツ</span>
<b slot="s2">これもs2のコンテンツ</b>
<pre>slot指定なし</pre>
<section>ここもslot指定なし</section>
</div>
<div id="div">
<p slot="s1">s1のコンテンツ</p>
<span slot="s2">s2のコンテンツ</span>
<b slot="s2">これもs2のコンテンツ</b>
<pre>slot指定なし</pre>
<section>ここもslot指定なし</section>
</div>
<!-- div's shadow tree -->
<section>
<h1>s1</h1>
<slot name="s1"></slot>
<h1>s2</h1>
<slot name="s2"></slot>
<h1>デフォルト</h1>
<slot></slot>
<h1>デフォルトは全部1つ目に入って2つ目は空</h1>
<slot></slot>
<h1>フォールバックコンテンツ</h1>
<slot name="s3">s3 がないので代わりにこれが表示される</slot>
</section>
<section>
<h1>s1</h1>
<slot name="s1"></slot>
<h1>s2</h1>
<slot name="s2"></slot>
<h1>デフォルト</h1>
<slot></slot>
<h1>デフォルトは全部1つ目に入って2つ目は空</h1>
<slot></slot>
<h1>フォールバックコンテンツ</h1>
<slot name="s3">s3 がないので代わりにこれが表示される</slot>
</section>
document.body.innerHTML = document_tree
document.querySelector("#div").attachShadow({mode: "open"}).innerHTML = shadow_tree
document.querySelector("#div").attachShadow({mode: "open"}).innerHTML = shadow_tree
<div id="div">
#shadow-root (open)
<section>
<h1>s1</h1>
<slot name="s1">
→<p>
</slot>
<h1>s2</h1>
<slot name="s2">
→<span>
→<b>
</slot>
<h1>デフォルト</h1>
<slot>
→<pre>
→<section>
</slot>
<h1>デフォルトは全部1つ目に入って2つ目は空</h1>
<slot>
</slot>
<h1>フォールバックコンテンツ</h1>
<slot name="s3">
s3 がないので代わりにこれが表示される
→#text
</slot>
</section>
<p slot="s1">s1のコンテンツ</p>
<span slot="s2">s2のコンテンツ</span>
<b slot="s2">これもs2のコンテンツ</b>
<pre>slot指定なし</pre>
<section>ここもslot指定なし</section>
#shadow-root (open)
<section>
<h1>s1</h1>
<slot name="s1">
→<p>
</slot>
<h1>s2</h1>
<slot name="s2">
→<span>
→<b>
</slot>
<h1>デフォルト</h1>
<slot>
→<pre>
→<section>
</slot>
<h1>デフォルトは全部1つ目に入って2つ目は空</h1>
<slot>
</slot>
<h1>フォールバックコンテンツ</h1>
<slot name="s3">
s3 がないので代わりにこれが表示される
→#text
</slot>
</section>
<p slot="s1">s1のコンテンツ</p>
<span slot="s2">s2のコンテンツ</span>
<b slot="s2">これもs2のコンテンツ</b>
<pre>slot指定なし</pre>
<section>ここもslot指定なし</section>
右矢印のところが slot 機能で表示されているものです
ShadowDOM をアタッチした要素の子要素は ShadowDOM の中の slot タグの中に表示されます
基本は 1 つ目の slot にすべて入ります
なので 2 つ目以降の slot は空になります
ただし 子要素の slot 属性に名前をつけておけば ShadowDOM 内の name 属性が同じ slot タグの中に表示されます
表示される位置が変わるだけで実体の位置は変わりません
parentElement が slot になったりはしないです
表示位置が ShadowDOM の中と言うだけで要素は ShadowDOM の外側にあります
slot タグにも子要素を書けます
slot に対応する要素がないときのフォールバックコンテンツです
slot に表示できるもの
特殊なものでは li や tr タグは表示できますですが option はできませんでした
<div>
#shadow-root (open)
<select>
<slot>
→<option>
→<option>
→<option>
</slot>
</select>
<option>option1</option>
<option>option2</option>
<option>option3</option>
</div>
#shadow-root (open)
<select>
<slot>
→<option>
→<option>
→<option>
</slot>
</select>
<option>option1</option>
<option>option2</option>
<option>option3</option>
</div>
→ 表示されない
<div>
#shadow-root (open)
<ul>
<slot>
→<li>
→<li>
→<li>
</slot>
</ul>
<li>item1</li>
<li>item2</li>
<li>item3</li>
</div>
#shadow-root (open)
<ul>
<slot>
→<li>
→<li>
→<li>
</slot>
</ul>
<li>item1</li>
<li>item2</li>
<li>item3</li>
</div>
→ 表示される
<div>
#shadow-root (open)
<table>
<tbody>
<slot>
→<tr>
→<tr>
</slot>
</tbody>
</table>
<tr><td>12345</td><td>6</td></tr>
<tr><td>7</td><td>890890</td></tr>
</div>
#shadow-root (open)
<table>
<tbody>
<slot>
→<tr>
→<tr>
</slot>
</tbody>
</table>
<tr><td>12345</td><td>6</td></tr>
<tr><td>7</td><td>890890</td></tr>
</div>
→ 表示される
ネスト
ShadowDOM の中の要素に ShadowDOM をアタッチすることもできます<!-- document tree -->
<div>
<p>text</p>
</div>
<div>
<p>text</p>
</div>
<!-- div's shadow tree -->
<section>
<slot></slot>
</section>
<section>
<slot></slot>
</section>
<!-- section's shadow tree -->
<h1>
<slot></slot>
</h1>
<h1>
<slot></slot>
</h1>
<!-- result -->
<div>
#shadow-root (open)
<section>
#shadow-root (open)
<h1>
<slot>
→<p>
</slot>
</h1>
</section>
</div>
<div>
#shadow-root (open)
<section>
#shadow-root (open)
<h1>
<slot>
→<p>
</slot>
</h1>
</section>
</div>
shadowDOM をアタッチできるタグ
attachShadow はすべてのタグにはできませんというか出来るタグのほうが少ないです
メソッド自体は Element クラスにあるので実行はできるもののエラーになります
try-catch しながらチェックした結果こうなりました
できる:
body, article, section, nav, aside, h1, h2, h3, h4, h5, h6, header,
footer, p, blockquote, main, div, span
できない:
html, head, title, base, link, meta, style, hgroup, address, hr, pre,
ol, ul, menu, li, dl, dt, dd, figure, figcaption, a, em, strong, small,
s, cite, q, dfn, abbr, ruby, rt, rp, data, time, code, var, samp, kbd,
sub, sup, i, b, u, mark, bdi, bdo, br, wbr, ins, del, picture, source,
img, iframe, embed, object, param, video, audio, track, map, area, table,
caption, colgroup, col, tbody, thead, tfoot, tr, td, th, form, label,
input, button, select, datalist, optgroup, option, textarea, output,
progress, meter, fieldset, legend, details, summary, dialog, script,
noscript, template, slot, canvas
body, article, section, nav, aside, h1, h2, h3, h4, h5, h6, header,
footer, p, blockquote, main, div, span
できない:
html, head, title, base, link, meta, style, hgroup, address, hr, pre,
ol, ul, menu, li, dl, dt, dd, figure, figcaption, a, em, strong, small,
s, cite, q, dfn, abbr, ruby, rt, rp, data, time, code, var, samp, kbd,
sub, sup, i, b, u, mark, bdi, bdo, br, wbr, ins, del, picture, source,
img, iframe, embed, object, param, video, audio, track, map, area, table,
caption, colgroup, col, tbody, thead, tfoot, tr, td, th, form, label,
input, button, select, datalist, optgroup, option, textarea, output,
progress, meter, fieldset, legend, details, summary, dialog, script,
noscript, template, slot, canvas
JavaScript
JavaScript は ShadowDOM の内外関係なく実行される空間は同じ window のグローバルです内側にアクセスするには ShadowDOM をアタッチした要素を通して ShadowRoot を経由してアクセスします
attachShadow
attachShadow メソッドは mode の指定が必須ですopen か closed を指定します
closed の場合は element.shadowRoot で ShadowRoot を取得できません
attachShadow の返り値を通してしかアクセスできません
ShadowDOM をアタッチした箇所でしか内側にアクセスできないので closed です
devtools を使えば closed でも気にせず操作できます
ShadowRoot
ShadowRoot は DocumentFragment を継承していますShadowRoot.prototype.__proto__.constructor.name
// "DocumentFragment"
// "DocumentFragment"
イベント
ShadowDOM の外では ShadowRoot を持つ要素からイベントが起きたように見えます一番外側の window にリスナをつけます
window.addEventListener("click", eve => console.log(eve.target, eve.path))
<body>
<div id="xxx">
#shadow-root (open)
<section>click here</section>
</div>
</body>
<div id="xxx">
#shadow-root (open)
<section>click here</section>
</div>
</body>
ShadowDOM の中の section をクリックしてみます
<div id="xxx"></div>
0: section
1: document-fragment
2: div#xxx
3: body
4: shadow
5: document-fragment
6: html
7: document
8: Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
0: section
1: document-fragment
2: div#xxx
3: body
4: shadow
5: document-fragment
6: html
7: document
8: Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
イベント発生元を表す target は div になっています
path のほうでは ShadowDOM の中も全部含まれています
もうちょっと複雑な構造にしてみます
<body>
<div id="xxx">
#shadow-root (open)
<slot></slot>
<div id="yyy">
#shadow-root (open)
<div>
<input id="btn" type="button" value="click here">
</div>
</div>
</div>
</body>
<div id="xxx">
#shadow-root (open)
<slot></slot>
<div id="yyy">
#shadow-root (open)
<div>
<input id="btn" type="button" value="click here">
</div>
</div>
</div>
</body>
ShadowDOM の中の input#btn をクリックします
<div id="yyy"></div>
0: input#btn
1: div
2: document-fragment
3: div#yyy
4: slot
5: document-fragment
6: div#xxx
7: body
8: shadow
9: document-fragment
10: html
11: document
12: Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
0: input#btn
1: div
2: document-fragment
3: div#yyy
4: slot
5: document-fragment
6: div#xxx
7: body
8: shadow
9: document-fragment
10: html
11: document
12: Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
path には slot も含まれます
div#yyy は div#xxx の子要素で div#xxx の ShadowDOM の中には属していないので target は div#yyy になります
CSS
CSS は内側と外側でスコープが分離されています内側に書いたセレクタは外側にマッチしませんし 外側に書いたセレクタは内側にマッチしません
ですが 子要素への継承は起きるので外側に書いたスタイルが内側に適用されることはあります
font-size や color がそれにあたります
継承されたくないなら内側のスタイルでリセットする必要があります
ShadowDOM のホストになる要素は内外両方から設定できます
ただし外側優先です
外側の selector
内側から ShadowDOM のホストを対象にしたいなら:host {}
と書けます
またホスト要素の状態をセレクタで指定したいなら
:host(.active) {}
のようにカッコで指定します
:host.active {}
だと動きません
ホストが特定セレクタにマッチする状態のときの内側のセレクタはそのまま :host に続けて書けます
:host(.foo[data-type="bar"]) .baz {
color: red;
}
color: red;
}
ホスト要素の祖先で指定したセレクタを指定することもできます
:host-context(.active) p {
color: red;
}
color: red;
}
<div class="active">
<div>
#shadow-root (open)
<p></p>
</div>
</div>
<div>
#shadow-root (open)
<p></p>
</div>
</div>
「.active p」としたいのに ShadowDOM の壁で「(.active) → (ShadowRoot) → (p)」となっているときに shadowDOM の外側のセレクタを :host-context に書くだけです
カスタムプロパティ
ShadowDOM 内側のスタイルはあくまでデフォルトで それを使う外側で色やボーダーやマージンなど見た目を調整したいこともありますカスタムプロパティを使うと対応できます
[内側]
#left{
color: var(--left-color, pink);
}
#right{
color: var(--right-color, aquamarine);
}
color: var(--left-color, pink);
}
#right{
color: var(--right-color, aquamarine);
}
[外側]
div.has-shadow{
--left-color: red;
--right-color: blue;
}
--left-color: red;
--right-color: blue;
}
var 関数を使って 「--」 から始まる名前を指定しておくと 外側で shadowDOM のホストに指定した同名のプロパティの値が使われます
var の 2 つめの引数は指定がないときのデフォルト値です
Template
template タグです内側は切り離されて DocumentFragment になります
ShadowDOM とは違って DOM 構造を定義しておくだけのものです
いっさい表示されませんし JavaScript の実行もされません
基本はこれの内側の DocumentFragment をクローンして同じものをいっぱい作るときに使います
JavaScript はクローンして DOM ヘアタッチしたタイミングで実行されます
script タグと同じく好きなところに書けるメリットがあります
tbody, tr, ul の内側など置ける場所が決まっていて パース時に別の場所移動させられて要素へのセレクタが変わってしまう心配もありません
この機能は昔から変わってないのですでに使われてるところが多いと思います
よくあるのはこういうのです
<table>
<tbody>
<template>
<tr><td class="title"></td><td class="body"></td></tr>
</template>
</tbody>
</table>
<script>
const tbody = document.querySelector("tbody")
const tpl = document.querySelector("template").content
fetch("api.json").then(e => e.json()).then(data => {
for(const item of data){
const clone = tpl.cloneNode(true)
clone.querySelector(".title").textContent = item.title
clone.querySelector(".body").textContent = item.body
tbody.append(clone)
}
})
</script>
<tbody>
<template>
<tr><td class="title"></td><td class="body"></td></tr>
</template>
</tbody>
</table>
<script>
const tbody = document.querySelector("tbody")
const tpl = document.querySelector("template").content
fetch("api.json").then(e => e.json()).then(data => {
for(const item of data){
const clone = tpl.cloneNode(true)
clone.querySelector(".title").textContent = item.title
clone.querySelector(".body").textContent = item.body
tbody.append(clone)
}
})
</script>
fetch してきた結果をテーブルで表示するものです
tr を作る時に中身を全部 JavaScript で作るのではなくテンプレートとして準備しておいて変動する箇所のみ書き換えます
HTMLImports
別の HTML をインポートできますこれも iframe に近い感じですが表示はしません
リソースとして取得してくるような使い方です
link タグに rel="import" を指定することで使えます
href 属性にインポートする html のパスを指定します
インポートした HTML は window.document とは別にドキュメントが作られそこに属します
JavaScript で element.import にアクセスすると HTMLDocument が取得できます
インポートする HTML 中に script タグがあった場合は解析時に即実行されます
JavaScript の実行空間は共通です
インポート中の script からでも window.document はメインのドキュメントを参照します
自身の属する document を参照するには document.currentScript.ownerDocument を使います
document.querySelector で link 要素を取得して import プロパティにアクセスする手段もあります
ただしその場合は インポートする側に id などの要素を特定できる属性が必須になってしまうのであまりオススメじゃないです
[main.html]
<link id="import" rel="import" href="import.html">
<div id="div"></div>
<script>
console.log(document.querySelector("#template"))
print1()
document.querySelector("#div").append(
document.querySelector("#import").import.querySelector("#template").content
)
</script>
<div id="div"></div>
<script>
console.log(document.querySelector("#template"))
print1()
document.querySelector("#div").append(
document.querySelector("#import").import.querySelector("#template").content
)
</script>
[import.html]
<template id="template">
<h1>foo</h1>
<p>bar</p>
</template>
<script>
console.log("import.html script start")
function print1(){console.log(1)}
console.log(document.currentScript.ownerDocument.querySelector("#template"))
console.log("import.html script end")
</script>
<h1>foo</h1>
<p>bar</p>
</template>
<script>
console.log("import.html script start")
function print1(){console.log(1)}
console.log(document.currentScript.ownerDocument.querySelector("#template"))
console.log("import.html script end")
</script>
[最終的な DOM 構造]
<div id="div">
<h1>foo</h1>
<p>bar</p>
</div>
<h1>foo</h1>
<p>bar</p>
</div>
[console の結果]
import.html script start
<template id="template">…</template>
import.html script end
null
1
<template id="template">…</template>
import.html script end
null
1
まとめる
それぞれ単体でも十分使えますが 4 つをまとめて使うことができますCustomElement のコンストラクタで自身に ShadowDOM をアタッチすることで内側はそのクラスで管理するようにできます
querySelector にひっかかったり css の影響を受けたくないときに便利です
ShadowDOM は作っても中身は空です
自分で中の要素をつくらないといけないのですが文字列ベースで作るのは大変です
template タグを用意しておけば 中身をクローンして ShadowRoot に append するだけで要素の構造の初期化は完了です
template タグや CustomElement の定義をメインの HTML に入れると長くなりますし使いまわすのが難しいです
コンポーネントのファイルを読み込むだけで CustomElement を使えるようにするために HTMLImports が使えます
独立した HTML ファイルにまとめて HTMLImports でロードします
ロード時に JavaScript は実行されて CustomElement が定義されるので使う側はロードするだけ済みます
サンプル
トグルボタンを作ってみたサンプルですreadonly, disabled などは未対応です
ボタンをちゃんと継承できるようになれば自力で頑張る必要もないところなので作ってません
[main.html]
<!doctype html>
<link id="import" rel="import" href="toggle-button.html">
<table width=300>
<tr>
<th>AAA</th>
<td>
<toggle-button id="tb1" state="right"></toggle-button>
</td>
</tr>
<tr>
<th>BBB</th>
<td>
<toggle-button id="tb2" leftvalue="foo" rightvalue="bar" state="left"></toggle-button>
</td>
</tr>
</table>
<link id="import" rel="import" href="toggle-button.html">
<table width=300>
<tr>
<th>AAA</th>
<td>
<toggle-button id="tb1" state="right"></toggle-button>
</td>
</tr>
<tr>
<th>BBB</th>
<td>
<toggle-button id="tb2" leftvalue="foo" rightvalue="bar" state="left"></toggle-button>
</td>
</tr>
</table>
[toggle-button.html]
<!doctype html>
<template id="toggle-button-template">
<style>
div{
width: 60px;
height: 28px;
display: inline-block;
position: relative;
}
div::before{
content: "";
display: block;
width: 100%;
height: 10px;
border-radius: 5px;
background: silver;
margin: 9px 0;
}
input{
display: none;
}
label{
width: 20px;
height: 20px;
border-radius: 50%;
box-shadow: 1px 1px 1px 1px #555;
background: #f5f5f5;
position: absolute;
top: 4px;
left: 0;
}
input:checked+label{
left: auto;
right: 0;
}
</style>
<div>
<input id="checkbox" type="checkbox">
<label for="checkbox"></label>
</div>
</template>
<script>
{
const self_doc = document.currentScript.ownerDocument
customElements.define("toggle-button", class extends HTMLElement {
constructor(){
super()
const root = this.attachShadow({mode: "open"})
const template = self_doc.querySelector("#toggle-button-template").content
root.append(template.cloneNode(true))
if(this.hasAttribute("state")){
this.state = this.getAttribute("state")
}
this.addEventListener("click", eve => {
this.toggle()
})
}
get value(){
const check = this.shadowRoot.querySelector("#checkbox")
return [
this.getAttribute("leftvalue") || "left",
this.getAttribute("rightvalue") || "right",
][+check.checked]
}
get state(){
const check = this.shadowRoot.querySelector("#checkbox")
return ["left", "right"][+check.checked]
}
set state(value){
const check = this.shadowRoot.querySelector("#checkbox")
check.checked = value === "right"
}
toggle(){
const check = this.shadowRoot.querySelector("#checkbox")
check.checked = !check.checked
return this.value
}
})
}
</script>
<template id="toggle-button-template">
<style>
div{
width: 60px;
height: 28px;
display: inline-block;
position: relative;
}
div::before{
content: "";
display: block;
width: 100%;
height: 10px;
border-radius: 5px;
background: silver;
margin: 9px 0;
}
input{
display: none;
}
label{
width: 20px;
height: 20px;
border-radius: 50%;
box-shadow: 1px 1px 1px 1px #555;
background: #f5f5f5;
position: absolute;
top: 4px;
left: 0;
}
input:checked+label{
left: auto;
right: 0;
}
</style>
<div>
<input id="checkbox" type="checkbox">
<label for="checkbox"></label>
</div>
</template>
<script>
{
const self_doc = document.currentScript.ownerDocument
customElements.define("toggle-button", class extends HTMLElement {
constructor(){
super()
const root = this.attachShadow({mode: "open"})
const template = self_doc.querySelector("#toggle-button-template").content
root.append(template.cloneNode(true))
if(this.hasAttribute("state")){
this.state = this.getAttribute("state")
}
this.addEventListener("click", eve => {
this.toggle()
})
}
get value(){
const check = this.shadowRoot.querySelector("#checkbox")
return [
this.getAttribute("leftvalue") || "left",
this.getAttribute("rightvalue") || "right",
][+check.checked]
}
get state(){
const check = this.shadowRoot.querySelector("#checkbox")
return ["left", "right"][+check.checked]
}
set state(value){
const check = this.shadowRoot.querySelector("#checkbox")
check.checked = value === "right"
}
toggle(){
const check = this.shadowRoot.querySelector("#checkbox")
check.checked = !check.checked
return this.value
}
})
}
</script>
適当に操作してみるとこんな感じです
document.querySelector("#tb1").value
// right
document.querySelector("#tb1").state
// right
document.querySelector("#tb2").value
// foo
document.querySelector("#tb2").state
// left
document.querySelector("#tb2").toggle()
// bar
document.querySelector("#tb2").state
// right
// right
document.querySelector("#tb1").state
// right
document.querySelector("#tb2").value
// foo
document.querySelector("#tb2").state
// left
document.querySelector("#tb2").toggle()
// bar
document.querySelector("#tb2").state
// right
state が left か right を表していて value では left と right に設定した名前で取得できます
template 内の script で初期化出来ない
ShadowDOM 内のイベントリスナをするなどの初期化はクラスのコンストラクタではなく template タグの中の script タグに任せたいですですが template タグ内の script タグから自身の要素を取得できません
document.currentScript は null になります
単純にクリックで色を切り替える color-button を作ってみます
理想ではこう書きたいです
<!doctype html>
<template id="color-button-template">
<style>
:host{
display: inline-block;
width: 80px;
height: 80px;
}
div{
width: 100%;
height: 100%;
}
</style>
<div></div>
<script>
{
const root = document.currentScript.getRootNode()
const div = root.querySelector("div")
div.addEventListener("click", update)
update()
function update(){
div.style.background = "#" + Math.random().toString(16).substr(2, 3)
}
}
</script>
</template>
<script>
{
const self_doc = document.currentScript.ownerDocument
customElements.define("color-button", class extends HTMLElement {
constructor(){
super()
const root = this.attachShadow({mode: "open"})
const template = self_doc.querySelector("#color-button-template").content
root.append(template.cloneNode(true))
}
})
}
</script>
<template id="color-button-template">
<style>
:host{
display: inline-block;
width: 80px;
height: 80px;
}
div{
width: 100%;
height: 100%;
}
</style>
<div></div>
<script>
{
const root = document.currentScript.getRootNode()
const div = root.querySelector("div")
div.addEventListener("click", update)
update()
function update(){
div.style.background = "#" + Math.random().toString(16).substr(2, 3)
}
}
</script>
</template>
<script>
{
const self_doc = document.currentScript.ownerDocument
customElements.define("color-button", class extends HTMLElement {
constructor(){
super()
const root = this.attachShadow({mode: "open"})
const template = self_doc.querySelector("#color-button-template").content
root.append(template.cloneNode(true))
}
})
}
</script>
クラスのコンストラクタではテンプレートのクローンを ShadowRoot に append するだけで template 内の script タグで初期化を行っています
ですが実際のところ これでは null の getRootNode プロパティへのアクセスとなってエラーです
トグルボタンのようにコンストラクタで処理するか ちょっと無理矢理 template の script タグから自身の ShadowRoot にアクセスできるようにするかです
<!doctype html>
<template id="color-button-template">
<style>
:host{
display: inline-block;
width: 80px;
height: 80px;
}
div{
width: 100%;
height: 100%;
}
</style>
<div></div>
<script>
{
const root = customElements.get("color-button").latest_root // changed
const div = root.querySelector("div")
div.addEventListener("click", update)
update()
function update(){
div.style.background = "#" + Math.random().toString(16).substr(2, 3)
}
}
</script>
</template>
<script>
{
const self_doc = document.currentScript.ownerDocument
customElements.define("color-button", class extends HTMLElement {
constructor(){
super()
const root = this.attachShadow({mode: "open"})
const template = self_doc.querySelector("#color-button-template").content
this.constructor.latest_root = root // added
root.append(template.cloneNode(true))
}
})
}
</script>
<template id="color-button-template">
<style>
:host{
display: inline-block;
width: 80px;
height: 80px;
}
div{
width: 100%;
height: 100%;
}
</style>
<div></div>
<script>
{
const root = customElements.get("color-button").latest_root // changed
const div = root.querySelector("div")
div.addEventListener("click", update)
update()
function update(){
div.style.background = "#" + Math.random().toString(16).substr(2, 3)
}
}
</script>
</template>
<script>
{
const self_doc = document.currentScript.ownerDocument
customElements.define("color-button", class extends HTMLElement {
constructor(){
super()
const root = this.attachShadow({mode: "open"})
const template = self_doc.querySelector("#color-button-template").content
this.constructor.latest_root = root // added
root.append(template.cloneNode(true))
}
})
}
</script>
参考
主にこの辺ですhttps://developers.google.com/web/fundamentals/web-components/shadowdom?hl=ja
https://developers.google.com/web/fundamentals/web-components/customelements?hl=ja