Hello thereπ
I was reading an article on scoping CSS in Vue recently and came across this sentence "This is similar to the style encapsulation found in the Shadow DOM". My idea of what the shadow DOM meant was too vague to work with so I did some research and this article was born.
What is the DOM?π§
The Document Object Model, or DOM, is a visual representation of the structure and content of an HTML document. You can think of the DOM as a tree consisting of nodes (e.g. HTML elements, scripts, text, etc.).
Below is a visual representation of a DOM tree:
The DOM is a language-neutral web API, that allows you to create and manipulate or modify HTML documents. The creation of modern, dynamic web pages depends on the ability to manipulate or modify a web document. A programming language like Javascript, lets us access the nodes that make up the DOM, using the methods provided by the DOM.
To test this, open developer tools in your browser (Ctrl + Shift + i
) and run the following in your console: document.getElementsByTagName('body')
This returns an HTML Collection, which is an array that contains an object of properties that describe the body
of that HTML document. One of the properties is the childNodes
property, which is a NodeList, an array that contains a list of all the nodes contained in the body
tag.
One of the primary differences between a Node List and an HTML Collection is that the former is a generic list of nodes and is not specifically tailored to HTML elements so it could return scripts, text nodes, as well as the typical HTML elements.
If we had a body
tag that contains an ordered list of tasks, the DOM tree would look like this:
The HTML document:
<!-- ... -->
<body>
<ol>
<li>Feed the cat</li>
<li>Hit the gym</li>
<li>Be a great software engineer</li>
<li>Eat dinner</li>
</ol>
</body>
When we run the method document.getElementsByTagName('li')
, we get an HTML Collection array, that looks something like this: HTMLCollection(4) [li, li, li, li]
. Further inspection of this array would show you not only these li
tags but also their properties e.g. text content, class, id, etc.
Light DOMπ
The light DOM is the main document structure that you create with regular HTML elements. When you write regular HTML elements and structure the document, you're working within the Light DOM. It represents the hierarchy of elements as they are structured in an HTML document, just like we discussed above.
Shadow DOMπ
The Shadow DOM, however, refers to the encapsulated DOM subtree that is attached to a custom element. Custom Elements allow you to define custom HTML elements, giving us the ability to create tags with specific behaviors. The Shadow DOM and custom elements are integral parts that make up a web component. The Shadow DOM provides isolation and encapsulation for the content of a component, thereby creating a reusable, encapsulated custom element i.e. web component.
When you create a custom element using the Shadow DOM, you can attach a hidden subtree of elements, separate from the main document's DOM (the Light DOM), to it.
The Shadow DOM doesn't only hide/isolate custom elements, it also isolates scoped CSS (styles defined within the Shadow DOM apply only to the elements within that Shadow DOM) and can contain its own markup and JavaScript logic. Elements within the Shadow DOM are not accessible from the outside unless explicitly exposed, allowing for better encapsulation and preventing external styles or scripts from affecting the component's internal structure.
Here is an example of how you can create a web component:
// my-custom-element.js
// Define the class for the custom element
class MyCustomElement extends HTMLElement {
constructor() {
super();
// Create a shadow root for encapsulation
const shadow = this.attachShadow({ mode: 'open' });
// Create an element inside the shadow DOM
const paragraph = document.createElement('p');
paragraph.textContent = 'This is my reusable custom element!';
// Append the paragraph to the shadow root
shadow.appendChild(paragraph);
}
}
// Define the custom element using the custom element registry
customElements.define('my-custom-element', MyCustomElement);
The
<my-custom-element></my-custom-element>
tag is our custom element defined in HTML.JavaScript part:
We define a class
MyCustomElement
that extendsHTMLElement
.In the constructor, we create a Shadow DOM using
attachShadow()
with{ mode: 'open' }
, allowing us to access and modify the Shadow DOM.Inside the Shadow DOM, we create a paragraph element (
<p>
) and set its text content to "This is a custom element!"Finally, we define the custom element using
customElements.define()
, where we specify the tag name ('my-custom-element') and the associated class (MyCustomElement
).
When you add this logic to an HTML file and then open it in a browser, you'll see "This is my reusable custom element!" displayed, which is the content of our custom element created using the Shadow DOM. This web component is reusable and you can also add styles and functionality to it.
To import a web component and use it in another HTML file, Javascript modules can be utilized. Below are the steps:
Create the web component: We did that earlier. That's our
my-custom-element.js
file.Import the web component: In the HTML file where we want to use our web component, we will utilize the
import
statement to import the web component JavaScript file<!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Importing Web Component Example</title> </head> <body> <my-custom-element></my-custom-element> <script type="module"> import './my-custom-element.js'; </script> </body> </html>
In the above HTML file, we've used the
import
statement to import themy-custom-element.js
file that contains the definition of themy-custom-element
web component.Make sure to use
type="module"
in the script tag to indicate that this script should be treated as an ES module. By importing the JavaScript file containing the web component definition using theimport
statement in your HTML file withtype="module"
, you can use the custom element (<my-custom-element></my-custom-element>
) as intended, and the browser will handle the component registration and usage.
Andddd that's it!π We've created a custom web component, that can be reused anywhere and as many times as we want.
Web Components and Microfrontendsπ€
After the explanation above, I imagine you're thinking, "Web components sound very similar to microfrontends". That's true! They are both ways of creating modular, reusable, and consequently, maintainable front-end architectures.
Microfrontends is an architectural pattern where a front-end application is composed of loosely coupled, independently deployable modules or micro-applications, each responsible for specific features or functionalities. Web components can be used within a microfrontend architecture to create modular and reusable UI elements or components that can be shared across the different micro-applications.
I will be writing a more exhaustive article on Microfrontends later on so stay tuned!
Communication Stylesπ£
Now you're thinking, "Well, how do they talk to each other?" (established mind reader hereπ).
One of the ways web components talk to each other is via Custom Events. Typically, when communicating, there's a sender and a receiver. Similarly, in this case, there'll be a sender-component
and a receiver-component
. The sender-component
will dispatch a custom event, and the receiver-component
will listen for that event and respond, e.g. by running a block of Javascript logic that updates the UI, etc.
Here's a basic example:
Let's say we are building a simple e-commerce app that contains a list of products and a cart badge in the nav bar that contains the number of items in the cart. And when we click on the 'add to cart' button of a product card component, the cart badge increases the number of items in the cart.
To accomplish this, we'll create two web components: a product-card
component that includes an "Add to Cart" button, and a cart-badge
component that displays the number of items in the cart. When the "Add to Cart" button is clicked on the product-card
, it will emit a custom event, and the cart-badge
component will listen for this event to update the cart count.
First, we create the product card:
// product-card.js
class ProductCard extends HTMLElement {
constructor() {
super();
// Create a shadow root
this.attachShadow({ mode: 'open' });
// Define the product card content inside the shadow DOM
this.shadowRoot.innerHTML = `
<div class="product">
<h3>Product Name</h3>
<p>Description of the product.</p>
<button class="add-to-cart">Add to Cart</button>
</div>
`;
// Add event listener for 'Add to Cart' button click
this.shadowRoot.querySelector('.add-to-cart').addEventListener('click', () => {
// Dispatch a custom event 'add-to-cart-clicked' to indicate the addition of a product to the cart
this.dispatchEvent(new CustomEvent('add-to-cart-clicked'));
});
}
}
// Define the custom element for the product card
customElements.define('product-card', ProductCard);
You can add styles if you want:
// product-card.js
this.shadowRoot.innerHTML = `
<style>
/* Styles specific to the product card within the Shadow DOM */
h3 {
color: red;
}
/* Other styles specific to the product card */
</style>
<div class="product">
<h3>Product Name</h3>
<p>Description of the product.</p>
<button class="add-to-cart">Add to Cart</button>
</div>
`;
Then, the cart badge that updates item count:
// cart-badge.js
class CartBadge extends HTMLElement {
constructor() {
super();
// Create a shadow root
this.attachShadow({ mode: 'open' });
// Initialize cart count to 0
this.cartCount = 0;
// Render the initial cart count inside the Shadow DOM
this.render();
}
connectedCallback() {
// Listen for the 'add-to-cart-clicked' event dispatched by the product card
document.addEventListener('add-to-cart-clicked', () => {
// Increment cart count when the 'add-to-cart-clicked' event is received
this.cartCount++;
this.render(); // Update the cart badge display
});
}
render() {
// Display the cart count in the badge
this.shadowRoot.innerHTML = `<span class="badge">${this.cartCount}</span>`;
}
}
// Define the custom element for the cart badge
customElements.define('cart-badge', CartBadge);
The
ProductCard
component represents a product card containing a button to add the product to the cart. When the "Add to Cart" button is clicked, it dispatches a custom event namedadd-to-cart-clicked
.The
CartBadge
component displays the number of items in the cart. It listens for theadd-to-cart-clicked
event and increments the cart count when the event is received. The cart count is then rendered in the badge.
You may have noticed that the render()
method is run in the cart-badge
component but not the product-card
component. The reason why this.render()
is used in the cart-badge.js
file but not in the product-card.js
file is due to the differences in the approach used to update the user interface in each component.
In the example provided:
In the
product-card.js
file:- The
ProductCard
component is responsible for dispatching theadd-to-cart-clicked
event when the "Add to Cart" button is clicked. It doesn't need to handle the UI rendering related to the cart count itself. Its main responsibility is to dispatch events when certain actions (such as clicking the "Add to Cart" button) occur.
- The
In the
cart-badge.js
file:The
CartBadge
component uses therender()
method to update the visual representation (the cart count displayed in the badge) whenever the cart count changes. This approach helps keep the rendering logic encapsulated within theCartBadge
component. Whenever there's a change in the cart count, the component re-renders the badge to reflect the updated count.The
constructor()
is called when an instance of the element is created. The Shadow DOM can be initialized here, but it might not be rendered in the document until the element is connected.The
connectedCallback()
method, which is a lifecycle method that is automatically called when the custom element is inserted into the document, listens for theadd-to-cart-clicked
event to update the cart count. At this point, the Shadow DOM is accessible, and operations related to rendering or updating the Shadow DOM can be performed.Please note that the
cart-badge.js
file can add an event listener to theadd-to-cart-clicked
event even though that custom event was not created and is not within its local scope. This is because whenthis.dispatchEvent(new CustomEvent('add-to-cart-clicked'))
is run inproduct-card.js
, the custom-event,'add-to-cart-clicked'
was dispatched to the document level and can be captured and handled at higher levels in the DOM tree by any appropriate event listener, regardless of the Shadow DOM or Light DOM boundaries. It is not confined to the element's own Shadow DOM or Light DOM but can be intercepted by any appropriate event listener attached to parent elements, the document, or even window level using event bubbling or capturing mechanisms.
The separation of concerns in this manner follows the principles of encapsulation and modularity. Each component has its specific responsibility: ProductCard
handles user interactions and event dispatching, while CartBadge
handles displaying the cart count and updating the UI based on changes to the count.
However, depending on the complexity of the application and the requirements, it may be a better choice to centralize the state management or UI updates in a separate module or framework (such as React and one of its state management libraries, Redux) rather than handling it within individual components, allowing for a more structured and centralized approach to state management and UI updates across components.
Finally, we import our web components in our index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Product App with Web Components</title>
</head>
<body>
<nav>
<!-- ... -->
<!-- Instantiate cart-badge component -->
<cart-badge></cart-badge>
</nav>
<!-- Instantiate product-card component -->
<product-card></product-card>
<!-- Import JavaScript files for product-card and cart-badge components -->
<script type="module" src="product-card.js"></script>
<script type="module" src="cart-badge.js"></script>
</body>
</html>
The Shadow DOM Treeπ³
The Shadow DOM establishes encapsulation by creating a scoped subtree of elements within a web component. Visualizing the tree involves understanding how web components, encapsulation, and their associated elements relate to each other within the DOM structure.
Root (Document)
βββ Regular DOM (Light DOM)
βββ Other elements
βββ <product-card> (Web Component)
β βββ #shadow-root (Shadow DOM)
β βββ <style> (Encapsulated Styles)
β βββ <div class="product"> (Encapsulated Content)
β β βββ <h3>Product Name</h3>
β β βββ <p>Description of the product.</p>
β β βββ <button>Add to Cart</button>
β βββ ... (Other encapsulated elements)
βββ Other elements
βββ ...
Last reading for the day!π You're thinking, 'Soooo... a document can have multiple Shadow DOMs?' Yes! Absolutely! Multiple web components can exist on a web page, and each of these components can have its own Shadow DOM, independent of other components and the main document's DOM.
Here's an abstract representation of how that may look like:
Root (Document)
βββ Regular DOM (Light DOM)
β βββ Other elements
β βββ <product-card1> (Web Component 1)
β β βββ #shadow-root (Shadow DOM for Component 1)
β β βββ <style> (Encapsulated Styles for Component 1)
β β βββ <div> (Encapsulated Content for Component 1)
β β βββ ... (Other encapsulated elements for Component 1)
β βββ <product-card2> (Web Component 2)
β β βββ #shadow-root (Shadow DOM for Component 2)
β β βββ <style> (Encapsulated Styles for Component 2)
β β βββ <div> (Encapsulated Content for Component 2)
β β βββ ... (Other encapsulated elements for Component 2)
β βββ ...
βββ ...
Having multiple Shadow DOMs for different web components is a core feature of web components, providing modularity, encapsulation, and reusability across different parts of a web application or webpage.
Conclusion
I hope you have learned something useful today about the DOM. In this article, we have discussed what the DOM is, the Light and Shadow DOM and applications of the shadow DOM with examples.
We have also established that I can read mindsπ
Happy Codingπ