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#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
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#address.com');
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#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));
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;
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#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'
});
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>
Related
Im still relatively new to JS. I know i probably shouldnt write my code the way i have done here in the real world, but im only doing this to test my knowledge on for loops and pulling JSON data.
My question is, with the way i have structured my code, is it possible for me to add classnames/Id's to the elements i have made using doc.createElement? for example if i wanted to add custom icons or buttons to each element? I cant seem to think of a way to add them other than having to write out all the HTML and do it that way. Here's my code :
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./styles.css">
<title>Document</title>
</head>
<body>
<section>
</section>
<script src="./app.js"></script>
</body>
</html>
JS
const allCustomers = document.querySelector("section");
let custName = "";
let username = "";
let email = "";
let id = "";
const requestURL = "https://jsonplaceholder.typicode.com/users";
fetch(requestURL)
.then((response) => response.text())
.then((text) => DisplayUserInfo(text));
function DisplayUserInfo(userData) {
const userArray = JSON.parse(userData);
for (i = 0; i < userArray.length; i++) {
let listContainer = document.createElement("div");
let myList = document.createElement("p");
let myListItems = document.createElement("span");
myList.textContent = `Customer : ${userArray[i].name}`;
myListItems.innerHTML =`<br>ID: ${userArray[i].id} <br>Email: ${userArray[i].email} <br>Username: ${userArray[i].username}`;
myListItems.appendChild(myList);
listContainer.appendChild(myListItems);
allCustomers.appendChild(listContainer);
}
}
DisplayUserInfo();
Any pointers would be greatly appreciated as well as any constructive feedback. Thanks
Yes, for sure you can add any attribute for a created element. element.classList.add('class-name-here') for adding class, element.id = 'id-name-here' for adding id.
const allCustomers = document.querySelector("section");
let custName = "";
let username = "";
let email = "";
let id = "";
const requestURL = "https://jsonplaceholder.typicode.com/users";
fetch(requestURL)
.then((response) => response.text())
.then((text) => DisplayUserInfo(text));
function DisplayUserInfo(userData) {
const userArray = JSON.parse(userData);
for (i = 0; i < userArray.length; i++) {
let listContainer = document.createElement("div");
let myList = document.createElement("p");
myList.classList.add('active');
myList.id = 'paragraph'
let myListItems = document.createElement("span");
myList.textContent = `Customer : ${userArray[i].name}`;
myListItems.innerHTML =`<br>ID: ${userArray[i].id} <br>Email: ${userArray[i].email} <br>Username: ${userArray[i].username}`;
myListItems.appendChild(myList);
listContainer.appendChild(myListItems);
allCustomers.appendChild(listContainer);
}
}
DisplayUserInfo();
.active {
color: red;
}
#paragraph {
font-size: 24px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./styles.css">
<title>Document</title>
</head>
<body>
<section>
</section>
<script src="./app.js"></script>
</body>
</html>
is it possible for me to add classnames/Id's to the elements i have
made using doc.createElement
Yes possible with classList for adding class and setAttribute to add id
let listContainer = document.createElement("div");
// To add class
listContainer.className = 'your-class'; //if you have just one
listContainer.classList.add("my-class");//if you want to add multiple
// To add id
listContainer.setAttribute("id", "your_id");
When you use document.createElement it returns an Element. You can use Element attributes and methods to reach what you need. There are some docs for this class on MDN.
This means you can:
> myDiv = document.createElement("div")
<div></div>
> myDiv.id = "test"
'test'
> myDiv
<div id="test"></div>
For classes you can use the attributes className or classList.
I'm attempting to create a simple to-do list and I've encountered two problems:
After refreshing the page, all the created elements are no longer visible on the page despite being in local storage.
After refreshing the page and submitting new values to the input, localStorage overwrites itself.
Despite that, the items displayed from the input fields are from the previous localStorage, which no longer exists (I really hope this makes sense).
const inputEl = document.getElementById("inputEl")
const submitBtn = document.getElementById("submit")
const clearBtn = document.getElementById("clearBtn")
const todoListContainer = document.getElementById("todoList")
const taskContainer = document.querySelector(".task")
const cancelBtn = document.querySelector(".cancelBtn")
const doneBtn = document.querySelector(".doneBtn")
const errorMsg = document.querySelector(".error")
let localStorageContent = localStorage.getItem("tasks")
let tasksItem = JSON.parse(localStorageContent)
let tasks = []
function createTask() {
if (inputEl.value.length != 0) {
const newDiv = document.createElement("div")
newDiv.classList.add("task")
const newParagraph = document.createElement("p")
const newCancelBtn = document.createElement("button")
newCancelBtn.classList.add("cancelBtn")
newCancelBtn.textContent = "X"
const newDoneBtn = document.createElement("button")
newDoneBtn.classList.add("doneBtn")
newDoneBtn.textContent = "Done"
todoListContainer.appendChild(newDiv)
newDiv.appendChild(newParagraph)
newDiv.appendChild(newCancelBtn)
newDiv.appendChild(newDoneBtn)
//^^ Creating a container for a new task, with all its elements and assigning the classes^^
tasks.push(inputEl.value)
inputEl.value = ""
for (let i = 0; i < tasks.length; i++) {
localStorage.setItem("tasks", JSON.stringify(tasks))
newParagraph.textContent = JSON.parse(localStorageContent)[i]
}
errorMsg.textContent = ""
} else {
errorMsg.textContent = "You have to type something in!"
errorMsg.classList.toggle("visibility")
}
}
submitBtn.addEventListener("click", () => {
createTask()
})
clearBtn.addEventListener("click", () => {
localStorage.clear()
})
HTML code below:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style.css">
<script src="/script.js" defer></script>
<title>To-do list</title>
</head>
<body>
<h2 class="error visibility"></h2>
<div id="todoList">
<h1>To-Do List</h1>
<input type="text" name="" id="inputEl" placeholder="Add an item!">
<button type="submitBtn" id="submit">Submit</button>
<button id="clearBtn">Clear list</button>
<div class="task">
</div>
</div>
</body>
</html>
After refreshing the page, all the created elements are no longer visible on the page despite being in local storage
That is because you are rendering the HTML only after the click event and not on page load. To render the HTML for existing tasks stored in the localStorage you have to write a code that loops over your existing tasks in the tasksItem and applies the rendering logic to it.
I would suggest splitting the rendering code from your createTask() function and create a new function for it (for example renderTask()), then you can use it inside a loop on page load and also call the function once a new task is created in the createTask() function.
window.addEventListener('load', (event) => {
// Your read, loop and render logic goes here
})
After refreshing the page and submitting new values to the input, localStorage overwrites itself.
That's because you are actually overriding the tasks in the localStorage. To keep existing tasks, you have to use your tasksItem variable instead of the blank tasks array to create your tasks in and save them to the localStorage.
So, instead of:
tasks.push(inputEl.value)
You would use:
tasksItem.push(inputEl.value)
The same goes for:
for (let i = 0; i < tasksItem.length; i++) {
localStorage.setItem("tasks", JSON.stringify(tasksItem))
// …
}
I created a custom HTML input element like follow:
<html>
<script>
class TestInput extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({mode:'open'});
var cinput = document.createElement('input');
cinput.setAttribute('type', 'text');
cinput.setAttribute('name', 'test');
cinput.setAttribute('value', 'test');
shadow.append(cinput);
this.cinput = cinput;
}
}
customElements.define('test-input', TestInput);
</script>
<body>
<form action="/test">
<test-input></test-input>
<input type="submit"></input>
</form>
</body>
</html>
However, when I used spring to receive the form parameters, I get nothing. How can I submit the form with the value of input inside the shadow root?
Per web.dev/more-capable-form-controls linked in the comments by #nick-parsons.
First you want to add formAssociated static prop to the class of your custom-element and assign it true in order to turn your custom-element into a form-associated custom element like so:
class MyCustomElem extends HTMLElement {
static formAssociated = true;
//...
}
Then you want to call HTMLElement.attachInternals() on your custom-element
can be done in the constructor() like
let internals = this.attachInternals()
in order to use ElementInternals.setFormValue() returned from attachInternals().
Finally, you would use setFormValue() to update the value that your custom-element will submit to the outer form, probably called through an addEventListener().
Modifying the example from the question:
<html>
<script>
class TestInput extends HTMLElement {
// make element form-associated
static formAssociated = true;
constructor() {
super();
var shadow = this.attachShadow({mode:'open'});
var cinput = document.createElement('input');
cinput.setAttribute('type', 'text');
cinput.setAttribute('name', 'test');
cinput.setAttribute('value', 'test');
shadow.append(cinput);
this.cinput = cinput;
// get form-related abilities
this.internals = this.attachInternals();
this.cinput.addEventListener("input",
(event) => {
let value = event.textContent
// update value that form has access to
this.internals.setFormValue(value)
})
}
}
customElements.define('test-input', TestInput);
</script>
<body>
<form action="/test">
<test-input></test-input>
<input type="submit"></input>
</form>
</body>
</html>
I want to create a custom tag with Kotlin that contains default content. The linked example works fine, but I didn't manage to add some default content (e.g. input element) to the custom tag.
I've tried different things, but so far only managed to add the input element next to the custom tag in the DOM, but not inside it.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Client</title>
</head>
<body>
<script src="webcomponentexampleproject.js"></script>
<div id="root"></div>
</body>
</html>
client.kt
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.html.InputType
import kotlinx.html.dom.append
import kotlinx.html.dom.create
fun main() {
window.onload = {
document.getElementById("root")!!.append {
webcomponent {
text = "added it"
+"some more text"
}
}
}
}
WebComponent.kt
import kotlinx.html.*
import kotlinx.html.js.onChangeFunction
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
import kotlin.properties.Delegates
#JsExport
class WebComponent(consumer: TagConsumer<*>, _text: String = "", _backgroundColor: String = "none") :
HTMLTag("webcomponent", consumer, emptyMap(), inlineTag = true, emptyTag = false), HtmlInlineTag {
var text: String by Delegates.observable(_text) { prop, old, new ->
el.value = text
}
var backgroundColor: String by Delegates.observable(_backgroundColor) { prop, old, new ->
el.style.backgroundColor = backgroundColor
}
private val el: HTMLInputElement
init {
//TODO: this input element should be INSIDE the tag
el = consumer.input {
type = InputType.text
value = this#WebComponent.text
}.unsafeCast<HTMLInputElement>()
}
}
// make the new custom tag usable via the kotlinx.html DSL
fun <T> TagConsumer<T>.webcomponent(block: WebComponent.() -> Unit = {}): T {
return WebComponent(this).visitAndFinalize(this, block)
}
Try to call onTagContentUnsafe after the element init:
private val el: HTMLInputElement
init {
el = consumer.input {
type = InputType.text
value = this#WebComponent.text
}.unsafeCast<HTMLInputElement>()
consumer.onTagContentUnsafe {
+el.outerHTML
}
}
Basically, I want to querySelect a <template> from javascript and I keep getting null.
JavaScript file:
class MyImportWebcomponent extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
const shadowRoot = this.attachShadow({ mode: "open" });
const template = document.querySelector("my-import-webcomponent-template");
const instance = template.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
}
customElements.define("my-import-webcomponent", MyImportWebcomponent);
Template object from my vanilla webcomponent
<template id="my-import-webcomponent-template">
<div id="mydiv" name="mydiv">
<p>Trying html importing another javascript file</p>
</div>
</template>
<script src="/to-try/my-import-webcomponent.js"></script>
index.html
<my-import-webcomponent></my-import-webcomponent>
<link rel="import" href="./to-try/my-import-webcomponent.html" />
The main issue is that document.querySelector("my-import-webcomponent-template") returns undefinied.
IN case it adds something usefull, if I try keep both javascript and html in same file and instead of querySelector I create straight the element, I successfully can do it.
All in a single file
const templateString = `<div id="mydiv" name="mydiv"><p>Trying html plus javascript in same file</p></div>`;
const template = document.createElement("template");
template.innerHTML = templateString;
export class MyCompleteWebcomponent extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define("my-complete-webcomponent", MyCompleteWebcomponent);
My question would be exact the same as queryselector if it wasn't for two reasons: (1) they seemed to rely on Polifys which it isn't my case and (2) the answer accepted is based on document.currentScript.ownerDocument which demands an old library as far as I know.
*** edited after suggestion to use instead of
<!-- <link rel="import" href="./to-try/my-import-webcomponent.html" /> -->
<script type="module" src="./to-try/my-import-webcomponent.js"></script>
<my-import-webcomponent></my-import-webcomponent>
Nothing changed at all
*** edited after recommended to add "#". No changes at all
If you want to load HTML files with <link rel="import"> then you'll need to load the HTML Imports library before.
<script src="https://raw.githubusercontent.com/webcomponents/html-imports/master/html-imports.min.js"></script>
<link rel="import" href="./to-try/my-import-webcomponent.html">
...