Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Getting Started with Web Components

You're reading from   Getting Started with Web Components Build modular and reusable components using HTML, CSS and JavaScript

Arrow left icon
Product type Paperback
Published in Aug 2019
Publisher Packt
ISBN-13 9781838649234
Length 158 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Prateek Jadhwani Prateek Jadhwani
Author Profile Icon Prateek Jadhwani
Prateek Jadhwani
Arrow right icon
View More author details
Toc

Web Component specifications

Just like any technology, Web Components also have a set of specifications that need to be followed in order to achieve the functionality associated with them.

A Web Component specification has the following parts:

  • Custom element: The ability to create custom HTML tags and make sure that the browser understands how to use this HTML tag
  • Shadow DOM: The ability to encapsulate the contents of the component from other parts of the DOM
  • Template: Being able to create a reusable DOM structure that can be modified on the fly and output desired results

These three specifications, when used together, provide a way to create a custom HTML tag that can output desired results (DOM structure) and let it encapsulate from the page, making it reusable again and again.

Now that we know these specifications and what they do, let's dive into them individually and try to look at their JavaScript APIs.

Custom elements

A custom element specification lets you create a custom HTML tag that can be used as its own HTML tag on the page. In order to achieve this, we need to first write a class with the functionalities associated with that HTML element, and then we need to register it so that the browser understands what this HTML tag is, and how it can be used on the page. 

If you are someone who is new to the concept of classes in JavaScript, here is how you can create a class:

class myClass {
constructor() {
// do stuff
}
}

Pretty simple, right? Let's use the same class structure to create our custom element class, say HelloWorld:

class HelloWorld extends HTMLElement {
constructor() {
// very important
// needed in every constructor
// which extends another class
super();

// do magic here
this.innerText = 'Hello World';
}
}

In the preceding code, our custom element class is called HelloWorld and it is extending interface from the HTMLElement class, which represents how an HTML element should work on a page. So, HelloWorld now knows what click events are, what CSS is, and so on, simply by extending HTMLElement.

Inside this class, we have the constructor() method, which gets called as soon as a new instance of this class is created. The super() function needs to be called in order to correctly instantiate the properties of the extended class.

The preceding code simply creates the element class definition. We still need to register this element. We can do so by writing the following code:

customElements.define('hello-world', HelloWorld);

What it does is register the class HelloWorld by defining it using the define() interface in the customElements interface; hello-world is the name of the custom element that is going to be available on the page.

Once this is defined, you can used the custom element by simply writing the HTML tag as following:

<hello-world><hello-world>

When this code is run on a browser, it will render the text, Hello World.

Types of custom elements

Now that you have understood how we can register custom elements, it is time we looked deeper into the type of custom elements. Based on the type of requirement, we can create two types of custom elements: 

  • Autonomous custom element: Any element that can be used by itself, without depending on another HTML element can be considered an autonomous custom element. In technical terms, Any custom  hat extends HTMLElement is an autonomous custom element.

Let's have another example of an autonomous custom element. Let's create a SmileyEmoji element that shows a smiley emoji. Here is what it looks like:

class SmileyEmoji extends HTMLElement {
constructor() {
super();
// let's set the inner text of
// this element to a smiley
this.innerText = '';
}
}

customElements.define('smiley-emoji', SmileyEmoji);

This registers the smiley-emoji custom element, which can be used as follows:

<smiley-emoji></smiley-emoji>
  • Customized built-in element: This type of custom element can extend the functionality of an already existing HTML tag. Let's create a custom element that extends HTMLSpanElement instead of HTMLElement. And its functionality is, say, it needs to add a smiley emoji at the end of the custom element:
class AddSmiley extends HTMLSpanElement {
constructor() {
super();

// lets append a smiley
// to the inner text
this.innerText += '';
}
}
customElements.define('add-smiley', AddSmiley, { extends: 'span' });

Now, if you have the following HTML, this will add the smiley to the end of the text Hello World:

<span is="add-smiley">Hello World</span>

Try running the code for autonomous custom elements and customized built-in elements on a browser, or CodePen, or JSFiddle. The class and registration code will be in the JavaScript block and the rest will be in the HTML block.

Notice the difference in registration code for <smiley-emoji> and <add-smiley> custom elements. The second one uses an extra parameter that specifies what it is extending.

You can check whether a custom element is already defined or not with the help of the following code:

customElements.get('smiley-emoji');

It will either return undefined if it has not been registered, or will return the class definition, if it has been registered. This is a very helpful statement in large projects because registering an already registered custom element will break the code.

