skip to Main Content

I have some JS that creates a web component and then proceeds to add it to a very basic HTML page

 class WordCount extends HTMLParagraphElement {
    constructor() {
      // Always call super first in constructor
      super();

      // count words in element's parent element
      const wcParent = this.parentNode;

      function countWords(node) {
        const text = node.innerText || node.textContent;
        return text
          .trim()
          .split(/s+/g)
          .filter((a) => a.trim().length > 0).length;
      }

      const count = `Words: ${countWords(wcParent)}`;

      // Create a shadow root
      const shadow = this.attachShadow({ mode: "open" });

      // Create text node and add word count to it
      const text = document.createElement("span");

      text.textContent = count;

      // Append it to the shadow root
      shadow.appendChild(text);

      // Update count when element content changes
      setInterval(function () {
        const count = `Words: ${countWords(wcParent)}`;
        text.textContent = count;
      }, 200);
    }
  }

window.customElements.define("word-count", WordCount, { extends: "p" });

var c1 = document.getElementById('component1');
var header= document.createElement('h1')
header.innerText="Web Component 1"
c1.appendChild(header)

var article = document.createElement('article')
article.setAttribute('contenteditable', '')
c1.appendChild(article)

var h2 = document.createElement('h2');
h2.innerText = "Sample Heading";
article.appendChild(h2);

var p1 = document.createElement('p')
p1.innerText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
article.appendChild(p1)

var p2 = document.createElement('p')
p2.innerText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
article.appendChild(p2)

var p3 = document.createElement('p')
p3.setAttribute('is', 'word-count')
article.appendChild(p3)
customElements.upgrade(p3)

There is some VERY basic HTML that this attaches on to

<div id="component1">

</div>

Ultimately I’m puzzled why the word count doesn’t show. This is based upon the word count webcomponent example from mdn. The only difference here is that I’m building the HTML using JS rather than using the HTML directly.

I tried a few things, such as waiting until the HTML loads by wrapping the js in the DOMContentLoaded event listener, or using or not using the customElements.upgrade() method but that didn’t seem to make a difference.

2

Answers


  1. Your WordCount class extends HTMLParagraphElement, which implies it should be used with an instance of <p> tag as a customized built-in element. This requires you to first define this customized element using the window.customElements.define method, and to specify the element to extend using the extends option hence is='word-count' is not a valid custom element definition. Adding this line after you define the class should solve the issue.

    window.customElements.define('word-count', WordCount, { extends: 'p' });
    

    You can also use connectedCallback lifecycle method, which is called when the custom element is connected to the document’s DOM, to start your word count functionality. Try altering your class definition to something like this:

    class WordCount extends HTMLParagraphElement {
      constructor() {
        super();
        this.attachShadow({ mode: "open" });
      }
      
      connectedCallback() {
        this.updateWordCount();
        this.intervalId = setInterval(() => this.updateWordCount(), 200);
      }
    
      disconnectedCallback() {
        clearInterval(this.intervalId);
      }
    
      updateWordCount() {
        const wcParent = this.parentNode;
        
        function countWords(node) {
          const text = node.innerText || node.textContent;
          return text.split(/s+/g).filter(a => a.trim().length > 0).length;
        }
    
        const count = `Words: ${countWords(wcParent)}`;
        this.shadowRoot.textContent = count;
      }
    }
    
    Login or Signup to reply.
  2. First some corrections:

    • // Always call super first in constructor
      The documenation is wrong on this one.
      You can call any JS, just not the ‘this’ scope, because super() SETS and RETURNS the ‘this’ scope
      See my Dev.to post Web Components #102, 5 more lessons after #101

    • const wcParent = this.parentNode;
      You can’t (always) do this in the constructor, there is no Element DOM in the constructor phase. If your script will run before there is any DOM in the page this line will return nothing.
      Use the connectedCallback; that is when then Custom Element is connected to the DOM, but do note it fires on the opening tag, thus any HTML inside has not been parsed yet.

    • customElements.define('word-count', WordCount, { extends: 'p' }); is not supported in Safari. Since 2016 Apple has stated they will not implement Customized Built-In Elements, because those elements do not adhere to the (OOP) Liskov Principle.

    • setInterval is a bit blunt; just waisting CPU cycles.
      You want to use the MutationObserver API here to only monitor changes in HTML.

    A word/letter count Web Component can be done like:

    Note the shadowDOM is not required here; a hinderance when you want to style with global CSS

    <article contenteditable>
      <h3>Badly written Web Components</h3>
      <p>Besides setup, the main problem is that HTML is not treated with the 
    appropriate respect in the design of these components. They are not designed as closely as possible to standard HTML elements, but expect JS to be written for them to do anything. HTML is simply treated as a shorthand, or worse, as merely a marker to indicate where the element goes in the DOM, with all parameters passed in via JS.</p>
      <text-count letters words></text-count>
    </article>
    
    <script>
      customElements.define('text-count', class extends HTMLElement {
        constructor() {
          super().attachShadow({mode:'open'})
                 .innerHTML=`<style>span{font-weight:bold;background:beige;color:black}</style>`+
                            `<span><!--counter--></span>`;
        }
        connectedCallback() {
          let article = this.closest("article");
          if (article) {
            let updatecount /*function*/ = () => {
              let text = article.innerText.trim();//needs tweaking, also counts <text-count>
              this.shadowRoot.querySelector("span").innerText = 
                (this.hasAttribute("letters") ? text.length               + " letters " : "") +
                (this.hasAttribute("words")   ? text.split(/s+/g).length + " words"    : "");
            }
            article.onkeyup = (evt) => updatecount();
            updatecount();
          } else {
            console.warn("I can't find a parent <article>!");
          }
        }
      });
    </script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search