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:
instead of:
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@address.com');
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>
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
You can create a <TEMPLATE>
in DOM, or you can create a template
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)
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@address.com');
username.setAttribute('id', 'username');
const button = document.createElement('button');
button.setAttribute('type', 'button');
button.innerHTML = 'Next';
gateway.append(legend,username,button);
form.append(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@address.com" 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));
template.content
failedYour 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;