How can I catch children added to a web component before first render?

113 Views Asked by At

I'm attempting to create a web component that modifies the behavior and style of its child elements. However, I'd like to do this modification before the first render, so that no flickering occurs.

Below is a (hopefully) small example of a web component that this is intended to create content (identified by the content class) that is hidden when the user clicks the header (identified by the trigger class).

"use strict";

customElements.define(
  "shrinkable-content",
  class extends HTMLElement {
    static observedAttributes = ["shrunk"];

    constructor() {
      super();
    }

    connectedCallback() {
      setTimeout(() => {
        const triggers = Array.from(this.getElementsByClassName("trigger"));
        const contents = Array.from(this.getElementsByClassName("content"));
        if (typeof this.shrunk === "undefined") {
          this.shrunk = false;
        }
        contents.forEach( (content) => {
          content.oldStyle = content.style.display;
          if (this.shrunk) content.style.display = "none";
        });
        triggers.forEach((trigger) => {
          trigger.style.cursor = "pointer";
          trigger.addEventListener("click", (event) => {
            this.shrunk = !this.shrunk;
            contents.forEach((content) => {
              content.style.display = this.shrunk ? "none" : content.oldStyle;
            });
          });
        });
      });
    }

    attributeChangedCallback(name, oldValue, newValue) {
      if (name === 'shrunk') this.shrunk = (newValue === "true") || (newValue === "");
    }
  }
);
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <title>HTML + JavaScript</title>
    <script src="./shrinkable-content.js"></script>
  </head>
  <body>
    <ul>
      <li>
        <shrinkable-content shrunk>
          <button class="trigger">Alpha Item</button>
          <ul class="content">
            <li>Item One</li>
            <li>Item Two</li>
          </ul>
        </shrinkable-content>
      </li>
    </ul>
  </body>
</html>

Notice the setTimeout() call early in the connectedCallback() function. I added that because without it, the function is called before the children are added/mounted. However, it also delays the execute until after the first render, resulting in flickering.

For some more context: I'm really trying to avoid the 'shadow' DOM approach here; it really goes against the design principals I'm trying to follow.

Edit: There have been a couple solutions presented for the 'shrinking' feature (which is appreciated), but the real question here is about the lifecycle of web components; The example is just to help discuss the issue/question.

1

There are 1 best solutions below

2
On

Looks to me like you are rebuilding default HTML <details> and <summary>

<details>
  <summary>Alpha Item</summary>
  <ul class="content">
    <li>Item One</li>
    <li>Item Two</li>
  </ul>
</details>

Or wrapped in a more advanced Web Component, handling closing other opened <details>

<style>
  summary { font-weight: bold; cursor: pointer }
  details[open] { background: beige  }
</style>

<details-accordion>
  <details open>
    <summary>Summary 1</summary>
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
  </details>
  <details>
    <summary>Summary 2</summary>
      "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  </details>
  <details>
    <summary>Summary 3</summary>
      "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
   </details>
</details-accordion>

<script>
customElements.define(
  'details-accordion', class extends HTMLElement {
    connectedCallback() {
      this.onclick = e => [...this.children].forEach(d => 
                                    !e.ctrlKey 
                                      && 
                                    d.toggleAttribute("open", e.target == d));
    }
  });
</script>