As part of this ongoing series on Web Components, we've created our first Web Component, learned how to add options and settings, and learned how to progressively enhance Web Components.
Today, we're going to learn about the different ways to instantiate them (and some of the challenges with each approach).
Let's dig in!
"Instantiate" isn't exactly the right word
I used the word "instantiate" in the title of this article, but technically that's what happens when the constructor()
method runs.
What I mean more specifically is setting up the Web Component: getting child elements, setting properties, injecting any additional HTML, modifying attributes, and adding event listeners.
There are a few different ways and places to do that, with pros and cons to each.
Inside the constructor()
This is what we've been doing for all of the articles in the series so far.
The constructor()
is where we've put all the things. And generally, that works just fine!
It's simple, easy-to-read, and keeps everything together in one spot.
/** * The class constructor object */ constructor () { // Always call super first in constructor super(); // Instance properties this.button = this.querySelector('button'); this.count = parseFloat(this.getAttribute('start')) || 0; this.step = parseFloat(this.getAttribute('step')) || 1; this.text = this.getAttribute('text') || 'Clicked Times'; // Listen for click events this.button.addEventListener('click', this); // Announce UI updates this.button.setAttribute('aria-live', 'polite'); }
It becomes a challenge, though, when you dynamically create and inject a Web Component into an existing UI.
Imagine we have an existing page, and sometime after it loads, we decide to create and inject a new <wc-count>
element onto the page.
We'll start by using the document.createElement()
method to create the element. We'll use the Element.innerHTML
property to give it some content. Then, we can use the Element.append()
method to inject it into the UI.
let app = document.querySelector('#app'); let counter = document.createElement('wc-count'); // 👆 The constructor() runs here... counter.innerHTML = `<button>Clicked 0 Times</button>`; app.append(counter);
This looks great! But… it throws an Uncaught TypeError
:
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
The constructor()
method runs on the new <wc-count>
element as soon as we create it with the document.createElement()
method.
This kicks off all of the setup activities before we've used the Element.innerHTML
property to add the required <button>
. When we go to add our click
event listener to this.button
, we get an error because that button doesn't exist yet.
Here's a demo on CodePen.
Inside the connectedCallback()
method
Web Components have a handful of lifecycle events that automatically run callback methods, if you specify them.
We'll learn more about that in a future article, but one of those callback methods is the connectedCallback()
method, which runs when the element is actually connected to the DOM.
If we move all of the setup functions from the constructor()
to the connectedCallback()
method, we no longer get the Uncaught TyperError
.
/** * The class constructor object */ constructor () { // Always call super first in constructor super(); } /** * Setup the Web Component when it's connected to the DOM */ connectedCallback () { // Instance properties this.button = this.querySelector('button'); this.count = parseFloat(this.getAttribute('start')) || 0; this.step = parseFloat(this.getAttribute('step')) || 1; this.text = this.getAttribute('text') || 'Clicked Times'; // Listen for click events this.button.addEventListener('click', this); // Announce UI updates this.button.setAttribute('aria-live', 'polite'); }
Here's another demo.
Does that mean we should always run our setup tasks in the connectedCallback()
method?
Unfortunately, it's not quite that easy.
If for some reason a developer appends the new <wc-count>
element to the DOM and then adds the elements, we'll get our same Uncaught TypeError
.
let app = document.querySelector('#app'); let counter = document.createElement('wc-count'); app.append(counter); // 👆 The constructor() runs here... counter.innerHTML = `<button>Clicked 0 Times</button>`;
Here's another demo showing this error.
A hybrid approach
You can work around this timing issue by moving all of the setup functions to a setup()
method.
In it, I check that any required HTML elements exist before completing the setup, and bail early if they don't. In this case, I make sure this.button
exists.
I also like to set an ._instantiated
property after the Web Component is setup, and check for it before running the setup()
function, to avoid running the method twice.
/** * Setup the Web Component when it's connected to the DOM */ setup () { // Don't run twice if (this._instantiated) return; // Instance properties this.button = this.querySelector('button'); if (!this.button) return; this.count = parseFloat(this.getAttribute('start')) || 0; this.step = parseFloat(this.getAttribute('step')) || 1; this.text = this.getAttribute('text') || 'Clicked Times'; // Listen for click events this.button.addEventListener('click', this); // Announce UI updates this.button.setAttribute('aria-live', 'polite'); // Complete instantiation this._instantiated = true; }
Then, you can run the method inside the constructor()
and connectedCallback()
methods.
/** * The class constructor object */ constructor () { // Always call super first in constructor super(); // Setup the Web Component this.setup(); } /** * Run methods when the element connects to the DOm */ connectedCallback () { // Setup the Web Component this.setup(); }
You can also call the method directly on custom element, if needed.
let app = document.querySelector('#app'); let counter = document.createElement('wc-count'); app.append(counter); counter.innerHTML = `<button>Clicked 0 Times</button>`; counter.setup(); // 👆 The constructor() runs here...
Here's one last demo.
Which approach should you use?
Like all things web development, it depends.
- If I'm writing a Web Component for myself or a client who always uses pre-rendered or server-rendered HTML, I'll setup my Web Component on the
constructor()
. - If the Web Component might be loaded asynchronously or in unpredictable ways, I'll use the hybrid approach.
- If the setup tasks get really long, I'll use a
setup()
method with either approach.
Like this? You can support my work by purchasing an annual membership.
Cheers,
Chris
Want to share this with others or read it later? View it in a browser.
0 Komentar untuk "[Go Make Things] The different ways to instantiate a Web Component"