Web Components!
⏳ 8 min
Componentizing straight from the platform itself.
Let’s dive into the world of Web Components, a powerful and fascinating technology that, despite its potential, often takes a backseat to popular modern frameworks like React, Angular, or Vue. Currently, web development is dominated by these frameworks, each with its own characteristics and philosophies. However, Web Components offer a native way to create reusable components that are framework-agnostic, allowing their use in any web application without the need to rely on external libraries.
This technology consists of three fundamental pillars
: Custom Elements, Shadow DOM, and HTML Templates. Each of these elements plays a crucial role in creating components that are as powerful as they are easy to use.
Let’s take a closer look at each one:
1. Custom Elements
Custom Elements allow you to define your own HTML tags. So, instead of using a simple <div>
, you can have something like <my-component>
.
In our case, we’ll illustrate these concepts by creating a component that represents a “card”. This practical example will help us better understand how Web Components work and how we can leverage them to build reusable and modular user interfaces.
class CardComponent extends HTMLElement {
constructor() {
super();
}
}
customElements.define('card-component', CardComponent);
With this, when you use <card-component></card-component>
in your HTML, the browser will know what to do with it.
2. Shadow DOM
Shadow DOM allows you to encapsulate the HTML, CSS, and JavaScript of the component so that it is not affected by, and does not affect, the rest of the application.
The creation of the Shadow DOM is done within the constructor of the web component.
class CardComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
}
customElements.define('card-component', CardComponent);
Through the attachShadow
method, we create the Shadow DOM and attach it to the component instance.
If we add the open
mode parameter, the Shadow DOM is accessible from the outside through the element’s shadowRoot
property. This means you can access and manipulate the Shadow DOM content using JavaScript. However, if it is closed
, the Shadow DOM is not accessible from the outside. You cannot access the shadowRoot
property from outside the component.
3. HTML Templates
These are HTML code blocks that you define once and can reuse whenever you want.
There are two main ways to define our content: using HTML Templates
or using Template Literals
. Let’s explore both approaches.
HTML Templates are defined in your HTML but are not immediately rendered. They are activated only when needed. Here’s a simple example:
<template id="card-template">
<style>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
max-width: 300px;
box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.1);
}
</style>
<div class="card">
<img>
<h1></h1>
<p></p>
<a></a>
</div>
</template>
Template Literals are another way to define your content, but you do it directly in your JavaScript. They are very useful when you need something more dynamic or when you want to keep all your code in one place. Here’s an example:
const templateLiteral = `
<style>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
max-width: 300px;
box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.1);
}
</style>
<div class="card">
<img src="" alt="Image">
<h1>Título</h1>
<p>Descripción</p>
<a href="#">Enlace</a>
</div>
`;
shadowRoot.innerHTML = templateLiteral;
What’s the Difference?
HTML Templates:
- Defined once in your HTML.
- They are not rendered until cloned and inserted into the DOM.
- Ideal for more static content or when you prefer to separate your HTML and JavaScript.
Template Literals:
- Defined directly in your JavaScript.
- Inserted into the DOM when assigned.
- Perfect for dynamic content or when you want everything in a single file.
Full Example of Creating a Web Component
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Component Card</title>
</head>
<body>
<template id="card-template">
<style>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
max-width: 300px;
box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.1);
}
.card img {
max-width: 100%;
border-radius: 8px;
}
.card h1 {
font-size: 1.5em;
margin: 0.5em 0;
}
.card p {
font-size: 1em;
color: #666;
}
.card a {
display: inline-block;
margin-top: 1em;
padding: 0.5em 1em;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
}
.card a:hover {
background-color: #0056b3;
}
</style>
<div class="card">
<img src="https://via.placeholder.com/300">
<h1>Title</h1>
<p>Description</p>
<a>Link</a>
</div>
</template>
<script>
class CardComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const template = document.getElementById('card-template').content;
this.shadowRoot.appendChild(template.cloneNode(true));
}
}
customElements.define('card-component', CardComponent);
</script>
<card-component></card-component>
</body>
</html>
Result:
At this point, we have covered the basics and learned how to create static components that encapsulate their structure and styles, providing us with a powerful way to build reusable and modular elements directly from the platform.
Furthermore, you can now see how Web Components can enhance your projects. From defining your own HTML tags to encapsulating styles and behaviors, this technology offers you a flexibility and control that modern frameworks often hide behind layers of abstraction.
Would you like to discover how to make your components dynamic and responsive to changes in their environment? Do you want to know exactly when they are created, updated, and destroyed, and how you can leverage these moments to write more efficient and robust code? This is where the lifecycle methods of Web Components
come into play.
Lifecycle of Web Components
Web Components have a specific lifecycle that includes several callbacks you can use to execute code at different stages of the component’s life. The four main callbacks are:
-
Constructor: This method is called when an instance of the component is created. It’s ideal for initial setup, such as attaching a Shadow DOM and configuring initial properties.
-
connectedCallback: This callback is invoked when the component is added to the DOM. Here you can perform tasks such as fetching data from an API, adding event listeners, etc.
-
disconnectedCallback: This is executed when the component is removed from the DOM. It is useful for removing event listeners, for example.
-
attributeChangedCallback: This is triggered when one of the component’s observed attributes changes. To use it, you need to define the static method
observedAttributes
that returns a list of attributes to observe.
Let’s look at a complete example, where we will add dynamism to our <card-component>
using lifecycle methods and also add our own methods to the component to separate its logic.
class CardComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
static get observedAttributes() {
return ['title', 'image', 'description', 'link-text', 'link-href', 'event-name'];
}
attributeChangedCallback(name, oldValue, newValue) {
this.render();
}
connectedCallback() {
this.render();
this.addEventListeners();
}
disconnectedCallback() {
this.removeEventListeners();
}
styles() {
return `
<style>
:host {
display: block;
max-width: 300px;
margin: 10px;
}
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
box-shadow: 2px 2px 12px rgba(0, 0, 0, 0.1);
}
.card img {
max-width: 100%;
border-radius: 8px;
}
.card h1 {
font-size: 1.5em;
margin: 0.5em 0;
}
.card p {
font-size: 1em;
color: #666;
}
.card a {
display: inline-block;
margin-top: 1em;
padding: 0.5em 1em;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
}
.card a:hover {
background-color: #0056b3;
}
</style>
`;
}
render() {
this.shadowRoot.innerHTML = `
${this.styles()}
<div class="card">
<img src="${this.getAttribute('image')}" alt="${this.getAttribute('title') || 'Image'}">
<h1>${this.getAttribute('title')}</h1>
<p>${this.getAttribute('description')}</p>
<a href="${this.getAttribute('link-href')}">${this.getAttribute('link-text') || 'Enlace'}</a>
</div>
`;
}
addEventListeners() {
const link = this.shadowRoot.querySelector('a');
const eventName = this.getAttribute('event-name') || 'card-link-click';
link.addEventListener('click', this.linkClickHandler = (e) => {
e.preventDefault();
this.dispatchEvent(new CustomEvent(eventName, {
detail: {
message: 'Link clicked!',
href: link.getAttribute('href')
},
bubbles: true,
composed: true
}));
});
}
removeEventListeners() {
const link = this.shadowRoot.querySelector('a');
link.removeEventListener('click', this.linkClickHandler);
}
}
customElements.define('card-component', CardComponent);
<card-component
title="My Card Title"
image="https://via.placeholder.com/300"
description="This is the card title description"
link-text="More info..."
link-href="https://example.com"
event-name="custom-link-click">
</card-component>
<script src="index.js"></script>
<script>
document.querySelector('card-component').addEventListener('custom-link-click', (e) => {
console.log(e.detail.message);
console.log(e.detail.href);
});
</script>
Summary of our component:
- constructor: Called when an instance of the component is created or updated. Here, we initialize the Shadow DOM.
- static get observedAttributes(): Specifies which attributes to observe. When any of these attributes change,
attributeChangedCallback
is called. - attributeChangedCallback(name, oldValue, newValue): Called when one of the observed attributes is changed. Here, we simply re-render the component.
- connectedCallback(): Called when the component is added to the DOM. Here, we render the content and add event listeners.
- disconnectedCallback(): Called when the component is removed from the DOM. Here, we remove the event listeners.
Other methods we have added, but which are not part of the component’s lifecycle:
- styles(): Returns the CSS styles for the component.
- render(): Generates and adds the component’s HTML content to the Shadow DOM.
- addEventListeners(): Adds event listeners to the component, such as a click on the link.
- removeEventListeners: Removes the event listeners from the component when it is removed from the DOM.
Farewell 👋
And so we come to the end of our exciting journey through the world of Web Components. We have learned how to create custom components, encapsulate styles, manage the lifecycle, and make our components dynamic. From creating a simple card with a title, image, description, and link, to understanding how and when these components are created, updated, and destroyed.
Web Components may seem like a technology hidden under the shadow of major frameworks like React or Angular, but they offer a flexibility and control that is worth exploring. So, if you ever feel curious or need a lightweight and modular solution for your projects, don’t hesitate to give Web Components a try.