Shadow DOM

This is the second specification for Web Components and this one is responsible for encapsulation. Both the CSS and DOM can be encapsulated so that they are hidden from the rest of the page. What a shadow DOM does is let you create a new root node, called shadow root, that is hidden from the normal DOM of the page.

However, even before we jump into the concept of a shadow DOM, let's try to look at what a normal DOM looks like. Any page with a DOM follows a tree structure. Here I have the DOM structure of a very simple page:

In the preceding image, you can see that #document is the root node for this page. 

You can find out the root node of a page by typing document.querySelector('html').getRootNode() in the browser console.

If you try to get the child nodes of an HTML tag using document.querySelector('html').childNodes in the browser console, then you can see the following screenshot:

This shows that the DOM follows a tree structure. You can go deeper into these nodes by clicking on the arrow right next to the name of the node. And just like how I have shown in the screenshot, anyone can go into a particular node by expanding it and change these values. In order to achieve this encapsulation, the concept of a shadow DOM was invented.

What a shadow DOM does is let you create a new root node, called shadow root, that is hidden from the normal DOM of the page. This shadow root can have any HTML inside and can work as any normal HTML DOM structure with events and CSS. But this shadow root can only be accessed by a shadow host attached to the DOM. 

For example, let's say that instead of having text inside the <p> tag in the preceding example, we have a shadow host that is attached to a shadow root. This is what the page source would look like:

Furthermore, if you tried to get the child nodes of this <p> tag, you would see something like this:

Notice that there is a <span> tag in the shadow root. Even though this <span> tag is present inside the <p> tag, the shadow root does not let JavaScript APIs touch it. This is how the shadow DOM encapsulates code inside itself.

Now that we know what a shadow DOM does, let's jump on to some code and learn how to create our own shadow DOMs.

Let's say we have a DOM with a class name entry. This is what it looks like:

<div class="entry"></div>

In order to create a shadow DOM in this div, we will first need to get the reference to this .entry div, then we need to mark it as a shadow root, and then append the content to this shadow root. So, here is the JavaScript code for creating a shadowRoot inside a div and adding contents to it:

// get the reference to the div
let shadowRootEl = document.querySelector('.entry');

// mark it as a shadow root
let shadowRoot = shadowRootEl.attachShadow({mode: 'open'});

// create a random span tag
// that can be appended to the shadow root
let childEl = document.createElement('span');
childEl.innerText = "Hello shadow DOM";

// append the span tag to shadow root
shadowRoot.appendChild(childEl);

Pretty simple, right? Remember, we are still discussing the shadow DOM spec. We haven't started implementing it inside a custom element yet. Let's recall the definition of our hello-world custom element. This is what it looked like:

class HelloWorld extends HTMLElement {
constructor() {
super();

// do magic here
this.innerText = 'Hello World';
}
}

customElements.define('hello-world', HelloWorld);

Notice that the text Hello World is currently being added to normal DOM. We can use the same shadow DOM concepts discussed earlier in this custom element.

First, we need to get the reference to the node where we want to attach the shadow root. In this case, let's make the custom element itself the shadow host by using the following code:

let shadowRoot = this.attachShadow({mode: 'open'});

Now, we can either add a text node or create a new element and append it to this shadowRoot:

// add a text node
shadowRoot.append('Hello World');

Templates

Till now, we have only created custom elements and shadow DOMs that require only one or, at the most, two lines of HTML code. If we move on to a real-life example, HTML code can be more than two lines. It can start from a few lines of nested div to images and paragraphs—you get the picture. The template specification provides a way to hold HTML on the browser without actually rendering it on the page. Let us look at a small example of a template:

<template id="my-template">
<div class="red-border">
<p>Hello Templates</p>
<p>This is a small template</p>
</div>
</template>

You can write a template inside <template> tags and assign it an identifier, just as I have done with the help of an id. You can put it anywhere on the page; it does not matter. We can get its content with the help of JavaScript APIs and then clone it and put it inside any DOM, just as I have shown in the following:

// Get the reference to the template
let templateReference = document.querySelector('#my-template');

// Get the content node
let templateContent = templateReference.content;

// clone the template content
// and append it to the target div
document.querySelector('#target')
.appendChild(templateContent.cloneNode(true));

Similarly, we can have any number of templates on the page, which can be used by any JavaScript code.

Let's now use the same template with a shadow DOM. We will keep the template as it is. The changes in the JavaScript code would be something like this:

