How to prevent flickering with web components? - javascript

Consider a simple web component
class TimeAgo extends HTMLElement {
constructor() {
super();
this.innerHTML = '2 hours ago'
}
}
customElements.define('time-ago', TimeAgo);
Being used like this
This post was created <time-ago datetime="2020-09-26T11:28:41.9731640+01:00">September 26th</time-ago>
When the page renders, the browser will first write "September 26th" and right after that switch to "2 hours ago".
Is there a way to prevent this from happening? I'd much prefer to display "2 hours" on first paint.
The JS file is loaded through a CDN, moving the script tag up and down the response didn't change anything.

But is your script executed before that part of the DOM is parsed?
Code below displays 1A in Chromium and 1 in FireFox,
because (don't pin me on terminology) in Chromium the 1 is injected into the DOM before content A is parsed.
So if you don't want a FOUC leave <time-ago> content empty.. or maybe blur it with CSS
<!DOCTYPE html>
<html>
<head>
<script>
window.onload = () => {
console.log("onload");
};
customElements.define(
"my-element",
class extends HTMLElement {
constructor() {
super();
console.log("constructor");
}
connectedCallback() {
console.log("connectedCallback");
this.innerHTML = "1";
}
}
);
</script>
</head>
<body>
<my-element>A</my-element>
<script>
console.log("end DOM");
</script>
</body>
</html>

One of the possible solution I can think of is to enable shadow-dom
class TimeAgo extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.innerHTML = '2 hours ago'
}
}
customElements.define('time-ago', TimeAgo);
By defining your web component to be shadow dom, you can change the content even before it's attached to the DOM.

Related

Trying to apply this script to a specific div

Here's a transition loader running while downloading an external mp3, waiting to be ready to stream with <audio></audio> tags inside a <div class="player"></div>.
How can I apply this script to the specific <div class="player"></div> only and not to the whole page? Thanks.
let e= {
backgroundColor: "#fff", filterBrightness:2, strokeWidth:10, timeOnScreen:100
},
t=document.querySelector("*"),
r=document.createElement("style"),
i=document.createElement("<div>"),
s="http://www.w3.org/2000/svg",
n=document.createElementNS(s, "svg"),
o=document.createElementNS(s, "circle");
document.head.appendChild(r),
r.innerHTML="#keyframes swell{to{transform:rotate(360deg)}}",
i.setAttribute("style", "background-color:"+e.backgroundColor+";color:"+e.backgroundColor+";display:flex;align-items:center;justify-content:center;position:fixed;top:0;height:100vh;width:100vw;z-index:2147483647"),
document.body.setAttribute("style", "overflow:hidden!important;"),
document.body.prepend(i),
n.setAttribute("style", "height:50px;filter:brightness("+e.filterBrightness+");animation:.3s swell infinite linear"),
n.setAttribute("viewBox", "0 0 100 100"),
i.prepend(n),
o.setAttribute("cx", "50"),
o.setAttribute("cy", "50"),
o.setAttribute("r", "35"),
o.setAttribute("fill", "none"),
o.setAttribute("stroke", "currentColor"),
o.setAttribute("stroke-dasharray", "165 57"),
o.setAttribute("stroke-width", e.strokeWidth),
n.prepend(o),
t.style.pointerEvents="none",
t.style.userSelect="none",
t.style.cursor="wait",
window.onload=()=> {
setTimeout(()=> {
t.style.pointerEvents="", t.style.userSelect="", t.style.cursor="", i.remove(), document.body.setAttribute("style", "")
}
, e.timeOnScreen)
}
Convert "myplayer" into a web component.
https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
class MyPlayer extends HTMLParagraphElement {
constructor() {
// Always call super first in constructor
super();
// Element functionality written in here
// ADD YOUR CODE HERE
}
}
customElements.define("myplayer", MyPlayer);
This creates an HTML element that has code specific to its self.

Changing shadowRoot.innerHTML stop inherited behavior for attributes and events

