Skip to content

Declarative shadow dom support #1469

@Jamesernator

Description

@Jamesernator

Declarative shadow dom is a relatively new feature, but now has cross browser support so it would be good if parse5 could support parsing and serializing declarative shadow roots.

From an API standpoint I think this would look as follows:

  • add an option to enable shadowroot parsing/serialization
    • note that even in HTML, this is explicitly enabled via using Document.parseHTMLUnsafe/element.setHTMLUnsafe, existing parsing methods explicitly do not have this behaviour by default
  • add a new shadowRoot property to element nodes in the default tree adapter
  • for serialization add an optional list of shadow roots to serialize

For parsing:

  • when encountering a <template shadowrootmode="..."> in the parsing stream do not actually append this element to the parent, rather on the tree adapter call some new method attachDeclarativeShadowRoot or similar on the tree adapter
  • the children of such a template are appended to the resulting shadow root returned from attachDeclarativeShadowRoot
  • additional <template shadowrootmode>` beyond the first should result in these being added as normal children, though it might be worth having an optional hook on the tree adapter to produce warnings (Chrome emits a warning on multiple declarative shadow roots currently for example)

For serializing:

  • an additional function getShadowRoot is added, this returns both a shadow root node and flags for the associated shadowrootdelegatesfocus/shadowrootclonable/shadowrootserializable/shadowrootcustomelementregistry attributes , if and only if the shadow root is serializable then the contents are serialized into a <template> with the appropriate attributes and is added to the element before the rest of the children

As an example:

<!doctype html>

<div>
    <template shadowrootmode="open" shadowrootdelegatesfocus="">
        <p>Hello world</p>
    </template>
</div>
const dom = parse5.parse(loadHtml("./example.html"), {
    allowDeclarativeShadowRoots: true,
    treeAdapter: {
        // ...rest of tree adapter

        // called when `<template shadowrootmode="open"` is encountered
        attachDeclarativeShadowRoot(
            element: Element,
            // in the example above, this would be { mode: "open", delegatesFocus: true, serializable: false, 
            { mode, delegatesFocus, serializable, clonable }: {
                mode: "open" | "closed", 
                delegatesFocus: boolean, 
                serializable: boolean,
                clonable: boolean,
            } 
       ) {
            element._attachDeclarativeShadowRoot({ mode, delegatesFocus, serializable, clonable });
        },
    },
});

// ======
const doc2 = new HTMLDocument();
const el = doc2.createElement("div");
const shadowRoot = el.attachShadow({ mode: "closed", clonable: true });
shadowRoot.innerHTML = `<!--contents-->`
doc2.body.append(el);

// same options as https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#gethtmloptions
const html = parse5.serialize(doc2, {
    // enable serializing declarative shadow roots
    serializableShadowRoots: true,
    // force specific shadow roots to be serialized even if not marked
    // as serializable
    shadowRoots: [shadowRoot],

    treeAdapter: {
        // returns the associated document fragment that acts as the shadow root
        // along with the associated attributes needed for serialization
        // this could optionally be a separate type in tree adapter or the current
        // DocumentType type could just be re-used
        getShadowRoot(element: Element): null | { 
            shadowRoot: DocumentFragment, 
            mode: "open" | "closed",
            delegatesFocus: boolean, 
            serializable: boolean,
            clonable: boolean,
        } {
            if (element._shadowRoot === null) return null;
            
            return { 
                shadowRoot: element._shadowRoot,
                mode: element._shadowRoot._mode,
                // etc
            }
        }
    },
});

// Effective output: (ignoring whitespace differences)
assertHTMLEqual(html, `
    <!doctype html>
    <html>
        <head></head>
        <body>
            <div>
                <template shadowrootmode="closed" shadowrootclonable="">
                    <!--contents-->
                </template>
            </div>
        </body>
    </html>
`);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions