Search code examples
javascripthtmlweb-componenthtml-templates

How to append TEMPLATE Element to shadow dom?


When I try to append template to the shadow DOM, it only shows as a "#documentFragment", and never renders or copies the actual elements structured within the template.

I spent hours trying to figure it out. The solution I found was to use:

  • template.firstElementChild.cloneNode(true);

instead of:

  • template.content.cloneNode(true);

then, and only then, everything works as expected.

My question is, am I doing something wrong?

const template = document.createElement('template');
const form = document.createElement('form');
const gateway = document.createElement('fieldset');
const legend = document.createElement('legend');
gateway.appendChild(legend);
const username = document.createElement('input');
username.setAttribute('type', 'email');
username.setAttribute('name', 'username');
username.setAttribute('placeholder', '[email protected]');
username.setAttribute('id', 'username');
gateway.appendChild(username);
const button = document.createElement('button');
button.setAttribute('type', 'button');
button.innerHTML = 'Next';
gateway.appendChild(button);
form.appendChild(gateway);
template.appendChild(form);
class UserAccount extends HTMLElement {
  constructor() {
    super();
    const shadowDOM = this.attachShadow({
      mode: 'open'
    });
    const clone = template.firstElementChild.cloneNode(true);
    // This does not work
    // const clone = template.content.cloneNode(true);
    shadowDOM.appendChild(clone);
    shadowDOM.querySelector('legend').innerHTML = this.getAttribute('api');
  }
}
window.customElements.define('user-account', UserAccount);
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">

  <!-- <link rel="stylesheet" href="./css/main.css"> -->
  <script src="./js/user-account.js" defer></script>
  <title>Title</title>
</head>

<body>

  <user-account api="/accounts"></user-account>

</body>

</html>


Solution

  • TEMPLATES are only interesting if you need to make multiple copies or want to work in plain HTML + CSS as much as possible.

    Many Web Components show the usage:

    const template = document.createElement("template");
    template.innerHTML = "Hello World"
    

    and then do:

    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ mode: "open" });
        this._shadowRoot.appendChild(template.content.cloneNode(true));
      }
    

    Which, because the template is only used as a single "parent" container, you can write as:

    constructor() {
        super().attachShadow({ mode: "open" }).innerHTML = "Hello World";
      }
    

    Note: super() returns this, and attachShadow() sets and returns this.shadowRoot ... for free

    Two types of TEMPLATES

    You can create a <TEMPLATE> in DOM, or you can create a template in Memory

    Templates in Memory

    9 out of 10 Memory-templates can be done with other HTMLElements as container,
    as is the case with your code, where FORM can be the main container. No need for a template container.

    If you do build a template in memory, learn the value of append() over (the often misused) appendChild()

    In Memory templates are great for making (many) alterations (with code)

    Templates in DOM

    No need for trying to stuff HTML and CSS in JavaScript strings, you have a DOM in the HTML document!
    Use the <TEMPLATE> HTML Element.

    Add shadowDOM <slot> to the mix and you will spent less time debugging JavaScript and more time writing semantic HTML.

    DOM Templates are great for easy HTML and CSS editting (in your IDE with syntax highlighting) of more static HTML/CSS structures


    Here are both types of TEMPLATES with your code, which one is easier for a developer?

      const form = document.createElement('form');
      const gateway = document.createElement('fieldset');
      const legend = document.createElement('legend');
      const username = document.createElement('input');
      username.setAttribute('type', 'email');
      username.setAttribute('name', 'username');
      username.setAttribute('placeholder', '[email protected]');
      username.setAttribute('id', 'username');
      const button = document.createElement('button');
      button.setAttribute('type', 'button');
      button.innerHTML = 'Next';
      gateway.append(legend,username,button);
      form.appendChild(gateway);
      
      class Form extends HTMLElement {
        constructor(element) {
          super().attachShadow({mode:'open'}).append(element);
        }
        connectedCallback() {
          this.shadowRoot.querySelector('legend').innerHTML = this.getAttribute('api');
        }
      }
      
      window.customElements.define('form-one', class extends Form {
        constructor() {
          super(form)
        }
      });
      window.customElements.define('form-two', class extends Form {
        constructor() {
          super(document.getElementById("FormTwo").content);
        }
      });
    <template id="FormTwo">
      <form>
        <fieldset>
          <legend></legend>
          <input type="email" name="username" placeholder="[email protected]" id="username">
          <button type="button">Next</button>
        </fieldset>
      </form>
    </template>
    
    <form-one api="/accounts"></form-one>
    <form-two api="/accounts"></form-two>

    Note:

    In the above code the <TEMPLATE>.content is moved to shadowDOM.

    To re-use (clone) the <TEMPLATE> the code must be:

    super(document.getElementById("FormTwo").content.cloneNode(true));

    Why your template.content failed

    Your code failed because with

      const template = document.createElement('template');
      const form = document.createElement("form");
      template.appendChild(form);
    

    template has no content

    TEMPLATE isn't a regular HTMLElement, you have to append to .content

      const template = document.createElement('template');
      const form = document.createElement("form");
      template.content.appendChild(form);
    

    will work

    Most Web Component examples show:

      const template = document.createElement("template");
      template.innerHTML = "Hello World"
    

    innerHTML sets .content under the hood

    Which explains why instead of:

    template.content.appendChild(form);

    you can write:

    template.innerHTML = form.outerHTML;