Introduction
When I start a new project, I always like to try out different styles of creating content. I rarely grab a framework like React or Vue unless I am planning on creating a large-scale application. So when I was creating a website for my Data Structures and Algorithms studies, I wanted to have code blocks of examples next to some text explaining each part. But as I got past arrays, stacks and queues, I started to realise how bloated and repetitive my HTML has become.
If I had continued this way, I would have had about 130 blocks of code to hard-code in, so I looked at different solutions to help generate each block. That is when I came across web components. So today I am going to teach you about web components so that you can add them to your arsenal of web tools.
Web Components
Web Components allow you to create reusable custom elements with their functionality encapsulated away from your code. So basically, you can create templates in the style you want these components to look like and change out information that can be passed in or placed within the generated code.
Starting off, you need a template tag, these tags serve as a system for holding HTML fragments, which can be utilised by JavaScript or generated immediately into the shadow DOM ( We will talk more about this later ). Here is an example of a template tag:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Web Components</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<script src="main.js" async defer type="module"></script>
</head>
<body>
<template>
<h1>hello world</h1>
</template>
</body>
</html>
When you run this, nothing will appear on the website, that is because template tags are not visible as UI, but are stored in your code. You can check using Inspect in the browser. To generate the template, we must create a JavaScript class that extends HTMLElement and use customElements to create a custom tag for our template.
Let's create the class CustomComponents and create a custom element tag:
class CustomComponents extends HTMLElement{
constructor()
{
super();
let template = document.querySelector("template").content;
let templateClone = template.cloneNode(true);
this.append(templateClone);
}
}
customElements.define("custom-tag", CustomComponents);
In this code, we are first creating a class called CustomComponents that extends the class HTMLElement. We then call the constructor, where we first call super() to bring in the HTMLElement class constructors' information. We then set a template variable, then we check the document for the template code and we use the content method to return the content of the template.
After that, we clone the node using the method cloneNode() with the value of true, this clones the whole subtree of the element. Once we have our content, we can append it to the class CustomComponents using the this.append().
Finally, we use the customElements.define() method that allows us to create a custom name element tag that is connected to our CustomComponents class. When naming the custom name, you must add a hyphen(-) in your name so that the browser can tell which tags are custom.
After we have all that set up, you can add your custom tag to your HTML page
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Web Components</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<script src="mains.js" async defer type="module"></script>
</head>
<body>
<template>
<h1>hello world</h1>
</template>
<custom-tag></custom-tag>
</body>
</html>
Now, if we check the page, we will see hello world. Another way of doing this is using the slot tag. These tag grabs the content between your custom tags and display it where the slot tags were.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Web Components</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<script src="mains.js" async defer type="module"></script>
</head>
<body>
<template><slot></slot></template>
<custom-tag><h1>hello world</h1></custom-tag>
</body>
</html>
We can even pass values into our custom tags so that we can update information as the template is generated for each tag.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Web Components</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<script src="mains.js" async defer type="module"></script>
</head>
<body>
<template>
<div>
<h1>name</h1>
<p>age</p>
</div>
</template>
<custom-tag name="Bob" age="67"></custom-tag>
<custom-tag name="Jane" age="55"></custom-tag>
</body>
</html>
class CustomComponents extends HTMLElement{
constructor()
{
super();
let template = document.querySelector("template").content.cloneNode(true);
const personName = this.getAttribute("name");
const personAge = this.getAttribute("age");
template.querySelector("h1").textContent = personName;
template.querySelector("p").textContent = personAge;
this.append(template);
}
}
customElements.define("custom-tag", CustomComponents);
An example of how I used them was to create code blocks like this for my project:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Web Components</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<script src="main.js" async defer type="module"></script>
</head>
<body>
<custom-slot code="createArray" codefile="array.js"></custom-slot>
<custom-slot code="pushArray" codefile="array.push().js"></custom-slot>
</body>
</html>
"use strict"
import { codeStore } from "./codeStore.js";
class CustomComponents extends HTMLElement{
constructor()
{
super();
let template = templateBlock.content.cloneNode(true);
const code = this.getAttribute("code");
const file = this.getAttribute("codefile");
const genCode = codeStore[code];
template.querySelector(".custom-code").innerHTML = genCode;
template.querySelector(".file-name").textContent = file;
template.querySelector(".custom-code").innerHTML = genCode;
this.append(template);
}
}
const templateBlock = document.createElement("template");
templateBlock.id = "custom-template";
templateBlock.innerHTML =
`
<div class="text-code-container">
<div class="ui">
<div class="ui-flex">
<div class="ui-buttons">
<div class="ui-red"></div>
<div class="ui-yellow"></div>
<div class="ui-green"></div>
</div>
<div class="ui-header">
<p class="file-name">untitled.js</p>
</div>
</div>
<div class="code-block">
<pre>
<code class="custom-code">
</code>
</pre>
</div>
</div>
</div>
`;
customElements.define("custom-slot", CustomComponents);
This helped me to generate each code block in a similar format without having to copy and paste each block. I placed my template within my JavaScript file, as I felt I didn't like how it looked on the DOM. I also didn't add the CSS or data files, as it would make it too bloated.
Shadow DOM
When creating custom elements, you don't want JavaScript or CSS changing styles within them, that is why they are encapsulated using the shadow DOM. The shadow DOM enables you to attach a DOM tree to an element and have the internals of this tree hidden from our JavaScript and CSS. So technically, the first way I showed you wasn't the best practice, but it shows how they work.
To add the shadow DOM to your class, you call this.attachShadow and choose the mode to be closed if we don't want the outer JavaScript to manipulate the element or opened to allow it to.
We will add the first line after the cloning of the template and instead of appending to the this, we will append the shadow variable we created
const shadow = this.attachShadow({ mode: 'closed' });
shadow.append(template);
Visually, nothing has changed, but we have added a shadow DOM, and to prove it, we will add some CSS and a basic div with the same element.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Web Components</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<script src="mains.js" async defer type="module"></script>
</head>
<body>
<template>
<div>
<h1>name</h1>
<p>age</p>
</div>
</template>
<div>
<h1>John</h1>
<p>34</p>
</div>
<custom-tag name="Bob" age="67"></custom-tag>
<custom-tag name="Jane" age="55"></custom-tag>
</body>
</html>
*,
*::after,
*::before {
box-sizing: border-box;
border: 0;
padding: 0;
margin: 0;
}
div
{
padding: 1rem;
background-color: rgb(253, 190, 166);
}
h1
{
color: rgb(34, 7, 189);
}
p
{
font-size: 2rem;
}
Results:
!Showing CSS only working on basic div](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m3077793ud4ow3gcixmq.png)
As you can see, the div with the value John has CSS styles, but the two custom elements with the shadow DOM do not. To style the custom element, we can set the styles on the template tag or we can use adopted stylesheets.
The basic way is to just add a style tag to your template tags:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Web Components</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<script src="mains.js" async defer type="module"></script>
</head>
<body>
<template>
<style>
div{padding: 1rem;background-color: rgb(253, 190, 166);}
h1{color: rgb(34, 7, 189);}
p{font-size: 2rem;}
</style>
<div>
<h1>name</h1>
<p>age</p>
</div>
</template>
<div>
<h1>John</h1>
<p>34</p>
</div>
<custom-tag name="Bob" age="67"></custom-tag>
<custom-tag name="Jane" age="55"></custom-tag>
</body>
</html>
The second method is the way I prefer to add a style sheet to my shadow DOM by creating a new CSSStyleSheet(). I replace the content with my CSS file and get the shadow DOM to adopt the new style sheet.
const sheet = new CSSStyleSheet();
async function main()
{
await sheet.replace(await fetch('/style.css').then((r) => r.text()));
class CustomComponents extends HTMLElement{
constructor()
{
super();
let template = document.querySelector("template").content.cloneNode(true);
const shadow = this.attachShadow({ mode: 'closed' });
const personName = this.getAttribute("name");
const personAge = this.getAttribute("age");
template.querySelector("h1").textContent = personName;
template.querySelector("p").textContent = personAge;
shadow.adoptedStyleSheets = [sheet];
shadow.append(template);
}
}
customElements.define("custom-tag", CustomComponents);
}
main()
If you want, you can just write your CSS into the new stylesheet in the JavaScript file.
Web components also have life cycle callbacks that we can use:
connectedCallback()
- When a custom element is added to the DOM, it calls the connectedCallback()
disconnectedCallback()
- When a custom element is removed from the DOM it calls the disconnectedCallback().
adoptedCallback()
- If another DOM tree uses the adoptNode() method the adoptedCallback() is called on the custom element. As our node custom element is moved to a new DOM, it will call this method.
attributeChangedCallback()
- The attributeChangedCallback() is called when attributes are added, removed or changed on a custom element.
Conclusion
If you would like to read more on Web Components here is the MDN docs. Web Components are a quick and handy solution for repetitive UI that needs a few tweaks in projects. I hope you have learnt something new today and I hope you can add these new skills to your web development toolset.
Top comments (0)