Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Get the Contents of a Custom Element

I'm creating a custom element that will be able to convert its contents from markdown to HTML. However, I'm not able to get the contents of my custom elements.

<!doctype html>
<html>
<body>
   <template id="mark-down">
      <div class="markdown"></div>
   </template>
   <!-- Converts markdown to HTML -->
   <script src="https://cdn.jsdelivr.net/gh/showdownjs/showdown/dist/showdown.js"></script>
   <script>
      customElements.define('mark-down',
         class extends HTMLElement {
            constructor() {
               super()
               let template = document.querySelector('#mark-down').content
               this.attachShadow({ mode: 'open' }).appendChild(template.cloneNode(true))
            }
            connectedCallback() {
               console.log(this) // Returns the whole <mark-down> node and its contents
               console.log(this.innerHTML) // So why does this return a blank string?
               // This should theoretically work --> let markdown = this.innerHTML
               let markdown = '## Test'
               let converter = new showdown.Converter()
               let html = converter.makeHtml(markdown)
               this.shadowRoot.innerHTML = html;
            }
         });
   </script>

   <main>
      <mark-down>
## Our Markdown

These contents should get converted

* One
* Two
* Three
      </mark-down>
   </main>
</body>
</html>

My issue is in the connectedCallback(). When logging this, I get the whole <mark-down> node with its contents in markdown. However, it doesn't seem to have valid properties. Using innerHTML returns a blank, where it should return the markdown; other combinations, like this.querySelector('mark-down'), return null.

What can I do to get the contents of my custom element?

like image 578
Denis G. Labrecque Avatar asked Jul 17 '20 21:07

Denis G. Labrecque


2 Answers

I have been bitten by this so much, I purposely asked a StackOverflow question for people to find

wait for Element Upgrade in connectedCallback: FireFox and Chromium differences

The easiest workaround is a setTimeout in the connectedCallback

<script>
  customElements.define('my-element', class extends HTMLElement {
    connectedCallback() {
      console.log(this.innerHTML);// "" in all Browsers
      setTimeout(() => {
        // now runs asap 
        console.log(this.innerHTML); // "A"
      });
    }
  })
</script>

<my-element>A</my-element>

What this and all mentioned workarounds do is postpone code execution until the DOM is fully parsed.
setTimeout runs after DOMContentLoaded, but if you wrap everything in DOMContentLoaded the whole Element creation runs late, same applies for defer or placing <script> at the end of your page

Supersharp explains the why better in:

wait for Element Upgrade in connectedCallback: FireFox and Chromium differences

like image 181
Danny '365CSI' Engelman Avatar answered Oct 19 '22 07:10

Danny '365CSI' Engelman


After some research online, I found the following nugget about connectedCallback: each time the custom element is appended into a document-connected element. This will happen each time the node is moved, and may happen before the element's contents have been fully parsed.

Therefore, depending on the browser, innerHTML may not in fact be defined when being used. That's why the above snippet, while fine in Firefox, doesn't work in Chrome or Edge.

To solve this problem, place the script tags at the bottom of the body, in which case the element will be parsed first, and the script will know what innerHTML contains.

Another way around this is to wrap the custom element constructor inside a DOM loaded event. That event would look like so:

document.addEventListener('DOMContentLoaded', (e) => {
   class markDown extends HTMLElement {
      ...
   }
}

Yet another way of doing this is putting your script in a separate file, and marking the script tag with the defer attribute.

All three solutions work whether or not the class is explicitly named and defined by a separate statement, as mentioned by Triby's answer, or anonymous and wrapped by the custom element definition function, as in the original question.

like image 3
Denis G. Labrecque Avatar answered Oct 19 '22 06:10

Denis G. Labrecque