Below ake-class2 inherits from/extends ake-class1.
Adding <select> element to ake-class2.shadowRoot.
console.log this.clickme button to make sure it's inherited correctly.
clickme button doesn't work without adding again lines after comment These 3 lines in ake-class2.
I couldn't understand why this behavior happen.
why this happpens ?
<html>
<head>
<title>AKE Front</title>
<script>
class1_html = `
<div class="container">
<button class="clickme">Click Me</button>
</div>
`
class2_html = `
<select></select>
`
/*--------------------------------------------------------------------------------*/
class AKEclass1 extends HTMLElement { //custom-component class
constructor() {
super(); // always call super() first in the constructor.
//const root = this.createShadowRoot(); //chrome only - deprecated
const root = this.attachShadow({mode: 'open'}); //By calling attachShadow with mode: 'open', we are telling our element to save a reference to the shadow root on the element.shadowRoot property
this.shadowRoot.innerHTML = class1_html;
// These 3 lines
this.container = this.shadowRoot.querySelector("div.container");
this.clickme = this.container.querySelector("button.clickme");
this.clickme.addEventListener("click", this.clickMe.bind(this));
}
clickMe() {
alert("Hello !");
}
}
customElements.define('ake-class1', AKEclass1);
/*--------------------------------------------------------------------------------*/
class AKEclass2 extends AKEclass1 { //custom-component class
constructor() {
super(); // always call super() first in the constructor.
this.shadowRoot.innerHTML += class2_html;
// These 3 lines
//this.container = this.shadowRoot.querySelector("div.container");
//this.clickme = this.container.querySelector("button.clickme");
//this.clickme.addEventListener("click", this.clickMe.bind(this));
}
}
customElements.define('ake-class2', AKEclass2);
/*--------------------------------------------------------------------------------*/
</script>
</head>
<body>
<ake-class2 class="ake_window"></ake-class2>
</body>
</html>
As mentioned in the comments .innerHTML += is the culprit.
What it does:
Create a NEW string by concatening .innerHTML + NEWString
delete the innerHTML DOM tree
and then Garbage Collection (GC) kicks in:
Delete all existing DOM elements, thus remove all connected listeners
set the NEW String as innerHTML
Some 'gurus' say this makes innerHTML evil, I say you need to understand what it does.
In the SO snippet below you see the listener being connected twice, but only executed once when clicked
<script>
class BaseClass extends HTMLElement {
constructor() {
super().attachShadow({mode:'open'})
.innerHTML = `<button>Click ${this.nodeName}</button>`;
this.listen();// but removed by GC
}
listen(){
console.log("add listener on", this.nodeName);
this.shadowRoot
.querySelector("button")
.onclick = (evt) => this.clicked(evt);
}
clicked(evt){
console.log("clicked", this.nodeName)
}
}
//customElements.define('element-1', BaseClass);
customElements.define('element-2', class extends BaseClass {
connectedCallback(){
this.shadowRoot.innerHTML += ` with concatenated HTML`;
this.listen();
}
});
</script>
<element-2></element-2>
Notes:
Using the inline onclick handler, it only allows for one handler where addEventListener can add more (you can use it here if you like)
No need for oldskool .bind(this) by defining lexical scope with a arrow function, not a function reference
all can be chained because
super() sets AND returns the this scope
attachShadow sets AND returns this.shadowRoot

Slots does not work on a html web component without shadow dom

I have a html web component without shadow dom and I try to add a slot. For some reason it does not work.
I expected it to switch "Foo bar" to "Hello world" but that does not happen.
Does slots only works with shadow dom and a template?
How can I get it to work?
class HelloWorld extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `
<div>
<slot name="element-name">Foo Bar</slot>
</div>
`;
}
}
customElements.define("hello-world", HelloWorld);
<hello-world>
<span slot="element-name">Hello World</span>
</hello-world>
Yes, <slot> only works in shadowDOM
Slotted content is reflected lightDOM content
See: ::slotted CSS selector for nested children in shadowDOM slot
A Web Component without shadowDOM only has innerHTML
If you do this.innerHTML= on such a Web Component it replaces the innerHTML, just like on any other HTML tag
with shadowDOM:
<hello-world>
<b slot="none">Mighty</b>
<span slot="title">Web Components</span>
Hello!
</hello-world>
<script>
customElements.define("hello-world", class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:"open"})
.innerHTML = `<div><slot></slot><slot name="title">Foo Bar</slot></div>`;
this.onclick = (evt) => this.querySelector('b').slot = "title";
}
});
</script>

Lit-Element: which event to use for DOM updates?

