0

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>

1 Answers1

0

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>