this.getAttribute not working in web component while violating the spec - javascript

I'm making a web component that will show "Hello {name}!" where {name} comes from name="foo". When I try it I don't get any errors but it just displays "Hello null!".
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./script.js"></script>
</head>
<body>
<hello-world name="Joe"></hello-world>
</body>
</html>
script.js:
class HelloWorld extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const p = document.createElement('p');
p.innerHTML = `Hello ${this.getAttribute('name')}!`;
this.shadowRoot.append(p);
}
}
customElements.define('hello-world', HelloWorld);
In any situation lets assume that name will always have an input.

You must not read attributes in the constructor, otherwise you're violating the specification:
The element's attributes and children must not be inspected, as in the non-upgrade case none will be present, and relying on upgrades makes the element less usable.
From: https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance
You must delay this kind of work until the connectedCallback triggers, or, in the case of attributes, configure a proper attributeChangedCallback.
Conforming to the spec will also solve your initial issue, and it will greatly enhance the functionality and usefulness of your web component.
Please note that it's also not the smartest of ideas to pick an attribute name that already exists in the HTML specification, as a universal attribute.
class HelloWorld extends HTMLElement {
p = document.createElement('p');
constructor() {
super();
this.attachShadow({ mode: 'open' }).append(this.p);
}
static get observedAttributes() { return ['greeting-name']; }
attributeChangedCallback(attr, oldVal, newVal) {
if (oldVal === newVal) return; // nothing to do
switch (attr) {
case 'greeting-name':
this.p.textContent = `Hello ${newVal || 'world'}!`;
break;
}
}
connectedCallback() {
if (!this.getAttribute('greeting-name')) { this.setAttribute('greeting-name', 'world'); }
}
}
customElements.define('hello-world', HelloWorld);
<hello-world greeting-name="Joe"></hello-world>
<hello-world id="foo"></hello-world>
<input type="text" oninput="document.getElementById('foo').setAttribute('greeting-name', this.value)" placeholder="Type a name" />

Related

Javascript, misunderstanding the lifecycle of a CustomHTMLElement: clarification needed

I am currently studying CustomHtmlElements and I have a misunderstanding about the order in which the constructors are invoked.
For example, if I have two CustomHtmlElements:
class ExForm extends HTMLElement {
constructor() {
super();
console.log(`${this.getAttribute('name')} constructor ${performance.now()}`);
//debugger;
}
connectedCallback() {
console.log(`${this.getAttribute('name')} connected ${performance.now()}`);
}
}
customElements.define("ex-form", ExForm);
class ExInput extends HTMLElement {
constructor() {
super();
console.log(`${this.getAttribute('name')} constructor ${performance.now()}`);
//debugger;
}
connectedCallback() {
console.log(`${this.getAttribute('name')} connected ${performance.now()}`);
}
}
customElements.define("ex-input", ExInput);
<!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" />
<title>Document</title>
</head>
<body>
<ex-form name="form1">
<ex-input name="input1" ></ex-input>
</ex-form>
<ex-form name="form2">
<ex-input name="input2"></ex-input>
</ex-form>
</body>
<script src="./index.js"></script>
</html>
I was expecting that the order of constructors execution would be:
form1, input1, form2, input2
However, when I executed the code, the order was:
form1, form2, input1, input2
Can someone clarify why there is a discrepancy between the order of constructor execution and the order in which the HTML elements are rendered to the page?
It depends on when the Web Component is defined.
Your JS code in a StackOverflow Code snippet will run after the DOM is parsed, thus will upgrade the existing Custom Elements (initially parsed as HTMLUnknownElement)
ex-form is defined first, so all existing <ex-form> will be upgraded first.
Then ex-input is defined, and all existing <ex-input> are upgraded next.
(see snippet below) If you move the declaration before the DOM is parsed, you get the order you expect:
In general: don't rely on order in the DOM, you have no control on when the user/developer loads your Web Component file.
There is customElements.whenDefined( ) if you need to check for dependencies.
<script>
class BaseClass extends HTMLElement {
constructor() {
super();
console.log(`constructor ${this.nodeName} `, this.innerHTML);
}
connectedCallback() {
console.log(`${this.nodeName} connected ${this.innerHTML}`);
}
}
customElements.define("ex-form", class extends BaseClass {});
customElements.define("ex-input", class extends BaseClass {});
</script>
<ex-form>
<ex-input></ex-input>
<ex-input></ex-input>
</ex-form>
<ex-form>
<ex-input></ex-input>
</ex-form>

How can I add and remove Native web components based on viewport size?