The documentation over at github.com/Polymer/lit-element describes the lifecycl, if a property of some lit-element is changed. However, I can not seem to find any documentation about a lifecycle if the DOM content of the element is changed.
So assume I have some nested DOM structure and my outermost element should display something based on the DOM content. For sake of simplicity the example below will just display the number of child-elements of the given type.
Now at some point my application inserts a new nested element (click the test button below). At this point I would like to update the shown count.
From my tests it seems that render() is not called again in that case, neither is updated().
Which event do I need to listen or which function do I need to implement for to recognize such a change?
My only current workaround is to use requestUpdate() manually after the DOM update, but I think such changes should be handled by lit-element itself.
document.querySelector( 'button' )
.addEventListener( 'click', () => {
const el = document.querySelector( 'my-element' );
el.insertAdjacentHTML( 'beforeend', '<my-nested-element>new addition</my-nested-element>' );
})
my-element, my-nested-element {
display: block;
}
<script src="https://unpkg.com/#webcomponents/webcomponentsjs#latest/webcomponents-loader.js"></script>
<!-- Works only on browsers that support Javascript modules like Chrome, Safari, Firefox 60, Edge 17 -->
<script type="module">
import {LitElement, html} from 'https://unpkg.com/#polymer/lit-element/lit-element.js?module';
class MyElement extends LitElement {
constructor(){
super();
this.number = this.querySelectorAll( 'my-nested-element' ).length;
}
render() {
return html`<p>number of my-nested-element: ${this.number}</p>
<slot></slot>`;
}
}
customElements.define('my-element', MyElement);
class MyNestedElement extends LitElement {
render() {
return html`<slot></slot>`;
}
}
customElements.define('my-nested-element', MyNestedElement);
</script>
<my-element>
<my-nested-element>first</my-nested-element>
<my-nested-element>second</my-nested-element>
</my-element>
<button>test</button>
In order to detect a new element inserted from the Light DOM through a <slot> element, you can listen to slotchange events on the <slot> element, or on the Shadow DOM root itself.
See the running example below:
document.querySelector('button').onclick = () =>
document.querySelector('my-element').insertAdjacentHTML('beforeend', '<my-nested-element>new addition</my-nested-element>');
my-element,
my-nested-element {
display: block;
}
<script type="module">
import {LitElement, html} from 'https://unpkg.com/#polymer/lit-element/lit-element.js?module';
class MyElement extends LitElement {
firstUpdated() {
var shadow = this.shadowRoot
var nb = shadow.querySelector( 'span#nb' )
shadow.addEventListener( 'slotchange', () =>
nb.textContent = this.querySelectorAll( 'my-nested-element').length
)
}
render() {
return html`<p>number of my-nested-element: <span id="nb"></span></p>
<slot></slot>`;
}
}
customElements.define('my-element', MyElement);
</script>
<my-element>
<my-nested-element>first</my-nested-element>
<my-nested-element>second</my-nested-element>
</my-element>
<button>test</button>

Why can't I get the DOMNode's style (scrollHeight) in the React componentDidMount?

I am developping a React Application and trying to get the computed style 'scrollHeight' of a DOM Element.
I put this code in the componentDidMount:
componentDidMount() {
// let _this = this;
// window.onload = function(){
let imgFigureDOM = findDOMNode(_this.refs.imgFigure0),
imgW = imgFigureDOM.scrollWidth,
imgH = imgFigureDOM.scrollHeight;
// }
}
But, I can't get the correct value of scrollHeight only in the chrome browser.It seems that the chrome is not enough fast to render the DOMNode completely when the findDOMNode execute.
The value is correct when I use window.onload as above, but Shouldn't the DOMNode was completely loaded when the componentDidMount execute?
Thank you for your patient answer!
componentDidMount() is called when your React component is rendered. React has rendered an <img> tag, that doesn't mean that the image is loaded.
Let's set up some basic definitions to distinguish rendered from loaded:
rendered: React has converted your virtual DOM elements (specified in the render method) into real DOM elements and attached them to the DOM.
loaded: Image data or other remote content has downloaded completely (or failed to download).
So just add the onLoad and onError event handlers to your React <img> tag and away you go.
image-events
Short Example:
import React from 'react';
class ImageWithStatusText extends React.Component {
constructor(props) {
super(props);
this.state = { imageStatus: null };
}
handleImageLoaded(e){
this.setState({ imageStatus: 'loaded' });
console.log(e.target.scrollHeight);
}
handleImageErrored() {
this.setState({ imageStatus: 'failed to load' });
}
render() {
return (
<div>
<img
src={this.props.imageUrl}
onLoad={this.handleImageLoaded.bind(this)}
onError={this.handleImageErrored.bind(this)}
/>
{this.state.imageStatus}
</div>
);
}
}
export default ImageWithStatusText;

Categories