// Get the reference to the template
let templateReference = document.querySelector('#my-template');

// Get the content node
let templateContent = templateReference.content;

// Get the reference to target DOM
let targetDOM = document.querySelector('#target');

// add a shadow root to the target reference DOM
let targetShadowRoot = targetDOM.attachShadow({mode: 'open'});

// clone the template content
// and append it to the target div
targetShadowRoot.appendChild(templateContent.cloneNode(true));

We are doing the same thing that we did in the previous example, but, instead of appending the code directly to the target div, we are first attaching a shadow root to the target div, and then appending the cloned template content.

We should be able to use the exact same concept inside the autonomous custom element that uses a shadow DOM. Let's give it a try.

Let's edit the id of the template and call it hello-world-template:

<template id="hello-world-template">
<div>
<p>Hello Templates</p>
<p>This is a small template</p>
</div>
</template>

We will follow the exact same approach that we followed in the preceding example. We will get the template content from the template reference, clone it, and append it in the custom element, making the code of the custom element look like the following:

class HelloWorld extends HTMLElement {
constructor() {
super();

// Get the reference to the template
let templateReference = document.querySelector('#hello-world-template');

// Get the content node
let templateContent = templateReference.content;

let shadowRoot = this.attachShadow({mode: 'open'});

// add a text node
shadowRoot.append(templateContent.cloneNode(true));
}
}

customElements.define('hello-world', HelloWorld);

Now we can simply call the HTML tag inside our page using the following code:

<hello-world></hello-world>

If we inspect the DOM structure inside developer tools, this is what we see:

Module loader API

Module loader API is not a part of Web Component spec sheet, but it is definitely something that is useful to know when it comes to creating and using multiple classes. As the name says, this specification lets a user load the modules. That is, if you have a bunch of classes, you can use module loaders to load these classes into the web page.

If your build process involves using WebPack or Gulp or anything else that lets you import modules directly or indirectly, please feel free to skip this section.

Let's start with the basics. Let's say we have our index.html like this:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
</head>
<body>
<p>Placeholder for Random Number</p>
</body>
</html>

We can see that there is a <p> tag in this HTML file. Now, let's say we have a class called AddNumber, whose purpose is to add a random number between 0 and 1 to this <p> tag. This would make the code look something like this:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
</head>
<body>
<p>Placeholder for Random Number</p>

<script type="text/javascript">
class AddNumber {
constructor() {
document.querySelector('p').innerText = Math.random();
}
}

new AddNumber();
</script>

</body>
</html>

Simple, right? If you open the page on a browser, you will simply see a random number, and if you inspect the page, you will see that the random number replaced the text which was inside the <p> tag.

If we choose to store it in a JavaScript file, we can try to import it using the following code, where addNumber.js is the name of the file:

<script type="text/javascript" src="./addNumber.js"></script>

Now, let's say you have a randomNumberGenerator function instead of the Math.random() method. The code would look something like this:

class AddNumber {
constructor() {
// let's set the inner text of
// this element to a smiley
document.querySelector('p').innerText = randomNumberGenerator();
}
}
function randomNumberGenerator() {
return Math.random();
}
new AddNumber();

We also want the ability to let the user create a new object of the AddNumber class, rather than us creating it in the file. We do not want the user to know how randomNumberGenerator works, so we want the user to be only able to create the object of AddNumber. This way, we reach how modules work. We, the creators of modules, decide which functionalities the user can use and which they cannot.

We can choose what the user can use with the help of the export keyword. This would make the code look something like this:

//addNumber.js

export default class AddNumber {
constructor() {
document.querySelector('p').innerText = randomNumberGenerator();
}
}

function randomNumberGenerator() {
return Math.random();
}

