Happy New Year and Happy First Post of the Year! Not sure that's a thing but this is my blog so I'm making it a thing. The last few days I've been playing with web components again, this time based on a simple idea: Could I create a web component that relies on external data, and use slots to provide content for the various stages of loading? What I mean by that is something like this:
<get-remote-stuff>
<div slot="loading">
Please stand by, I'm loading your stuff.
</div>
<div slot="ready">
I got the remote stuff, here it is!
</div>
<div slot="error">
Sorry, something bad happened.
</div>
</get-remote-stuff>
The idea here is the component would handle automatically showing and hiding each slot based on the state of the remote, async process. I was able to get an example of this working, but I want to be clear that there are parts to this I'm not 100% convinced I understand correctly.
As always, I'm looking for your feedback, so drop me a line if you have any questions or clarifications.
For my first attempt, I used a fake async process that simply used setTimeout
. First, I wrote some simple HTML:
<slot-one>
<span slot="loading">Loading slot</span>
<span slot="ready">Ready slot</span>
<span slot="error">Error slot</span>
</slot-one>
And then I created my slot-one
web component:
class SlotOne extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode:'open'});
const div = document.createElement('div');
div.innerHTML = `
<slot name="loading"></slot>
<slot name="ready"></slot>
<slot name="error"></slot>
`;
const style = document.createElement('style');
style.innerHTML = `
slot {
display:none;
}
`;
shadow.appendChild(div);
shadow.appendChild(style);
}
async connectedCallback() {
console.log('connected callback');
let loader = this.shadowRoot.querySelector('slot[name=loading]');
loader.style.display='inline';
let ready = this.shadowRoot.querySelector('slot[name=ready]');
setTimeout(() => {
console.log('delayed thing done');
loader.style.display='none';
ready.style.display='inline';
}, 3000);
}
}
customElements.define('slot-one', SlotOne);
From the top, I start off by creating two DOM elements. One renders the slots and the other uses CSS to hide them. Notice I'm pointing to the slot
element, not the div
that will be rendered when the component is loaded.
In connectedCallback
, I use querySelector
on the shadowRoot
to unhide the loading slot and get a pointer to the ready
slot.
I do my async process, which in this case is just setTimeout
, and when it's complete, I hide the loader and show the ready state. This seemed to work just fine, and you can see it in action below:
For my second attempt, I wanted to do two things. First, switch to a 'real' async process. I got a free key for Weather API. Given a location value and a key, I could get the weather report here: https://api.weatherapi.com/v1/current.json?key=${key}&q=${q}&aqi=no
. This returns a bunch of information, but for simplicity's sake, I decided to just return the current temperature. Here's the function:
async getTemperature(q,key) {
let resp = await fetch(`https://api.weatherapi.com/v1/current.json?key=${key}&q=${q}&aqi=no`);
let data = await resp.json();
return data.current.temp_f;
}
Yes, I didn't add error checking here, and I should, but as I'm on vacation, I'm being a bit lazy. (Ok, those of you who know me know I don't need an excuse to be lazy. ;)
Ok, with this in place, my goal this time was pretty simple - after getting the result, make it available to the slot. To handle this, I used a simple variable token. Here's how it looks:
<current-temp location="70508">
<span slot="loading">
Getting temperature...
</span>
<span slot="ready">
The temperature is {temp}F.
</span>
<span slot="error">
Error slot
</span>
</current-temp>
I use brackets to represent the variable and the component should handle the replacement. I thought this would be trivial, but here's where I ran into a brick wall. Here's the JavaScript I used to work with the slot before:
let ready = this.shadowRoot.querySelector('slot[name=ready]');
// stuff...
ready.style.display='inline';
But when I tried to write the contents of the slot, nothing worked. I tried innerHTML
, innerText
, even textContents
. Nothing worked. I then tried something else:
let readyDOM = this.querySelector('*[slot=ready]');
This is matching any HTML element with the slot
attribute set to ready
, i.e. the DOM item from the HTML inside the web component. Also, note I'm not using shadowRoot
. So... I can hide and show the slot
elements, but the actual text/HTML is in the "real" item with the named slot. I think that makes sense. Here's the complete connected
callback handler:
async connectedCallback() {
console.log('connected callback');
let loader = this.shadowRoot.querySelector('slot[name=loading]');
loader.style.display='inline';
let ready = this.shadowRoot.querySelector('slot[name=ready]');
let temp = await this.getTemperature(this.location, this.KEY);
loader.style.display='none';
ready.style.display='inline';
let readyDOM = this.querySelector('*[slot=ready]');
let content = readyDOM.innerText;
content = content.replace('{temp}', temp);
readyDOM.innerText = content;
}
This works, and I said, I think it makes sense, but I'd love it if someone were to share some details as I'm a bit fuzzy here. You can see the complete demo below, and yes, it is missing the error handler, but that wouldn't be hard to add.
For folks curious, right now in my zip code, it is 73.9 degrees. Because… Louisiana.
Also published here.