Etéreo

The world doesn’t need another agency. That’s why we came up.

Follow publication

Custom elements under the hood

--

Hi there 🫶 I want to share a recap about the inner workings of custom elements that I have put together now that I have been working with them a little bit more.

Understanding the parsing process

I wondered what happened exactly when the browser sees a custom element. Let’s go through the process:

When the DOM parser encounters a custom element tag like <my-component></my-component>, it goes through several key stages:

1. Tokenization

The HTML parser first tokenizes the input. During this phase:

  • It detects a tag named my-component
  • Begins creating an element corresponding to that tag name
  • Marks this element as a custom element candidate due to the presence of a dash (-) in its name

2. Element Creation

The parser then creates an instance of the element through these steps:

  • If the element has been defined (via customElements.define), it's instantiated as an instance of its custom class
  • The constructor is invoked
  • Tag attributes are processed and attached to the element
  • Any child nodes are parsed and added
  • The fully constructed element is inserted into the DOM tree at the parser’s current position

3. Lifecycle Callback

After construction and DOM insertion:

  • The connectedCallback is triggered
  • This marks the point where the element is fully integrated into the document

Working with Custom Elements

Now, there are some considerations about writing the custom element class that are not so obvious from the beginning.

Here’s a basic example of defining a custom element:

class MyCustomElement extends HTMLElement {
constructor() {
super();
this.innerHTML = "<div>MyCustomElement</div>";
}
}

customElements.define("my-custom-element", MyCustomElement);j

Important Considerations

When working with custom elements, keep in mind:

  • DOM Access: Within the custom element class, this represents the node being inserted, giving you access to the DOM.
  • Constructor Limitations: The constructor runs before the element is attached to the document
    - Layout measurements and parent-child relationships may be unreliable at this stage
    - Properties like offsetWidth or sibling queries may return inaccurate values
  • Best Practices:
    - Initialize visual state in the constructor
    - Leave DOM manipulations for the connectedCallback
    - Consider adding a loading state for heavy processing or async tasks in connectedCallback to avoid visual flicker

Here is a cool example that you can play around with to get a better understanding of the component lifecycle

// Define a custom element that demonstrates the full lifecycle
class LifecycleElement extends HTMLElement {
constructor() {
super();
console.log('1. Constructor: Element is being created');

// Create a shadow root for encapsulation
const shadow = this.attachShadow({ mode: 'open' });

// Initialize but don't measure or query siblings yet
this.initialContent = document.createElement('div');
this.initialContent.textContent = 'Initializing...';
shadow.appendChild(this.initialContent);

// Demonstrate why measuring here is premature
console.log('Width in constructor:', this.offsetWidth); // Will likely be 0
}

connectedCallback() {
console.log('2. Connected: Element is now in the DOM');

// Safe to measure and query the DOM now
console.log('Width in connectedCallback:', this.offsetWidth);

// Demonstrate async behavior
this.updateContent();
}

async updateContent() {
// Simulate an async operation
await new Promise(resolve => setTimeout(resolve, 1000));

const content = document.createElement('div');
content.innerHTML = `
<style>
.container {
padding: 20px;
border: 2px solid #333;
margin: 10px;
}
.info {
color: #666;
font-style: italic;
}
</style>
<div class="container">
<h2>Lifecycle Element</h2>
<p>Element width: ${this.offsetWidth}px</p>
<p class="info">This content was rendered asynchronously</p>
</div>
`
;

this.shadowRoot.replaceChildren(content);
}

disconnectedCallback() {
console.log('3. Disconnected: Element removed from DOM');
}

attributeChangedCallback(name, oldValue, newValue) {
console.log(`4. Attribute changed: ${name} from ${oldValue} to ${newValue}`);
}

static get observedAttributes() {
return ['data-state'];
}
}

// Register the custom element
customElements.define('lifecycle-element', LifecycleElement);

// Usage example
const example = `
// Create and append element
const element = document.createElement('lifecycle-element');
document.body.appendChild(element);

// Later: modify attribute
element.setAttribute('data-state', 'updated');

// Later: remove element
element.remove();
`
;

And that’s all for now, next chapter: Virtual DOM vs Shadow DOM.

Want to know more about us? Come by www.etereo.io ❤

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in Etéreo

The world doesn’t need another agency. That’s why we came up.

Written by RocĂ­o GarcĂ­a Luque

Los ideales son buenos, pero a veces las personas no lo son tanto

No responses yet

Write a response