I'm working with a customElement using Shadow DOM like:
<hello-there><b>S</b>amantha</hello-there>
And the innerHTML (generated by lit/lit-element in my case) is something like:
<span>Hello <slot></slot>!</span>
I know that if const ht = document.querySelector('hello-there')
I can call .innerHTML
and get <b>S</b>amantha
and on the shadowRoot for ht, I can call .innerHTML
and get <span>Hello <slot></slot>!</span>
. But...
The browser essentially renders to the reader the equivalent of if I had expressed (without ShadowDOM) the HTML <span>Hello <b>S</b>amantha!</span>
. Is there a way to get this output besides walking all the .assignedNodes
, and substituting the slot contents for the slots? Something like .slotRenderedInnerHTML
?
(update: I have now written code that does walk the assignedNodes and does what I want, but it seems brittle and slow compared to a browser-native solution.)
class HelloThere extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = '<span>Hello <slot></slot>!</span>';
}
}
customElements.define('hello-there', HelloThere);
<hello-there><b>S</b>amantha</hello-there>
<div>Output: <input type="text" size="200" id="output"></input></div>
<script>
const ht = document.querySelector('hello-there');
const out = document.querySelector('#output');
</script>
<button onclick="out.value = ht.innerHTML">InnerHTML hello-there</button><br>
<button onclick="out.value = ht.outerHTML">OuterHTML hello-there</button><br>
<button onclick="out.value = ht.shadowRoot.innerHTML">InnerHTML hello-there shadow</button><br>
<button onclick="out.value = ht.shadowRoot.outerHTML">OuterHTML hello-there shadow (property does not exist)</button><br>
<button onclick="out.value = '<span>Hello <b>S</b>amantha!</span>'">Desired output</button>
Since there doesn't seem to be a browser-native way of answering the question (and it seems that browser developers don't fully understand the utility of seeing a close approximation to what the users are approximately seeing in their browsers) I wrote this code.
Typescript here, with pure-Javascript in the snippets:
const MATCH_END = /(<\/[a-zA-Z][a-zA-Z0-9_-]*>)$/;
/**
* Reconstruct the innerHTML of a shadow element
*/
export function reconstruct_shadow_slot_innerHTML(el: HTMLElement): string {
return reconstruct_shadow_slotted(el).join('').replace(/\s+/, ' ');
}
export function reconstruct_shadow_slotted(el: Element): string[] {
const child_nodes = el.shadowRoot ? el.shadowRoot.childNodes : el.childNodes;
return reconstruct_from_nodeList(child_nodes);
}
function reconstruct_from_nodeList(child_nodes: NodeList|Node[]): string[] {
const new_values = [];
for (const child_node of Array.from(child_nodes)) {
if (!(child_node instanceof Element)) {
if (child_node.nodeType === Node.TEXT_NODE) {
// text nodes are typed as Text or CharacterData in TypeScript
new_values.push((child_node as Text).data);
} else if (child_node.nodeType === Node.COMMENT_NODE) {
const new_data = (child_node as Text).data;
new_values.push('<!--' + new_data + '-->');
}
continue;
} else if (child_node.tagName === 'SLOT') {
const slot = child_node as HTMLSlotElement;
new_values.push(...reconstruct_from_nodeList(slot.assignedNodes()));
continue;
} else if (child_node.shadowRoot) {
new_values.push(...reconstruct_shadow_slotted(child_node));
continue;
}
let start_tag: string = '';
let end_tag: string = '';
// see @syduki's answer to my Q at
// https://stackoverflow.com/questions/66618519/getting-the-full-html-for-an-element-excluding-innerhtml
// for why cloning the Node is much faster than doing innerHTML;
const clone = child_node.cloneNode() as Element; // shallow clone
const tag_only = clone.outerHTML;
const match = MATCH_END.exec(tag_only);
if (match === null) { // empty tag, like <input>
start_tag = tag_only;
} else {
end_tag = match[1];
start_tag = tag_only.replace(end_tag, '');
}
new_values.push(start_tag);
const inner_values: string[] = reconstruct_from_nodeList(child_node.childNodes);
new_values.push(...inner_values);
new_values.push(end_tag);
}
return new_values;
}
Answer in context:
const MATCH_END = /(<\/[a-zA-Z][a-zA-Z0-9_-]*>)$/;
/**
* Reconstruct the innerHTML of a shadow element
*/
function reconstruct_shadow_slot_innerHTML(el) {
return reconstruct_shadow_slotted(el).join('').replace(/\s+/, ' ');
}
function reconstruct_shadow_slotted(el) {
const child_nodes = el.shadowRoot ? el.shadowRoot.childNodes : el.childNodes;
return reconstruct_from_nodeList(child_nodes);
}
function reconstruct_from_nodeList(child_nodes) {
const new_values = [];
for (const child_node of Array.from(child_nodes)) {
if (!(child_node instanceof Element)) {
if (child_node.nodeType === Node.TEXT_NODE) {
new_values.push(child_node.data);
} else if (child_node.nodeType === Node.COMMENT_NODE) {
const new_data = child_node.data;
new_values.push('<!--' + new_data + '-->');
}
continue;
} else if (child_node.tagName === 'SLOT') {
const slot = child_node;
new_values.push(...reconstruct_from_nodeList(slot.assignedNodes()));
continue;
} else if (child_node.shadowRoot) {
new_values.push(...reconstruct_shadow_slotted(child_node));
continue;
}
let start_tag = '';
let end_tag = '';
const clone = child_node.cloneNode();
// shallow clone
const tag_only = clone.outerHTML;
const match = MATCH_END.exec(tag_only);
if (match === null) { // empty tag, like <input>
start_tag = tag_only;
} else {
end_tag = match[1];
start_tag = tag_only.replace(end_tag, '');
}
new_values.push(start_tag);
const inner_values = reconstruct_from_nodeList(child_node.childNodes);
new_values.push(...inner_values);
new_values.push(end_tag);
}
return new_values;
}
class HelloThere extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = '<span>Hello <slot></slot>!</span>';
}
}
customElements.define('hello-there', HelloThere);
<hello-there><b>S</b>amantha</hello-there>
<div>Output: <input type="text" size="200" id="output"></input></div>
<script>
const ht = document.querySelector('hello-there');
const out = document.querySelector('#output');
</script>
<button onclick="out.value = ht.innerHTML">InnerHTML hello-there</button><br>
<button onclick="out.value = ht.outerHTML">OuterHTML hello-there</button><br>
<button onclick="out.value = ht.shadowRoot.innerHTML">InnerHTML hello-there shadow</button><br>
<button onclick="out.value = ht.shadowRoot.outerHTML">OuterHTML hello-there shadow (property does not exist)</button><br>
<button onclick="out.value = reconstruct_shadow_slot_innerHTML(ht)">Desired output</button>