I am trying to have a look at custom elements and how they work and while the examples on MDN work fine I'm seemingly unable to replicate them myself.
The MDN article is here.
This is a working example from MDN.
My problem is that I can't ever seem to pass attributes into my component, they always come out as null instead of passing over the value of the parameter.
My JS is (test.js)
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
// Create a shadow root
const shadow = this.attachShadow({mode: 'open'});
// Create spans
const wrapper = document.createElement('span');
const info = document.createElement('span');
// Take attribute content and put it inside the info span
const text = this.getAttribute('foo'); // <-- this always returns null
info.textContent = `(${text})`;
shadow.appendChild(wrapper);
wrapper.appendChild(info);
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo);
And my Html:
<html>
<head>
<script src="test.js"></script>
</head>
<body>
<hr>
<popup-info foo="Hello World"></popup-info>
<hr>
</body>
</html>
What I'm expecting to see on screen is the text
(Hello World)
but all I ever see is
(null)
When I debug I can see that this.attributes has a length of 0 so it's not being passed in.
Has anyone seen this before when creating custom elements?
Keep Emiel his answer as the correct one.
Just to show there are alternative and shorter notations possible:
customElements.define('popup-info', class extends HTMLElement {
static get observedAttributes() {
return ['foo'];
}
constructor() {
const wrapper = document.createElement('span');
super().attachShadow({mode:'open'})// both SETS and RETURNS this.shadowRoot
.append(wrapper);
this.wrapper = wrapper;
}
attributeChangedCallback(name, oldValue, newValue) {
switch(name) {
case 'foo':
this.wrapper.textContent = `(${newValue})`;
break;
}
}
});
<popup-info
foo="Hello World"
onclick="this.setAttribute('foo','Another world')"
>
</popup-info>
Although your example seems to run fine when I try to run it here in a snippet, I still want to make a suggestion to improve it.
Use the observedAttributes static getter to define a list of attributes which the component should keep an eye on. When the value of an attribute has been changed and the name of the attribute is in the list, then attributeChangedCallback callback is called. In there you can assert logic on what to do whenever you attribute value has been changed.
In this case you could build your string that you desire. This also has the side effect that whenever the attribute value is changed again, the string will be updated.
class PopUpInfo extends HTMLElement {
/**
* Observe the foo attribute for changes.
*/
static get observedAttributes() {
return ['foo'];
}
constructor() {
super();
const shadow = this.attachShadow({
mode: 'open'
});
const wrapper = document.createElement('span');
const info = document.createElement('span');
wrapper.classList.add('wrapper');
wrapper.appendChild(info);
shadow.appendChild(wrapper);
}
/**
* Returns the wrapper element from the shadowRoot.
*/
get wrapper() {
return this.shadowRoot.querySelector('.wrapper')
}
/**
* Is called when observed attributes have a changed value.
*/
attributeChangedCallback(attrName, oldValue, newValue) {
switch(attrName) {
case 'foo':
this.wrapper.textContent = `(${newValue})`;
break;
}
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo);
<html>
<head>
<script src="test.js"></script>
</head>
<body>
<hr>
<popup-info foo="Hello World"></popup-info>
<hr>
</body>
</html>
You're missing a defer attribute in your script import within the HTML and it is not loading properly, thats the problem. The defer attribute allows the script to be executed after the page is parsed
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super()
// Create a shadow root
const shadow = this.attachShadow({ mode: 'open' })
// Create spans
const wrapper = document.createElement('span')
const info = document.createElement('span')
// Take attribute content and put it inside the info span
const text = this.getAttribute('foo') // <-- this always returns null
info.textContent = `(${text})`
shadow.appendChild(wrapper)
wrapper.appendChild(info)
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo)
<html>
<head>
<script src="app.js" defer></script>
</head>
<body>
<hr />
<popup-info foo="Hello World"></popup-info>
<hr />
</body>
</html>
Related
class MyElement extends HTMLElement {
constructor() {
super();
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// data
// Shadow DOM
this._shadowRoot = this.attachShadow({ mode: "open" });
this.render();
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
:host {
display: block;
}
span {color: ${this.color}}
</style>
<p>Notice the console displays three renders: the original, when color changes to blue after 2 secs, and when the array gets values</p>
<p>The color is: <span>${this.color}</span></p>
<p>The array is: ${this.myArray}</p>
`;
return template;
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.render();
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.render();
}
render() {
// Debug only
const props = Object.getOwnPropertyNames(this).map(prop => {
return this[prop]
})
console.log('Parent render; ', JSON.stringify(props));
// end debug
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
<!DOCTYPE html>
<head>
<script type="module" src="./src/my-element.js" type="module"></script>
<!-- <script type="module" src="./src/child-element.js" type="module"></script> -->
</head>
<body>
<p><span>Outside component</span> </p>
<my-element color="green"></my-element>
<script>
setTimeout(() => {
document.querySelector('my-element').color = 'blue';
document.querySelector('my-element').myArray = [1, 2, 3];
}, 2000);
</script>
</body>
I have a native web component whose attributes and properties may change (using getters/setters). When they do, the whole component rerenders, including all children they may have.
I need to rerender only the elements in the template that are affected.
import {ChildElement} from './child-element.js';
class MyElement extends HTMLElement {
constructor() {
super();
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// Shadow DOM
this._shadowRoot = this.attachShadow({ mode: "open" });
this.render();
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
span {color: ${this.color}}
</style>
<p>The color is: <span>${this.color}</span></p>
<p>The array is: ${this.myArray}</p>
<child-element></child-element>
`;
return template;
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.render(); // It rerenders the whole component
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.render();
}
render() {
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
window.customElements.define('child-element', ChildElement);
Because each setter calls render(), the whole component, including children unaffected by the updated property, rerenders.
Yes, if you go native you have to program all reactivity yourself.
(but you are not loading any dependencies)
Not complex, Your code can be simplified;
and you probably want to introduce static get observedAttributes and the attributeChangedCallback to automatically listen for attribute changes
customElements.define('my-element', class extends HTMLElement {
constructor() {
super().attachShadow({ mode: "open" }).innerHTML = `
<style id="STYLE"></style>
<p>The color is: <span id="COLOR"/></p>
<p>The array is: <span id="ARRAY"/></p>`;
}
connectedCallback() {
// runs on the OPENING tag, attributes can be read
this.color = this.getAttribute("color");
this.myArray = this.getAttribute("myArray"); // a STRING!!
}
get color() {
return this._color;
}
set color(value) {
this.setDOM("COLOR" , this._color = value );
this.setDOM("STYLE" , `span { color: ${value} }`);
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this.setDOM("ARRAY" , this._myArray = value );
}
setDOM(id,html){
this.shadowRoot.getElementById(id).innerHTML = html;
}
});
<my-element color="green" myArray="[1,2,3]"></my-element>
<my-element color="red" myArray="['foo','bar']"></my-element>
You are missing a certain level of abstraction. You are trying to emulate the Vue/React way of doing things where whenever the props change, the render() function is repeatedly called. But in these frameworks, render function doesn't do any DOM manipulation. It is simply working on Virtual DOM. Virtual DOM is simply a tree of JS object and thus very fast. And, that's the abstraction we are talking about.
In your case, it is best if you rely on some abstraction like LitElement or Stencil, etc. - any web component creation framework. They would take care of handling such surgical updates. It also takes care of scheduling scenarios where multiple properties are changed in a single event loop but render is still called exactly once.
Having said that, if you want to do this by yourself, one important rule: Never read or write to DOM from the web component constructor function. Access to DOM is after the connectedCallback lifecycle event.
Here is the sample that you can try and expand:
import { ChildElement } from './child-element.js';
class MyElement extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: "open" });
this.attached = false;
}
connectedCallback() {
// Check to ensure that initialization is done exactly once.
if (this.attached) {
return;
}
// Props
this._color = this.getAttribute("color");
this._myArray = this.getAttribute("myArray");
// Shadow DOM
this.render();
this.attached = true;
}
template() {
const template = document.createElement("template");
template.innerHTML = `
<style>
span {color: ${this.color}}
</style>
<p>The color is: <span id="span">${this.color}</span></p>
<p>The array is: ${this.myArray}</p>
<child-element></child-element>
`;
return template;
}
get color() {
return this._color;
}
set color(value) {
this._color = value;
this.shadowRoot.querySelector('#span').textContent = this._color;
this.shadowRoot.querySelector('style').textContent = `
span { color: ${this._color}; }
`;
}
get myArray() {
return this._myArray;
}
set myArray(value) {
this._myArray = value;
this.shadowRoot.querySelector('#span').innerHTML = this._myArray;
}
render() {
this._shadowRoot.innerHTML = '';
this._shadowRoot.appendChild(this.template().content.cloneNode(true));
}
}
window.customElements.define('my-element', MyElement);
window.customElements.define('child-element', ChildElement);
The above example is a classical vanilla JS with imperative way of updating the DOM. As said earlier to have reactive way of updating the DOM, you should build your own abstraction or rely on library provided abstraction.
While messing around with custom elements I wondered if one could use custom attributes within the elements (and possibly within their children too). I know VueJS does something similar with attributes like v-bind, v-for, etc; and I know there's probably a lot more going on under the hood there than I realize. I've tried registering custom elements and attempting to retrieve them like so:
<new-element cool="Awesome!"> </new-element>
class NewElement extends HTMLElement {
constructor() {
super();
this.coolAttr = this.getAttribute("cool");
}
}
customElements.define("new-element", NewElement);
However, when loading the page (in Google Chrome for me) the "custom" attributes disappear, and any attempt at getting them retrieves null. Is there a way to "register" these custom attributes, or do I have to stick with data- attributes?
Attributes become available in the connectedCallback,
they are not available yet in the constructor
Unless the Custom Element is PARSED (in the DOM) BEFORE the Element is defined!!
Also be aware the attributeChangedCallback runs before the connectedCallback
for Observed attributes
Also see: https://andyogo.github.io/custom-element-reactions-diagram/
.as-console-row-code {
font: 12px Arial!important;
background:yellow;
color:darkred;
}
.as-console-row:after{ display:none!important }
<before-element cool="Awesome?">FOO</before-element>
<script>
class NewElement extends HTMLElement {
log( ...args ){
console.log(this.nodeName, `cool:${this.getAttribute("cool")}`,"\t\t\t",...args );
}
static get observedAttributes() {
return ["cool"];
}
constructor() {
const name = "constructor"; // CAN! run code BEFORE super()!
// super() sets AND returns the 'this' scope
super().log(name);
}
connectedCallback() {
this.log("connectedCallback", this.innerHTML || "innerHTML not parsed yet");
// be aware this.innerHTML is only available for PARSED elements
// use setTimeout(()=>{...},0) if you do need this.innerHTML
}
attributeChangedCallback(name, oldValue, newValue) {
this.log(`attributeChangedCallback name:${name}, old:${oldValue}, new:${newValue}`);
}
}
customElements.define("before-element", class extends NewElement {});
customElements.define("after-element", class extends NewElement {});
</script>
<after-element cool="Awesome!!">BAR</after-element>
It can be easily solved by adding the name of the attributes after "data-".
<New-element data-*="Anything you want" />
for example, you can have:
<element data-cool="value">
You can have as many custom attributes as you want.
I want to create a class which has the div element as a property
<div id="corpus"></div>
class ProtoDiv {
divElement: HTMLDivElement;
constructor(){
this.divElement = <HTMLDivElement>document.getElementById("corpus")!;
}
getDivElem(this:ProtoDiv){
console.log(this.divElement)
}
}
const myDiv = new ProtoDiv();
console.log(myDiv.divElement);
why does the property returns null instead of the html div element ?
Your code totally works as plain JavaScript (I've just pasted the result of TypeScript transpilation). You must be missing something like the script execution order (does the div exist when the code execute?).
"use strict";
class ProtoDiv {
constructor() {
this.divElement = document.getElementById("corpus");
}
getDivElem() {
console.log(this.divElement);
}
}
const myDiv = new ProtoDiv();
console.log(myDiv.divElement);
<div id="corpus"></div>
I'm having an issue creating a Web Component using createElement. I'm getting this error:
Uncaught DOMException: Failed to construct 'CustomElement': The result must not have children
at appendTodo
class TodoCard extends HTMLElement {
constructor() {
super()
this.innerHTML = `
<li>
<div class="card">
<span class="card-content">${this.getAttribute('content')}</span>
<i class="fa fa-circle-o" aria-hidden="true"></i>
<i class="fa fa-star-o" aria-hidden="true"></i>
</div>
</li>
`
}
}
window.customElements.define('todo-card', TodoCard)
const todoList = document.getElementById('todo-list')
const todoForm = document.getElementById('todo-form')
const todoInput = document.getElementById('todo-input')
function appendTodo(content) {
const todo = document.createElement('todo-card')
todo.setAttribute('content', content)
todoList.appendChild(todo)
}
todoForm.addEventListener('submit', e => {
e.preventDefault()
appendTodo(todoInput.value)
todoInput.value = ''
})
any ideas?
Thanks.
A Custom Element that sets DOM content in the constructor
can never be created with document.createElement()
You will see many examples (including from me) where DOM content is set in the constructor.
Those Elements can never be created with document.createElement
Explanation (HTML DOM API):
When you use:
<todo-card content=FOO></todo-card>
The element (extended from HTMLElement) has all the HTML interfaces (it is in a HTML DOM),
and you can set the innerHTML in the constructor
But, when you do:
document.createElement("todo-card");
The constructor runs, without HTML interfaces (the element may have nothing to do with a DOM),
thus setting innerHTML in the constructor produces the error:
Uncaught DOMException: Failed to construct 'CustomElement': The result must not have children
From https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance:
The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.
In general, work should be deferred to connectedCallback as much as possible
shadowDOM is a DOM
When using shadowDOM you can set shadowDOM content in the constructor:
constructor(){
super().attachShadow({mode:"open"})
.innerHTML = `...`;
}
Correct code (no shadowDOM): use the connectedCallback:
<todo-card content=FOO></todo-card>
<script>
window.customElements.define(
"todo-card",
class extends HTMLElement {
constructor() {
super();
//this.innerHTML = this.getAttribute("content");
}
connectedCallback() {
this.innerHTML = this.getAttribute("content");
}
}
);
try {
const todo = document.createElement("todo-card");
todo.setAttribute("content", "BAR");
document.body.appendChild(todo);
} catch (e) {
console.error(e);
}
</script>
You have another minor issue: content was a default attribute, and FireFox won't stop warning you:
Or don't use createElement
const todo = document.createElement("todo-card");
todo.setAttribute("content", "BAR");
document.body.appendChild(todo);
can be written as:
const html = `<todo-card content="BAR"></todo-card`;
document.body.insertAdjacentHTML("beforeend" , html);
The connectedCallback can run multiple times!
When you move DOM nodes around:
<div id=DO_Learn>
<b>DO Learn: </b><todo-card todo="Custom Elements API"></todo-card>
</div>
<div id="DONT_Learn">
<b>DON'T Learn!!! </b><todo-card todo="React"></todo-card>
</div>
<script>
window.customElements.define(
"todo-card",
class extends HTMLElement {
connectedCallback() {
let txt = this.getAttribute("todo");
this.append(txt);// and appended again on DOM moves
console.log("qqmp connectedCallback\t", this.parentNode.id, this.innerHTML);
}
disconnectedCallback() {
console.log("disconnectedCallback\t", this.parentNode.id , this.innerHTML);
}
}
);
const LIT = document.createElement("todo-card");
LIT.setAttribute("todo", "Lit");
DO_Learn.append(LIT);
DONT_Learn.append(LIT);
</script>
connectedCallback runs for LIT
when LIT is moved
disconnectedCallback runs (note the parent! The Element is already in the new location)
connectedCallback for LIT runs again, appending "Learn Lit" again
It is up to you the programmer how your component/application must handle this
Web Component Libraries
Libraries like Lit, HyperHTML and Hybrids have extra callbacks implemented that help with all this.
I advice to learn the Custom Elements API first, otherwise you are learning a tool and not the technology.
And a Fool with a Tool, is still a Fool
i'm new on web component. I checked some example, but i really can't figure out how to load (insert in the DOM) the content of a of a separate web component. Starting from this example , I put this code in a file named my-element.html:
<template id="my-element">
<p>Yes, it works!</p>
</template>
<script>
document.registerElement('my-element', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({mode: 'open'});
const t = document.querySelector('#my-element');
const instance = t.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
});
</script>
This is my index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>my titile</title>
<link rel="import" href="my-element.html">
</head>
<body>
Does it work?
<my-element></my-element>
</body>
</html>
I'm on latest Chrome 56, so i don't need polyfill. I run polyserve, and only "Does it works?" appears. I tried (like the original example) the "customElements.define" syntax instead of "document.registerElement", but won't work. Have you some ideas? And what have I to change if i don't want to use shadow dom?
thanks
It's because when you do:
document.querySelector( '#my-element' );
...document refers to the main document index.html
If you want to get the template, you should use instead document.currentScript.ownerDocument
var importedDoc = document.currentScript.ownerDocument;
customElements.define('my-element', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({mode: 'open'});
const t = importedDoc.querySelector('#my-element');
const instance = t.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
});
Note that document.currentScript is a global variable, so it refers to your imported document only when it is currently parsed. That's why it's value is saved in a variable (here: importedDoc) to be reusable later (in the constrcutor call)
If you have multiple imported document you may want to isolate it in a closure (as explained in this post):
( function ( importedDoc )
{
//register element
} )(document.currentScript.ownerDocument);