When this file is imported (note that we haven't talked about imports yet), the user will only be able to use the AddNumber class. The randomNumberGenerator function won't be available to the user.

Similarly, if you have another file with, say, two other functions, add() and subtract(), you can export both of them as shown in the following:

// calc.js

export function add(x, y) {
return x + y;
}

export function subtract(x, y) {
return x - y;
}

Importing a module can be easily done with the help of the import keyword. In this section, we will talk about the type="module" attribute.

Inside the HTML file, index.html, instead of type=text/javascript, we can use type=module to tell the browser that the file that we are importing is a module. This is what it will look like when we are trying to import the file addNumber.js:

<script type="module" >
import AddNumberWithNewName from './addNumber.js';
new AddNumberWithNewName();
</script>

This is how it will look if we import functions from the calc.js module:

<script type="module" >
import {add, subtract} from './calc.js';
console.log(add(1,5));
</script>

Notice how we can change the name of the module exported from AddNumber, which uses export default, and how we have to use the same name as the name of the function exported using export.

Named export versus default export

In the previous examples, that is, addNumber.js and calc.js, we saw that there are two ways to export something: export and export default. The simplest way to understand it is as follows: when a file exports multiple things with different names and when these names cannot be changed after import, it is a named export, whereas, when we export only one thing from a module file and this name can be changed to anything after the import, it is a default export. 

Custom elements using imports

Let's say we need to create a Web Component that does a very simple task of showing a heading and a paragraph inside it, and the name of the custom element should be <revamped-paragraph>. This is what the definition of this Web Component would look like:

//revampedParagraph.js

export default class RevampedParagraph extends HTMLElement {
constructor() {
super();

// template ref and content
let templateReference = document.querySelector('#revamped-paragraph-template');
let template = templateReference.content;

// adding html from template
this.append(template.cloneNode(true));
}
}

Our index.html file, the file that imports this module, would look like this:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<title>Revamped Paragraph</title>

<!--
Notice how we use type="module"
-->
<script type="module">

// imports object from the module
// and names it as RevampedParagraph
// You can name it anything you want
// since it is a default export
import RevampedParagraph from './revampedParagraph.js';

// We are now defining the custom element
customElements.define('revamped-paragraph', RevampedParagraph);
</script>

</head>
<body>

<revamped-paragraph></revamped-paragraph>

<!--
Template for
Revamped Paragraph
-->
<template id="revamped-paragraph-template">
<h1>Revamped Paragraph</h1>
<p>This is the default paragraph inside
the revamped-paragraph element</p>
</template>

</body>
</html>

Notice how the template is a part of our HTML, and how it gets used when the module is imported. We will be learning about all the steps that take place from the actual registration of the Web Components to what happens when they are removed from the page in the next chapter, where we will learn about life cycle methods. But for now, we need to look at more examples to understand how to create Web Components.

Let's take a look at another example. In this example, we need to import multiple Web Components in the index.html file. The components are as follows:

  • A student attendance table component: A table that shows the index number, student name, and attendance in a checkbox. This data is obtained from a student.json file.
  • An information banner component: The purpose of this component is to show a phone number and an address for the school where these students are studying.
  • A time slot component: A component that lets the user select a time slot for the class between three sets of timings.

Let us start with the first one, the <student-attendance-table> component. We need to first identify what it needs. In my opinion, these are the things it needs:

  • A fetch call to the student.json file
  • A template for each row of the string. I will be using template strings here
  • A default text that says loading... when it is making the call and another text that says unable to retrieve student list. when the fetch call fails

This is what our student.json file looks like:

[
{
"name": "Robert De Niro",
"lastScore": 75
},
{
"name": "Jack Nicholson",
"lastScore": 87
},
{
"name": "Marlon Brando",
"lastScore": 81
},
{
"name": "Tom Hanks",
"lastScore": 62
},
{
"name": "Leonardo DiCaprio",
"lastScore": 92
}
]

This is what the definition of the Web Component looks like:

// StudentAttendanceTable.js

export default class StudentAttendanceTable extends HTMLElement {
constructor() {
super();

// we simply called another method
// inside the class
this.render();
}

render() {
// let put our loading text first
this.innerText = this.getLoadingText();

// let's start our fetch call
this.getStudentList();
}

getStudentList() {
// lets use fetch api
// https://developer.mozilla.org/en-US/docs/Web
// /API/Fetch_API/Using_Fetch
fetch('./student.json')
.then(response => {

// converts response to json
return response.json();

})
.then(jsonData => {
this.generateTable(jsonData);
})
.catch(e => {

// lets set the error message for
// the user
this.innerText = this.getErrorText();

// lets print out the error
// message for the devs
console.log(e);
});

}

generateTable(names) {
// lets loop through names
// with the help of map
let rows = names.map((data, index) => {
return this.getTableRow(index, data.name);
});

// creating the table
let table = document.createElement('table');
table.innerHTML = rows.join('');

// setting the table as html for this component
this.appendHTMLToShadowDOM(table);
}

getTableRow(index, name) {
let tableRow = `<tr>
<td>${index + 1}</td>
<td>${name}</td>
<td>
<input type="checkbox" name="${index}-attendance"/>
</td>
</tr>`;

return tableRow;
}

appendHTMLToShadowDOM(html) {
// clearing out old html
this.innerHTML = '';

let shadowRoot = this.attachShadow({mode: 'open'});

// add a text node
shadowRoot.append(html);
}

getLoadingText() {
return `loading..`;
}

getErrorText() {
return `unable to retrieve student list.`;
}
}

Notice the functions getLoadingText() and getErrorText(). Their purpose is simply to return a text. Then the render() method first consumes the getLoadingText() method, and then makes the call using getStudentList() to fetch the student list from student.json file.

Once this student list is fetched, it gets passed onto generateTable() method, where every name and its index is passed into the getTableRow() method to generate rows and then gets returned back to be a part of the table. Once the table is formed, it is then passed into the appendHTMLToShadowDOM() method to be added to the shadow DOM for the component.

It's time to look into the <information-banner> component. Since this component simply needs to display a phone number and an address of the school where they are studying, we can use <template> and make it work. This is what it looks like:

//InformationBanner.js

export default class InformationBanner extends HTMLElement {
constructor() {
super();

// we simply called another method
// inside the class
this.render();
}

render() {

// Get the reference to the template
let templateReference = document.querySelector('#information-banner-template');

// Get the content node
let templateContent = templateReference.content;

let shadowRoot = this.attachShadow({mode: 'open'});

// add the template text to the shadow root
shadowRoot.append(templateContent.cloneNode(true));
}
}

Furthermore, information-banner-template looks something like this:

<template id="information-banner-template">
<div>
<a href="tel:1234567890">Call: 1234567890</a>
<div>
<p>Just Some Random Street</p>
<p>Town</p>
<p>State - 123456</p>
</div>
</div>
</template>

As you can see, it is not much different than the custom elements we have already talked about in previous sections.

Let's move on to the last custom element, the <time-slot> component. Since it also involves a preset number of time slots, we can use a <template> tag to do our work. 

The template would look something like this:

<template id="time-slot-template">
<div>
<div>
<input type="radio" name="timeslot" value="slot1" checked> 9:00
AM - 11:00 AM
</div>
<div>
<input type="radio" name="timeslot" value="slot2"> 11:00 AM -
1:00 PM
</div>
<div>
<input type="radio" name="timeslot" value="slot3"> 1:00 PM - 3:00
PM
</div>
</div>
</template>

The definition of the <time-slot> component would look like this:

// TimeSlot.js

export default class TimeSlot extends HTMLElement {
constructor() {
super();

// we simply called another method
// inside the class
this.render();
}

render() {

// Get the reference to the template
let templateReference = document.querySelector('#time-slot-
template');

// Get the content node
let templateContent = templateReference.content;

let shadowRoot = this.attachShadow({mode: 'open'});

// add the template text to the shadow root
shadowRoot.append(templateContent.cloneNode(true));
}
}

It is the same as the previous component.

Now that we have written the Web Components, it's time to take a look at the index.html file that includes all of these components together. This is what it looks like:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<title>Student Page</title>

<!--
Notice how we use type="module"
-->
<script type="module">

// importing the first custom element
import StudentAttendanceTable from './StudentAttendanceTable.js';

// importing the second custom element
import InformationBanner from './InformationBanner.js';

// importing the third custom element
import TimeSlot from './TimeSlot.js';

customElements.define('student-attendance-table',
StudentAttendanceTable);
customElements.define('information-banner', InformationBanner);
customElements.define('time-slot', TimeSlot);
</script>

</head>
<body>

<time-slot></time-slot>
<student-attendance-table></student-attendance-table>
<information-banner></information-banner>

<template id="information-banner-template">
<div>
<a href="tel:1234567890">Call: 1234567890</a>
<div>
<p>Just Some Random Street</p>
<p>Town</p>
<p>State - 123456</p>
</div>
</div>
</template>

<template id="time-slot-template">
<div>
<div>
<input type="radio" name="timeslot" value="slot1" checked>
9:00 AM - 11:00 AM
</div>
<div>
<input type="radio" name="timeslot" value="slot2"> 11:00 AM -
1:00 PM
</div>
<div>
<input type="radio" name="timeslot" value="slot3"> 1:00 PM -
3:00 PM
</div>
</div>
</template>

</body>
</html>

As you can see, one <script> tag of type="module" can import all three of them together, and can register the custom elements, which can be used in the <body> tag.

You have been reading a chapter from
Getting Started with Web Components
Published in: Aug 2019
Publisher: Packt
ISBN-13: 9781838649234
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image