5
\$\begingroup\$

This is my first web component. I implement it as an autonomous custom HTML element. I cannot use a customized built-in element because it's not supported by Safari. Nevertheless, as it mainly behaves as a details HTML element, and as I do not want to reinvent the wheel, I use this type of element as a delegate. My example is self-contained to be easier to be tested, but I will obviously move the JavaScript code into a module later. It must work only in very modern web browsers.

It reproduces at least one behavior of the Java Swing JTree by using a padding. I would like to know whether it is possible to write it more concisely without sacrificing performance, I do not want to insert one style element per instance of my custom element.

<!DOCTYPE html>
<html lang="en">
<head>
  <script>

    customElements.define("custom-tree", class extends HTMLElement {

        // style sheet shared by all HTML elements created by this class
        static #sharedStyleSheet = new CSSStyleSheet();

        // previous owner document, tracked to handle adoption cases (see Document.adoptNode())
        #previousOwnerDocument;

        constructor() {
            super();
            // attaches the shadow DOM here as it must be done only once, keeps it open so that it can be accessed later through the shadowRoot property
            this.attachShadow({mode: "open"});
            // the previous owner document is not known yet as the constructor is called before the connection
            this.#previousOwnerDocument = null;
        }

        #ensuresOwnerDocumentSharedStyleSheetSupportEnabledIfNeeded() {
            // if the shared style sheet is not in the adopted style sheets of the owner document
            if (this.ownerDocument.adoptedStyleSheets.includes(this.constructor.#sharedStyleSheet) === false) {
                // adds the shared style sheet into the adopted style sheets of the owner document
                this.ownerDocument.adoptedStyleSheets.push(this.constructor.#sharedStyleSheet);
            }
        }

        #ensuresPreviousOwnerDocumentSharedStyleSheetSupportDisabledIfUnneeded() {
            // if this custom element is absent of the owner document
            if (this.#previousOwnerDocument.querySelectorAll(this.localName).length === 0) {
                // looks for the index of the shared style sheet in the adopted style sheets of the owner document
                const indexOfSharedStyleSheet = this.#previousOwnerDocument.adoptedStyleSheets.indexOf(this.constructor.#sharedStyleSheet);
                // if the shared style sheet is still in the adopted style sheets of the document
                if (indexOfSharedStyleSheet !== -1) {
                    // removes the shared style sheet from the adopted style sheets of the owner document
                    this.#previousOwnerDocument.adoptedStyleSheets.splice(indexOfSharedStyleSheet, 1);
                }
            }
        }

        connectedCallback() {
            // if the shared style sheet is empty
            if (this.constructor.#sharedStyleSheet.cssRules.length === 0) {
                // inserts a single rule, uses the lowercase tag name
                this.constructor.#sharedStyleSheet.insertRule(`${this.localName} > details details { padding-left: 1em; }`);
                // prevents the shared style sheet from being modified later
                Object.freeze(this.constructor.#sharedStyleSheet);
            }
            this.#ensuresOwnerDocumentSharedStyleSheetSupportEnabledIfNeeded();
            // uses a template with a single unnamed slot to use the content of this slot as a delegate and to avoid extending its class as it is unsupported by some web browsers (Safari)
            const templateElement = this.ownerDocument.createElement("template");
            templateElement.innerHTML = "<slot></slot>";
            this.shadowRoot.appendChild(templateElement.content.cloneNode(true));
            // ensures that the slot accepts only one details HTML element by using a slot change listener to detect an illegal change early
            this.shadowRoot.lastElementChild.addEventListener("slotchange", slotChangeEvent => {
                const assignedElements = slotChangeEvent.currentTarget.assignedElements();
                if (1 < assignedElements.length || assignedElements.some(assignedElement => assignedElement.localName !== "details")) {
                    // removes all children from the custom element
                    this.replaceChildren();
                    throw new Error("This custom element can only contain at most one details HTML element");
                }
                if (assignedElements.length === 1) {
                    // lets the summary markers open and close the details but not their respective contents
                    assignedElements[0].querySelectorAll(`:scope summary > *`).forEach(summaryChild => summaryChild.addEventListener("click", summaryChildClickEvent => summaryChildClickEvent.preventDefault()));
                }
            });
            // updates the previous owner document
            this.#previousOwnerDocument = this.ownerDocument;
        }

        connectedMoveCallback() {
            // the previous owner document remains unchanged as this element stays in the same document
        }

        adoptedCallback() {
            // this.ownerDocument contains the adopter (i.e the current owner document), not the previous owner document
            // ensures that the previous owner document stops supporting the shared style sheet if unneeded as this element is no longer in this document
            this.#ensuresPreviousOwnerDocumentSharedStyleSheetSupportDisabledIfUnneeded();
            // updates the previous owner document
            this.#previousOwnerDocument = this.ownerDocument;
            // ensures the current owner document supports the shared style sheet
            this.#ensuresOwnerDocumentSharedStyleSheetSupportEnabledIfNeeded();
        }

        disconnectedCallback() {
            // ensures that the previous owner document stops supporting the shared style sheet if unneeded as this element is no longer in this document
            this.#ensuresPreviousOwnerDocumentSharedStyleSheetSupportDisabledIfUnneeded();
            // updates the previous owner document
            this.#previousOwnerDocument = null;
        }
    });
</script>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
</head>
<body>
<custom-tree><details>
  <summary><span>Test 0</span></summary>
  <details>
  <summary><span>Test 1</span></summary>
    <details>
  <summary><span>Test 2</span></summary>
</details>
</details>
</details></custom-tree>
</body>
</html>
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I'd say the following changes would help with the overall conciseness and should not affect performance:

  1. Remove connectedMoveCallback; This isn’t a valid lifecycle method in the Custom Elements spec. Dropping it makes the class shorter and clearer, with no performance impact. This may depend on context though, but generally its worth taking off.

  2. Reuse a static <template>; Instead of creating a new <template> inside connectedCallback every time, define it once (e.g., as a static property). This reduces repeated code and avoids unnecessary DOM creation, improving both conciseness and efficiency.

  3. Shorten verbose private method names: Methods like #ensuresOwnerDocumentSharedStyleSheetSupportEnabledIfNeeded() could be renamed to something concise like #enableSharedStyles() and #disableSharedStyles(). This improves readability without changing behavior or performance.

New contributor
Chip01 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.