◆ slot の assign メソッドで slot の割り当てを JavaScript の処理で行える
◆ 手動割当をするには attachShadow のオプションで slotAssignment を manual に指定する必要あり
◆ assign 対象は Shadow DOM をアタッチした要素の子要素のみ
◆ assign しても slot 要素の子要素が消えないバグあり

Chrome86 で ShadowDOM の slot 機能が強化されたようです
https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Imperative-Shadow-DOM-Distribution-API.md

これまでの slot 機能

Shadow DOM をアタッチした要素の子要素は画面には表示されません

<elem-1>
<div>text</div>
</elem-1>

<!-- shadow dom of elem-1 -->
<div>
</div>

これだと elem-1 の内側は空っぽです
text は表示されません

Shadow DOM 内に slot 要素を配置すると その子要素として Shadow DOM をアタッチした要素の子要素が表示されます

<!-- shadow dom of elem-1 -->
<div class="foo">
<slot></slot>
</div>

これだと foo クラスの div の内側に text が表示されます
foo の style に 「color: red」 をつけると text が赤色で表示されます

複数の slot がある場合は slot の name 属性と子要素の slot 属性を元にどの slot に表示するか決まります

<elem-1>
<div>text</div>
<div slot="header">text2</div>
<div slot="footer">text3</div>
</elem-1>

<!-- shadow dom of elem-1 -->
<header>
<slot name="header"></slot>
</header>
<section>
<slot></slot>
</section>

これだと header の中の slot に text2 が表示されます
section の中の slot には text が表示されます
footer の slot はないので text3 は表示されません

slot や name 属性を省略した場合は空文字と一緒で 空文字どうしでマッチします
なので slot 未指定の子要素は name 未指定の slot 要素に表示されます
name 属性なしの slot はマッチするものがなかった場合に表示する場所ではないです

slot 属性は Shadow DOM をアタッチする要素の子要素である必要があります
div などでグループ化して 孫要素などになると効果がありません

<elem-1>
<section>
<div>text</div>
<div slot="main">text2</div>
</section>
</elem-1>

こうしても text2 だけが main の slot には表示されず text2 を含む section ごと name を指定しない slot に表示されます

新機能

この slot の割り当て機能をプログラムで制御できるようになりました

slot_elem.assign([elem1, elem2])

のように slot 要素のメソッドで どの要素を割り当てるか指定できます
複数箇所に割り当てることはできないので すでに割り当て済みの要素を別の slot に割り当てるとそっちに移動します
DOM の append メソッドみたいな感じですね

また この機能を使う場合は attachShadow メソッドのオプションが必要です

elem.attachShadow({ mode: "open", slotAssignment: "manual" })

slotAssignment に manual を指定します
このモードでは自動割当は行われないので 手動で割り当てない限りは slot には何も表示されません

手動割当でも 割り当てられるのは子要素のみです
孫要素などを assign 対象に含めるとエラーになります

assign でのよくあるエラー

Uncaught TypeError: Failed to execute 'assign' on 'HTMLSlotElement': The object must have a callable @@iterator property.

引数は配列など @@iterator を実装してる必要があります
append などの感覚で可変長引数で渡すとこれが出ます
配列で渡しましょう

Uncaught TypeError: Failed to execute 'assign' on 'HTMLSlotElement': Failed to convert value to 'Node'.

assign に渡したものに Node 以外のものがあると出るエラーです
querySelector などで取得した要素が見つからず null になってたりすると出ます

Uncaught DOMException: Failed to execute 'assign' on 'HTMLSlotElement': Node:  'DIV' is invalid for manual slot assignment.

childNodes に含まれない Node を assign に渡すと出るエラーです
孫要素や新規作成した要素を slot に割り当てることはできません
この例の DIV のように 要素の種類は教えてくれます

バグ

実装されたばかりだからか slot の子要素の扱いにバグがありました

slot 要素自体の子要素は slot で表示するものがない場合に表示されます

<slot name="contents">このスロットにマッチする要素がありませんでした</slot>

こういう感じで使えます
このメッセージは 後から子要素が追加されて それが slot に表示されると表示されなくなります

しかし 手動割当を行った場合は slot に要素が割り当てられても消えずに残っていました
一旦別の slot に割り当ててから再度割り当てると今度は消えているので回避策は一応あります

コード

slot のこれまでの機能と新機能の実際に動かせるコードです
プレビュー表示できます

elem-1 は基本的な slot 1 つに配置
elem-2 は name, slot 属性を使って配置
elem-3 は slotAssignment: "manual" を指定して手動配置
になってます

<!DOCTYPE html>

<script type="module">
customElements.define("elem-1", class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" }).innerHTML = `
<div style="border: 1px solid red; padding: 10px;">
<slot></slot>
</div>
`
}
})

customElements.define("elem-2", class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" }).innerHTML = `
<div style="border: 1px solid red; padding: 10px;">
<div style="border: 1px solid blue; padding: 10px;">
<slot name="x"></slot>
</div>
<slot></slot>
<div style="border: 1px solid green; padding: 10px;">
<slot name="y"></slot>
</div>
</div>
`
}
})

customElements.define("elem-3", class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open", slotAssignment: "manual" }).innerHTML = `
<div style="border: 1px solid red; padding: 10px;">
<div style="border: 1px solid blue; padding: 10px;">
<slot id="a"></slot>
</div>
<div style="border: 1px solid green; padding: 10px;">
<slot id="b"></slot>
</div>
</div>
`
}
connectedCallback() {
const a = this.shadowRoot.getElementById("a")
a.assign([this.children[0]])
const b = this.shadowRoot.getElementById("b")
b.assign([this.children[2]])
}
})
</script>

<elem-1>
<div>div1</div>
<div>div2</div>
<div>div3</div>
</elem-1>

<elem-2>
<div slot="x">div1</div>
<div>div2</div>
<div slot="y">div3</div>
</elem-2>

<elem-3>
<div>div1</div>
<div>div2</div>
<div>div3</div>
</elem-3>

slot-example01


もうひとつ 手動割当で assign 先を選択できるものです
child ごとにチェックボックスにチェックを入れて slotA, slotB のどっちに割り当てるかを選びます
現状では バグのせいでどちらかに最初に assign したときには No Item が残ってますが 別の slot に割り当てて一旦 slot を空にするとそれ以降はちゃんと動きます

<!DOCTYPE html>

<script type="module">
customElements.define("elem-1", class extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open", slotAssignment: "manual" }).innerHTML = `
<style>
.block {
margin: 10px;
padding: 10px;
border: 1px solid silver;
}
h1 {
margin: 10px 0;
font-size: 1.5em;
}
label {
margin: 0 5px;
}
</style>
<div class="block">
<h1>SlotA</h1>
<slot id="a">No Item</slot>
</div>
<div class="block">
<h1>SlotB</h1>
<slot id="b">No Item</slot>
</div>
<div>
<div>
<label>child1 <input id="c1" type="checkbox"></label>
<label>child2 <input id="c2" type="checkbox"></label>
<label>child3 <input id="c3" type="checkbox"></label>
</div>
<div>
<label>slotA <input type="radio" name="slot" value="a" checked></label>
<label>slotB <input type="radio" name="slot" value="b"></label>
</div>
<button id="assign">assign</button>
</div>
`
this.shadowRoot.getElementById("assign").onclick = () => {
const children = []
if (this.shadowRoot.getElementById("c1").checked) children.push(this.children[0])
if (this.shadowRoot.getElementById("c2").checked) children.push(this.children[1])
if (this.shadowRoot.getElementById("c3").checked) children.push(this.children[2])
const slot = this.shadowRoot.querySelector("[name=slot]:checked").value
this.shadowRoot.getElementById(slot).assign(children)
}
}
})
</script>

<elem-1>
<div>div1</div>
<div>div2</div>
<div>div3</div>
</elem-1>