I am creating a simple site bundled with Vite, but with no external frameworks like React, Vue, Svelte, etc. The site has repetitive HTML elements using different data. I would like to use custom elements for these. I’m using custom elements instead of full web components because I want to be able to use a global stylesheet. The data is in an external JSON file. I’m experiencing timing problems that create difficulties getting the data to the custom elements in time for them to be configured with it.
My HTML looks like:
<!doctype html>
<html lang="en">
<head>...</head>
<body>
<main>
<my-element data-content-id="0"></my-element>
<another-element data-content-id="1"></another-element>
<my-element data-content-id="2"></my-element>
<another-element data-content-id="3"></another-element>
</main>
<script type="module" src="/main.js"></script>
</body>
</html>
main.js:
import MyElement from "./custom-elements/MyElement.js"
import AnotherElement from "./custom-elements/AnotherElement.js"
const init = () => {
// load external config
const configFile = "/config.json"
fetch(configFile)
.then(config => config.json())
.then(config => {
console.log("[Main] app initiated in main.js")
const myElement = new MyElement(config)
const anotherElement = new AnotherElement(config)
}
})
.catch(error => console.error('Error:', error))
}
init()
MyElement.js:
class MyElement extends HTMLElement {
constructor(config) {
super()
this.config = config
console.log("constructor")
if (this.config) {
console.log("calling useData from constructor", this.config)
this.useData(this.config)
}
}
useData(prop) {
console.log("useData")
// ... logic here ...
}
connectedCallback() {
console.log("connected callback")
if (this.config) {
console.log("calling useData from callback", this.config)
this.useData(this.config)
}
this.innerHTML = `
<div>
...
</div>
`;
}
}
window.customElements.define('my-element', MyElement)
export default MyElement;
When I look at the log, I’m finding that the constructor
and connected callback
are logged before [Main] app initiated in main.js
. Then the constructor
is called/logged again when the MyElement class is instantiated, this time with data, so useData
runs/logs. But that this point, I’ve run the constructor twice, and I’ve lost access the custom element in the DOM to use the data.
So, to summarize, even though I’m not instantiating the class in main.js
until the data has loaded, the constructor is being called twice: once when the custom element appears in the DOM (I guess, and regardless, it is before the data is fetched), and once when it is instantiated with the data (after the data is fetched). Once the data is loaded and the class is instantiated, I don’t have the access to the custom element that I had via connectedCallback
.
Is there a way to use external, fetched data in custom elements and time it correctly?
UPDATE
I realized that I could:
- place the imported data in a global variable, and
- import the custom element classes after that variable is populated.
This give me a main.js
that looks like this:
const init = () => {
// external config
const configFile = "/config.json"
fetch(configFile)
.then(config => config.json())
.then(config => {
console.log("[Main] app initiated in main.js")
window.globalContentData = config
import MyElement from "./custom-elements/MyElement.js"
import AnotherElement from "./custom-elements/AnotherElement.js"
}
})
.catch(error => console.error('Error:', error))
}
init()
And an adjusted MyElement.js:
MyElement.js:
class MyElement extends HTMLElement {
constructor() {
super()
this.config = window.globalContentData
console.log("constructor")
if (this.config) {
console.log("calling useData from constructor", this.config)
this.useData(this.config)
}
}
useData(prop) {
console.log("useData")
// ... logic here ...
}
connectedCallback() {
console.log("connected callback")
if (this.config) {
console.log("calling useData from callback", this.config)
this.useData(this.config)
}
this.innerHTML = `
<div>
...
</div>
`;
}
}
window.customElements.define('my-element', MyElement)
export default MyElement;
This does work. That said, I’m feeling a bit dirty about polluting the global space and am wondering if there is a more elegant way.
FWIW, I’m loving the developer experience of using custom elements for this relatively simple site instead of using larger JS frameworks.
2
Answers
You load your script after the Web Components in DOM have been parsed.
Thus
constructor
andconnectedCallback
(which fires on the opening tag) have already run.See: https://andyogo.github.io/custom-element-reactions-diagram/
You now have a global
init()
which tell? your components what to do?Maybe reverse the logic, make the Web Component load the data (can be memoized)
You can help us answering your question, by adding a minimal-reproducible-example StackOverflow Snippet. It will help readers execute your code with one click. And help create answers with one click.
Something like this may work:
The docs say there’s 3 ways to create an autonomous custom element, and it appears you have a mix of 1 and 3:
https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-autonomous-example
For the other way, first remove the custom elements from your html file so it looks like:
Then here’s what your fetch handler might look like:
And the way i would do this in my projects would look something like: