I am making a custom web component using vanilla JavaScript and am having some issues with when the component should be rendered.
At first I had my render()
and hydrate()
calls inside the constructor of the component. This worked well when the component was already part of the DOM to begin with, however, if the component was created using document.createElement("my-button")
, then the render()
call would end up executing before I would have the chance to add attributes and child elements ect. which was a major problem.
The other alternative, which is what my code below shows, is it have the render()
and hydrate()
calls inside the connectedCallback()
method. This fixes the document.createElement("my-button")
problem, however, it introduces a new one. Because the connectedCallback()
only executes after the element is added to the DOM, I could potentially get a FOUC (Flash of unstyled content) before the component is finished rendering. It would’ve been nice if there was a beforeConnectedCallback()
so I can execute the code before it is added to the DOM, but this does not seem to exist.
So what should I do to get my component to automatically render and hydrate before it is added to the DOM?
Here is my component:
class MYButton extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.render();
this.hydrate();
}
render() {
let type = this.getAttribute("type");
if (type === "link") {
this.elmButton = document.createElement("a");
this.elmButton.setAttribute("href", this.getAttribute("href"));
} else {
this.elmButton = document.createElement("button");
this.elmButton.setAttribute("type", type);
}
while (this.firstChild) {
this.elmButton.appendChild(this.firstChild);
}
this.appendChild(this.elmButton);
}
hydrate() {
this.elmButton.addEventListener("click", () => alert("hello world"));
}
}
customElements.define('my-button', MYButton);
2
Answers
+1 for your
constructor
conclusion, you can’t add DOM there8 out of 10 developers get this wrong… because they define their components with
async
ortype=module
after all DOM is parsed-1 for your
connectedCallback
conclusion, it does NOT fire after DOM is created.For long read see my blogpost:
Web Component developers do not connect with the connectedCallback (yet)
The challenge with your Web Component is, you both want to create new lightDOM (your
button
code) AND read existing lightDOM (yourfirstChild
)Your create after read is perfectly fine, otherwise you would be endlessy adding that new button.
But there will always be a FOUC/Layout Shift, since you can’t have the cake and eat it…
But you can have the Web Component be
hidden
and display it when all work is done,or add a fancy opacity transition yourself.
Without the
render
andhydrate
mumbo-jumbo, the Web Component is:You do not need that
setTimeout
when you stuff all data in attributes instead of lightDOM.The
connectedCallback
fires on the opening tag… so can access allattributes
ShadowDOM and
<slot>
might give a better UI experience, but requires more code.CSS
:not(:defined)
can help, but you are now relying on dependencies outside the Web Componentyou could also do
this.replaceWith(button)
at the end, if your Web Component was only about creating that link/buttonMakes for some cleaner HTML
If you really want to execute after the
constructor
and before theconnectedCallback
you can ABUSE theattributeChangedCallback
because that will fire for every declaredObservedAttribute
before theconnectedCallback
fires on the<my-button>
opening tag.BUT! The lightDOM inside your
<my-button>
still was not parsed yet.There are "gurus" out there that tell you to "solve" this with
async
ortype="import"
They have no clue what is actually happening…
The other answer doesn’t work always. You can’t access attributes in the
constructor
when the DOM wasn’t parsed.Here is an MVP showing the fault in the other answer: https://jsfiddle.net/WebComponents/970xbetz/
What the OP is looking for … quoting the OP …
From my above comment …
Within a next iteration step one could provide an abstraction layer (e.g. an own
ConnectedTarget
class) which not only encapsulates all the code related to a properbeforeconnected
handling, but which likewise incorporates theconnected
handling.The implementation of the OP’s
MyComponent
class, then extendingConnectedTarget
, will be much more lightweight as shown with the following example …The last iteration takes into account every beneath comment of Danny ‘365CSI’ Engelman, regarding script loading, as well as FOUCs and Layout shifts.
The latter is what the OP does want to avoid.
Thus the final implementation now is agnostic of script loading order and independent of any particular timing of any valid
customElements.define
execution.All current changes had to be applied exclusively to the
ConnectedTarget
class, theMyComponent
class remained unchanged.