Web Components Custom Elements Lifecycle is What Makes Them Useful

Feedback from my previous post on Web Components was thatthe lifecycle callback methods like connectedCallback are actually what makes custom elements useful. After exploring thismore, I can see why and want to demonstrate.

The examples in my previous post demonstrated progressive enhancement for a server-rendered page. I anchored to this as thatwas the crux of Jim Neilsen���s blog post, and a lot of discussion around Web Components is how they can support progressiveenhancement. In that scenario, the callbacks on a custom element don���t seem useful.

But, as was pointed out to me a few times on Mastodon and email, these callbacks vastly simplify managing dynamic insertion of custom elements. If you want to insert (or remove) a component, the callbacks provided by the custom elements API trigger automatically.

If you are just using the DOM APIs���my so-called ���vanilla��� implementation���you have to do a lot of heavy lifting yourself.Let���s see all this in action.

Manual Lifecycle Management

Let���s enhnace the user avatar example and create a button that, when clicked, inserts a new user avatar component into theDOM, without re-rendering the page or performing any server-side interaction.

data-tooltip> [image error] src="https://naildrivin5.com/images/DavidC..." alt="Profile photo of Dave Copeland" width="64" height="64" title="Dave Copeland" /> data-add-new>Add New ComponentNew Components are added here

To focus on the behavior, I���m not going to extract the markup into a template���there will be some duplication but set thataside for a moment. Here is the JavaScript for thebutton press:

document.querySelectorAll("[data-add-new]").forEach( (e) => { e.addEventListener("click", (event) => { event.preventDefault() const section = document.querySelector("section") section.insertAdjacentHTML("beforebegin",` Old Profile photo of Dave Copeland`) })})

If you run this (see CodePen version), you���ll notice that whilethe [image error] tag is inserted, the tooltip is not added. This is because there is nothing to trigger the code that does theenhancement. That code already ran.

The Complexity of Doing it Yourself

To make this work, we need to create our own abstraction so that we can create a new component that we then enhance. Thereare a ton of ways to do this, but here is one that introduces the fewest new concepts.

First, we create a class that wraps the element and exposes an enhance method that does the progressive enhancement:

class UserAvatar { constructor(element) { this.element = element const $img = element.querySelector("img") this.src = $img.getAttribute("src"); this.name = $img.getAttribute("title"); } enhance() { this.element.insertAdjacentHTML( 'beforeend', `tooltip ${this.name}` ); }}

Now, our initialization code will create an instance of this class and call enhance:

document.querySelectorAll("[data-tooltip]:has(img[src][title])"). forEach( (element) => { const userAvatar = new UserAvatar(element) userAvatar.enhance() })

Here���s where it gets really nasty. Because we ultimately need an Element, we have to create one using DOM methods and notstrings:

document.querySelectorAll("[data-add-new]").forEach( (e) => { e.addEventListener("click", (event) => { event.preventDefault() const section = document.querySelector("section") const element = document.createElement("div") const img = document.createElement("img") img.setAttribute("src","https://naildrivin5.com/images/DavidC...") img.setAttribute("alt","Old Profile photo of Dave Copeland") img.setAttribute("width","64") img.setAttribute("title","Younger Dave Copeland") element.appendChild(img) const userAvatar = new UserAvatar(element) section.appendChild(userAvatar.element) userAvatar.enhance() })})

Yech. You can see this working on CodePen. Sure enough, whenthe dynamic component is added, the enhancement runs. There are a lot of ways to make this better, but that���s not the pointof this post.

Let���s see Jim Neilsen���s custom element do this.

Automatic Lifecycle Management

The additional code to handle the button is similar to what I used in my vanilla version (again, bear with me on the markup duplication���that can be eliminated and we���ll discuss how in a future post):

document.querySelectorAll("[data-add-new]").forEach((e) => { e.addEventListener("click", (event) => { event.preventDefault(); const section = document.querySelector("section"); section.insertAdjacentHTML( "beforebegin", ` Old Profile photo of Dave Copeland`); });});

If you run this on CodePen, it���just works. The reasonis connectedCallback(). This is documented as running ���when the element is added to the document���,and the words add and document mean something specific. It means when the element is dynamically put into the Documentbeing shown, connectedCallback() is called.

This is a significant savings. We could create helper functions or classes that allow our vanilla JS version to worklike this, but that would be some made-up, non-standard thing.

Revising the Four Steps to be Five

In my previous post, I outlined four steps that any JavaScript has to handle:

Locate the elements of the document that will need to change. Identify the events you want to respond to. Ensure all input and configuration needed to control behavior is available. Wire up the events to the document based on the inputs and configuration.

It seems that there should be a new step:

Ensure proper initialization and deinitialization when the document is dynamically manipulated.

With this fifth step, there is now a clear difference to use a custom element:

Step Web Components Vanilla 1 - Locate querySelector + defensive if statements querySelectorAll with specific selectors + defensive if statements 2 - Events N/A, but presumably querySelector, defensive if statements, and addEventListener N/A, but presumably querySelector, defensive if statements, and addEventListener 3 - Configuration getAttribute + defensive if statements getAttribute + defensive if statements 4 - Integration Code inside connectedCallback Code inside forEach 5 - Initialize/De-initialize Code inside connectedCallback or disconnectedCallback. A lot of non-standard code you have to write and manage. Locate Web Components - querySelector + defensive if statements Vanilla - querySelectorAll with specific selectors + defensive if statements Events Web Components - N/A, but presumably querySelector, defensive if statements, and addEventListener Vanilla - N/A, but presumably querySelector, defensive if statements, and addEventListener Configuration Web Components - getAttribute + defensive if statements Vanilla - getAttribute + defensive if statements Integration Web Components - Code inside connectedCallback Vanilla - Code inside forEach Initialize/De-initialize Web Components - Code inside connectedCallback or disconnectedCallback. Vanilla - A lot of non-standard code you have to write and manage.

What this tells me is that there is no real downside to Web Components, but some upside for situations when you will beadding or removing components dynamically. If you are using Hotwire (part of Rails), it works by sending server-renderedmarkup to the browser for dynamic insertion. This is a key benefit to that strategy.

Notably, React also provides a solution for this problem as its beuilt into the lifecycle of a component.

Why Wasn���t This Clear?

I think there are three reasons this isn���t clear:

I was anchored on a progressive enhancement scenario where there wasn���t any addition of custom elements. The documentation around all this is���pretty bad. It���s pretty academic1 in that it presents information without contedxt, and doesn���t outline any real benefit for using it or reason it exists. The other aspects of Web Components���Shadow DOM, templates, slots���don���t seem optimized for the decades-old use case ofmanaging re-usable markup. We���ll dig into that in a future post.

I think the real approach is to not judge Web Components on a problem I think they should solve, but against the problem they were designed to solve. And that problem is a very tiny subset of the problems facing web developers. It���s almost too small to notice.

1I don't know about you, but almost every single class in college���undergrad and grad���boiled down to ���presentinginformation to memorize but without any context for why it was useful to know���. Certainly high school was like this. Idon't understand why educational systems are so afraid of practical appilcations or contextualizing information. ���
 •  0 comments  •  flag
Share on Twitter
Published on November 19, 2023 01:00
No comments have been added yet.