Search code examples
javascriptinterpolationweb-component

Web Components conversion happens too late


In the following (reduced) code the first Tester instance uses a predefined template whereas the second one uses a directly coded one. The interpolation works for the second one since the html contains the proper HTML code. The first Tester instance still has the innerHTML of <test-component> during the time of the interpolation. How can I change the code concept to also interpolate the first example?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Web Components Demo</title>
</head>
<body>
    <div id="tc"></div>
    <div id="tc2"></div>
    <script>
        class TestComponent extends HTMLElement {
            constructor () {
                super();
                this.attachShadow({mode: "open"});
                this.shadowRoot.appendChild(this.createTemplate().content.cloneNode(true));
            }
            createTemplate () {
                const template = document.createElement("template");
                template.innerHTML = "<h1>{{title}}</h1><p>{{text}}</p>";
                return template;
            }
        }
        window.customElements.define("test-component", TestComponent);

        class Tester {
            constructor ({selector, stuff, template}) {
                this.selector = Array.from(document.querySelectorAll(selector));
                this.template = template || null;
                this.stuff = stuff;
                this.render();
                this.interpolate();
            }
            render () {
                this.selector.forEach(s => {if (this.template) s.innerHTML = this.template});
            }
            interpolate () {
                this.selector.forEach(s => {
                    for (let key in this.stuff) { 
                        const regex = new RegExp(`{{ *${key} *}}`, "g");       
                        s.innerHTML = s.innerHTML.replace(regex, this.stuff[key]);;
                    } 
                });
            }
        }

        new Tester ({
            selector: "#tc",
            stuff: {title: "Test Title", text: "Lorem ipsum dolor sit amet"},
            template: "<test-component>"
        });

        new Tester ({
            selector: "#tc2",
            stuff: {title: "Title that works", text: "Lorem ipsum dolor sit amet"},
            template: "<h1>{{title}}</h1><p>{{text}}</p>"
        })
    </script>
</body>
</html>

Solution

  • Your main problem is:

    s.innerHTML = s.innerHTML.replace(regex, this.stuff[key]);
    

    s is the outer DIV, not the <test-component> innerHTML

    effectively destroying and re-creating <test-component> for every key

    Thus the constructor() always sets your default template as (element) innerHTML again:

    console logs on your code running on the first example:

    Note

    constructor () {
        super();
        this.attachShadow({mode: "open"});       
        this.shadowRoot.appendChild(this.createTemplate().content.cloneNode(true));
    }
    createTemplate () {
        const template = document.createElement("template");
        template.innerHTML = "<h1>{{title}}</h1><p>{{text}}</p>";
        return template;
    }
    

    is a bit bloated: You are creating HTML inside a Template, then adding the cloned content of the Template (which is: HTML) to the empty shadowRoot innerHTML

    constructor () {
        super() // returns 'this'
           .attachShadow({mode: "open"}) // returns shadowRoot
           .innerHTML = "<h1>{{title}}</h1><p>{{text}}</p>";
    }
    

    You only have to clone (Templates) when you need to re-use the original (Templates)

    Solution: move {{}} parsing inside the element

    <test-component id=One></test-component>
    <test-component id=Two></test-component>
    <script>
      window.customElements.define("test-component", class extends HTMLElement {
        constructor() {
          super().attachShadow({ mode: "open" });
          this.setTemplate(`<b>{{title}}</b> {{text}}`);
        }
        setTemplate(html, data = {}) {
          this.shadowRoot.innerHTML = html;
          this.parse(data);
        }
        parse(data) {
          let html = this.shadowRoot.innerHTML;
          for (let key in data) {
            const regex = new RegExp(`{{${key}}}`, "g");
            html = html.replace(regex, data[key]);
          }
          this.shadowRoot.innerHTML = html;
        }
      });
      One.parse({
        title: "Test Title",
        text: "Lorem ipsum dolor sit amet"
      })
      Two.setTemplate('<h4>{{title}}<h4>{{subtitle}}', {
        title: "Test Two",
        subtitle: "a Sub title"
      })
      let Three = document.createElement('test-component');
      Three.parse({//parsed IN shadowDOM innerHTML!
        title: "Title Three",
        text: "text three"
      })
      document.body.append(Three)
    </script>