I want to add/remove the web components from DOM based on viewport size, let's say I have component A, which I want to add on Desktop, and when I resize the window ( shift to mobile view ) I want it to be removed.
can I use media-query here, if yes how?
class DesktopComponent extends HTMLElement {
constructor(){
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `<div>Desktop Component</div>`;
this.shadow.innerHTML = template;
}
}
customElements.define('wc-desktop',DesktopComponent);
class MobileComponent extends HTMLElement {
constructor(){
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = `<div>Mobile Component</div>`;
this.shadow.innerHTML = template;
}
}
customElements.define('wc-mobile',MobileComponent);
<!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">
<title>Document</title>
</head>
<body>
<wc-desktop>
<!-- this should be visible on Desktop only -->
</wc-desktop>
<wc-mobile>
<!-- this should be visible on Mobile only -->
</wc-mobile>
</body>
</html>
Media-queries can not remove DOM content; only do display:none.
You can check in the connectedCallback if CSS properties are set (by a media-query) or whatever check to test for Mobile/Desktop. And then do this.remove()
<template id="WC-DESKTOP">
Desktop
</template>
<template id="WC-MOBILE">
Mobile
</template>
<script>
class ScreenSizeComponent extends HTMLElement {
constructor() {
super() // sets AND returns the 'this' scope
.attachShadow({mode:"open"}) // sets AND returns this.shadowRoot
.append(document.getElementById(this.nodeName).content.cloneNode(true));
}
connectedCallback() {
let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
let isDESKTOPonMobile = isMobile && this.nodeName.includes("DESKTOP");
let isMOBILEonDesktop = !isMobile && this.nodeName.includes("MOBILE");
if (isDESKTOPonMobile || isMOBILEonDesktop) this.remove();
}
disconnectedCallback(){
console.log("removed DOM Element:",this.nodeName);
}
}
customElements.define('wc-desktop', class extends ScreenSizeComponent {});
customElements.define('wc-mobile', class extends ScreenSizeComponent {});
</script>
<wc-desktop>
<!-- this should be visible on Desktop only -->
</wc-desktop>
<wc-mobile>
<!-- this should be visible on Mobile only -->
</wc-mobile>
For more "removing Web Components" fun see: https://dev.to/dannyengelman/the-lt-site-head-web-component-you-never-see-in-f12-dev-tools-147f

HTML Import: How to obtain correct document object within imported html? [duplicate]

I made a simple example using Web Components with two custom elements (v1) where one is nested in another.
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Example</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="import" href="app-container.html">
</head>
<body>
<app-container></app-container>
</body>
</html>
app-container.html:
<link rel="import" href="toolbar.html">
<template id="app-container">
<app-toolbar></app-toolbar>
</template>
<script>
customElements.define('app-container', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = document.currentScript.ownerDocument.querySelector('#app-container').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
</script>
toolbar.html:
<template id="app-toolbar">
<p>Ok!</p>
</template>
<script>
customElements.define('app-toolbar', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = document.currentScript.ownerDocument.querySelector('#app-toolbar').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
</script>
But in the toolbar.html document.currentScript is the same as in the app-container.html and hence querySelector('#app-toolbar') can't find template with id app-toolbar. How to solve this problem?
Example tested on Chrome 55 (without polyfill).
document.currentScript contains a reference to the script that is currently parsed and executed. Therefore it is not valid anymore for your purpose when the constructor() function is called (from another script).
Instead you shoud save its value in a variable at the beginning of the script, and use this variable in the constructor:
<script>
var currentScript = document.currentScript
customElements.define( ... )
...
</script>
If you have multiple scripts, you should use distinct names.
Alternately, you can encapsulate the ephemeral value in a closure:
(function(owner) {
customElements.define('app-container', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = owner.querySelector('#app-container').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
})(document.currentScript.ownerDocument);
Here the value document.currentScript.ownerDocument is assigned to the owner argument which is still defined correctly when constructor() is called.
owner is locally defined so you can use the same name in the other document.

web component (vanilla, no polymer): how to load <template> content?

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);

Nested element (web component) can't get its template

I made a simple example using Web Components with two custom elements (v1) where one is nested in another.
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Example</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="import" href="app-container.html">
</head>
<body>
<app-container></app-container>
</body>
</html>
app-container.html:
<link rel="import" href="toolbar.html">
<template id="app-container">
<app-toolbar></app-toolbar>
</template>
<script>
customElements.define('app-container', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = document.currentScript.ownerDocument.querySelector('#app-container').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
</script>
toolbar.html:
<template id="app-toolbar">
<p>Ok!</p>
</template>
<script>
customElements.define('app-toolbar', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = document.currentScript.ownerDocument.querySelector('#app-toolbar').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
</script>
But in the toolbar.html document.currentScript is the same as in the app-container.html and hence querySelector('#app-toolbar') can't find template with id app-toolbar. How to solve this problem?
Example tested on Chrome 55 (without polyfill).
document.currentScript contains a reference to the script that is currently parsed and executed. Therefore it is not valid anymore for your purpose when the constructor() function is called (from another script).
Instead you shoud save its value in a variable at the beginning of the script, and use this variable in the constructor:
<script>
var currentScript = document.currentScript
customElements.define( ... )
...
</script>
If you have multiple scripts, you should use distinct names.
Alternately, you can encapsulate the ephemeral value in a closure:
(function(owner) {
customElements.define('app-container', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = owner.querySelector('#app-container').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
})(document.currentScript.ownerDocument);
Here the value document.currentScript.ownerDocument is assigned to the owner argument which is still defined correctly when constructor() is called.
owner is locally defined so you can use the same name in the other document.

Categories

Resources