How to append TEMPLATE Element to shadow dom?

4.4k Views Asked by At

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>

2

There are 2 best solutions below

1
On BEST ANSWER

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;

1
On

A 'template' element is a special element that doesn't actually render right away(reference). This is why appending the template produces nothing.

template.firstElementChild.cloneNode means "get the child of the template (i.e. the form) and clone it", which is the same as just appending the form, which works (below).

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'
    });
    shadowDOM.appendChild(form);
    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>