12. Web Components

Overview

By the end of this chapter, you will be able to create custom elements to use on a web page; encapsulate the structure and styles of a custom element with the Shadow DOM; create a simple-modal using HTML templates; and create and share your own web component.

This chapter introduces the three technologies used to create web components – custom elements, the Shadow DOM, and HTML templates – and how they can be combined to make reusable components.

Introduction

In the previous chapter, we learned about the techniques we can use to ensure we are writing in a well-supported standard. In this chapter, we will use many of these techniques to create web components – bits of UI that we can safely share across multiple web apps and sites.

The idea behind web components is that you, as a developer, are able to create a custom HTML element that is reusable.

To facilitate the reusability of the component, we need to be able to encapsulate the functionality or behavior of the component so that it doesn't pollute the rest of our code and, in turn, is not polluted by outside influences.

For example, we may want to create a web component that handles notifications or alert messages on a web page. To do this, we would want the colors for these messages to be consistent in all cases; perhaps we would add a red background for an error alert, a yellow background for an information alert, and green background for a successful action alert.

Let's assume that these alerts are styled with the following CSS styles:

<style>

.error {

 background: red;

}

.info {

 background: yellow;

}

.success {

 background: green;

}

</style>

If we added these alerts to a page not completely under our control, for example, a WordPress blog or large site using a UI library, it would be easy, for example, to have another error class set that obscures text by setting a style later in the HTML document:

.error {

 color: red;

}

Now, our error alert would appear as a red block with no visible text, which could be a real problem for our users. The encapsulation of a component, in this case, would solve this problem and give us more confidence in our alert component.

If you want to reuse a complex behavior or UI widget before using web components, you would have to:

  1. Copy and paste a block of HTML.
  2. Add the related CSS to the head element of the HTML document.
  3. Add JavaScript near the bottom of the HTML document.
  4. Make sure the changes have had no adverse effects on the rest of your HTML document.
  5. Make sure the rest of the document does not have any adverse effects on the added component.

This is not a great developer experience and it is prone to errors.

The great benefit of web components is that a developer can add a custom element to their web app or HTML document and everything that's needed for the component to work will be encapsulated or contained within that element. This provides a better developer experience, can lead to cleaner code and better-controlled interfaces for your components, and make the interaction between components easier.

We will look at the benefits of web components in more detail later in this chapter but first, we will look at the three technologies we need to make web components. To create web components, we will be working with a combination of three new technologies in HTML5, as follows:

  • Custom elements
  • The Shadow DOM
  • HTML templates

We will look at each of these technologies separately, starting with custom elements, before we look at how combining them makes web components a reality for the modern web.

Custom Elements

We've looked at a lot of the elements provided by the HTML5 standard in previous chapters; for example, the p element defines a paragraph within the body of an HTML document, the a element represents an anchor or link, and the video element defines a video source that we can embed in our HTML document.

HTML5 also tolerates non-standard elements; you could add a tag such as <something></something> in a HTML5 document and it would pass a validation check. However, this element would not have any semantic meaning for a browser. With a custom element, we can do more.

Custom elements let you create your own element types that you can then use on a web page. A custom element is a lot like one of those standard HTML elements; they use the same syntax (a name surrounded by angle brackets) and the main difference is that they are not defined in the HTML5 standard, so we have to register them to be able to use them.

To create a custom element, we will need to add it to the custom element registry, which we do with JavaScript. In JavaScript, this is defined as the CustomElementRegistry object. We can access the CustomElementRegisty object through a property called customElements, which is defined in the global window object.

Note

The global object (that is, in the case of the browser, the window object) contains the DOM document and all the browser APIs that are available in JavaScript. Whenever a JavaScript script runs in the browser, it has access to this window object.

The define Method

The main method of customElements that we want to use is the define method as it allows us to register a new custom element. We call the define method with the customElements.define() JavaScript code and we can pass arguments into the method between the brackets. We need to pass the method a unique name, which is the name for the custom element, and a JavaScript class that defines its behavior.

Note

The class keyword is used to create a JavaScript object with a custom set of methods and properties. We can create an instance of a class using the new keyword, and each instance will have the methods and properties defined in the class.

A class can inherit from another class using the extends keyword. This means that the new class will inherit the methods and properties of the class it extends.

Let's look at an example:

<main-headline />

<script>

     customElements.define("main-headline",

          class MainHeadline extends HTMLElement {

         }

     );

</script>

The define method takes a string as its first argument. This is the name you wish to register your new custom element with. In this example, we have named our custom element, main-headline. Any instances of the main-headline element in the HTML document will, therefore, be registered as this custom element.

The second argument of the define method is the element's constructor. This a JavaScript class that extends HTMLElement and defines the behavior of our custom element. In our example, the behavior adds nothing to HTMLElement, but we will look at some possibilities for extending the behavior of HTMLElement and other built-in elements later in this chapter.

While a JavaScript class can have any name that is valid for a JavaScript variable – the first character must be a letter or underscore and all the other characters can be an alphanumeric character or underscore – there is a convention for class names to use a capitalized first letter and capital letters for the first letter of each word, for instance, MainHeadline.

There is an optional third argument for the define method that takes an options object. Currently, the options object has one possible property, that is, the extends property. The extends property lets us specify a built-in element from which our custom element will inherit.

We will look at extending built-in HTML elements in more detail later in this chapter.

Naming Conventions

For a custom element name to be valid, it must have a hyphen somewhere in the name. For example, main-headline is a valid custom element name, but mainheadline is not.

There is no restriction on how many hyphens appear in the name, which means, for example, sub-sub-headline is a valid custom element name as well.

If we were to provide a non-valid name to the define method it would throw an error, like so:

<!DOCTYPE html>

<html lang="en">

    <head>

        <meta charset="utf-8">

        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <title>Naming convention</title>

    </head>

    <body>

         <mainheadline />

         <script>

             customElements.define("mainheadline",

                  class MainHeadline extends HTMLElement {

                 }

             );

         </script>

     </body>

</html>

This code would result in our browser throwing an error that we can view in the console tab of the developer tools. In Chrome, we can access the developer tools with the keyboard shortcuts Ctrl + Shift + I (PC) or Cmd + Opt + I (Mac).

For example, in the Console tab of the Chrome developer tools, we would see a message similar to the one shown in the following screenshot. The message, highlighted in red, says DOMException: Failed to execute 'define' on 'CustomElementRegistry': 'mainheadline' is not a valid custom element name:

Figure 12.1: Error message for an invalid custom element name

Figure 12.1: Error message for an invalid custom element name

Unique Names

A custom element name can only be registered once. For example, we cannot define two different main-headline elements. If you try to define a custom element when a custom element with the same name has already been defined, the browser will throw an error.

In code, this would look something like the following example, where we have called the customElements.define method twice with the name main-headline:

<!DOCTYPE html>

<html lang="en">

    <head>

        <meta charset="utf-8">

        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <title>Naming convention</title>

    </head>

    <body>

         <main-headline />

         <script>

             customElements.define("main-headline",

                  class MainHeadline extends HTMLElement {

                 }

             );

            customElements.define("main-headline",

                class OtherMainHeadline extends HTMLElement {

                  }

            );

         </script>

     </body>

</html>

In the Console tab of the Chrome developer tools, we will see a message similar to the one shown in the following screenshot. The message, highlighted in red, says DOMException: Failed to execute 'define' on 'CustomElementRegistry': this name has already been used with this registry:

Figure 12.2: Error message for an already used custom element name

Figure 12.2: Error message for an already used custom element name

Extends HTMLElement

If the constructor for our custom element does not extend HTMLElement, the browser will once again throw an error. For example, let's say we try and extend SVGElement instead of HTMLElement:

<sub-headline></sub-headline>

<script>

     customElements.define("sub-headline",

          class SubHeadline extends SVGElement {

         }

     );

</script>

This code would, again, result in an error being thrown, with a message similar to the one shown in the following screenshot. The message, highlighted in red, says TypeError: Illegal constructor:

Figure 12.3: Error message for constructor not inheriting from HTMLElement

Figure 12.3: Error message for constructor not inheriting from HTMLElement

The constructor can extend a subclass of HTMLElement rather than extend HTMLElement. For example, HTMLParagraphElement extends HTMLElement, and that means our constructor can extend HTMLParagraphElement rather than HTMLElement.

We will see more examples of extending a subclass of HTMLElement when we look at extending built-in elements later in this chapter.

So far, we've seen how we can define a custom element and pass it a class constructor that extends HTMLElement to define its behavior. Now, we'll try creating our own custom element.

Exercise 12.01: Creating a Custom Element

In this exercise, we are going to create a custom element and add it to a web page. We will create a blog-headline custom element for a blog post.

Here are the steps we will follow:

  1. Firstly, we want to create a new directory. We will call this web_components.
  2. In web_components, we will create an Exercise 12.01.html file. Open that file and copy this HTML code in to create a web page:

    <!DOCTYPE html>

    <html lang="en">

        <head>

             <meta charset="UTF-8">

            <title>Exercise 12.01: Creating a Custom Element</title>

        </head>

        <body>

             <main class="blog-posts">

                 <article>

                     <!-- add main-heading here -->

                 </article>

             </main>

        </body>

    </html>

  3. We want to replace the <!-- add main heading here --> comment with our custom element. When we register our custom element, we will call it blog-headline. We will start by adding an instance of the blog-headline element to our web page:

    <blog-headline />

  4. We now need to define our blog-headline custom element and add it to the custom elements registry. We will do this by adding a script element after the closing tag of the main element. We want to add this after the main element as we can be sure the content in the main element has loaded when the script is executed:

         </main>

         <script>

              // register blog-headline

         </script>

    </body>

  5. Next, we need to create our custom element's class constructor, which will extend HTMLElement:

    <script>

         class BlogHeadline extends HTMLElement {}

    </script>

  6. We will then register our custom element constructor using the define method on the global customElements object:

    <script>

         class BlogHeadline extends HTMLElement {}

         window.customElements.define("blog-headline", BlogHeadline);

    </script>

  7. So far, we have registered our blog-headline element but it doesn't do anything. In order to see our blog-headline element appear in a web page, we will add the text "Headline" as text content to the blog-headline element. We need to add a constructor method to our BlogHeadline class:

    <script>

         class BlogHeadline extends HTMLElement {

             constructor() {

                  super();

              }

         }

         window.customElements.define("blog-headline", BlogHeadline);

    </script>

    The constructor is a special method that is called when an instance of our custom element is created. We have to call the super function here as that will construct the parent class, that is, the HTMLElement class, that our BlogHeadline class extends. If we do not call super in the constructor, our BlogHeadline class will not have properly extended HTMLElement, which will cause an Illegal constructor error.

  8. In our constructor, the this keyword refers to the scope of our class, which refers to an instance of the custom element. This means we can add text to the element with the textContent property:

    <script>

         class BlogHeadline extends HTMLElement {

             constructor() {

                  super();

                 this.textContent = "Headline";

              }

         }

         window.customElements.define("blog-headline", BlogHeadline);

    </script>

  9. Finally, to show you that we can create multiple instances of our blog-headline element, we will duplicate the article element with the blog-headline element in it as shown in the following code:

                <article>

                    <blog-headline />    

                </article>

                <article>

                    <blog-headline />    

                </article>

If you now right-click on the filename in VSCode on the left-hand side of the screen and select open in default browser, you will see the result of this code that is shown in the following image. We have created a custom element, the blog-headline element, which currently adds the word "Headline" wherever it is added to the HTML document. In the next few exercises, we will expand upon the functionality of our blog-headline element to make it more useful:

Figure 12.4: The blog-headline custom element in action

By adding text to our blog-headline element, we have had a glimpse at how we can add behavior to our custom elements. So far, this custom element doesn't do much, but we will learn about techniques for making more complex custom elements and for adding styles, behavior, and customized instances of the element to make them more reusable.

Next, we will delve deeper into how we can customize the behavior and functionality of a custom element.

Behavior of a Custom Element

Like the standard elements in HTML5, our custom elements can have content (text or child elements) and can provide an interface for modifying the element via attributes. As each custom element has a JavaScript class associated with it, we can enhance the element with a JavaScript API, where we can define properties and methods that add functionality to our custom element.

We can add text to an instance of a custom element and add attributes in the same way we would with the built-in elements. For example, we could have a custom element, the styled-text element, with some text content and options for a dark, highlight, or light theme via the theme attribute:

<styled-text theme="dark">The castle stood tall.</styled-text>

We can then refer to the attribute from within our custom element's JavaScript class using the getAttribute, setAttribute, and hasAttribute methods. In JavaScript, we are able to control the flow of our code using if/else statements and switch statements.

Note

In JavaScript, we can use the if, else if, and else statements to control which lines of code are executed. if/else statements will check a condition that can be either true or false and will execute the code it encompasses if that condition is met. For example, the if(true) statement would always run, whereas if(false) would never run. An else statement will run when the condition of a preceding if statement is not met.

To style the text according to the theme attribute, we could define the custom element styled text. Here, we use the setStyle method to change the background color and color styles of the element depending on the value of the theme attribute. The code is as follows:

Example 12.01.html

23                   setStyle(theme) {

24                      if (theme === "highlight") {

25                          this.style.backgroundColor = "yellow";

26                           this.style.color = "black";

27                           }

28                        else if (theme === "light") {

29                          this.style.backgroundColor = "white";

30                          this.style.color = "black";

31                          }

32                       else if (theme === "dark") {

33                          this.style.backgroundColor = "black";

34                          this.style.color = "white";

35                      }

36                  }

If we apply each of the themes to the same text, we get the following output. The different theme attribute values, that is, highlight, light, and dark, each style the text differently:

Figure 12.5: A custom styled text element

In the next exercise, we will use a similar technique to that shown in the preceding example to work with multiple attributes on a custom element. We will set attributes on a blog-headline element to change the appearance of the headline.

As well as attributes with a value such as "light" or "dark" and "dog" or "cat", we can use boolean attributes. A boolean attribute is an attribute that is either present or absent from the element. You do not have to set a value for this attribute. The fact is that if it is present it is true and if it is absent it is false. As an example, we can set our blog-headline to either be a new post or not use an attribute.

For example, the following code would set the first custom element as an old post and the second element as a new post. This is done by using the newpost attribute:

 <blog-headline>Old post</blog-headline>

 <blog-headline newpost>New post</blog-headline>

We can put these techniques for adding attributes to a custom element and using those attributes to control the behavior of a custom element into practice with an exercise.

Exercise 12.02: Adding and Using Custom Elements with Attributes

In this exercise, we will expand upon the functionality of the blog-headline element that we created in our previous exercise.

We will add text content to the blog-headline element so that we have different text for each blog-headline instance.

We will then modify the blog-headline with attributes:

  • We will add an attribute for type that will add an icon to the heading depending on the type of blog post.
  • We will also add a newpost attribute, which will take a boolean value. If the newpost attribute exists on the blog-headline element, we will add an icon of a clock to the blog-headline element.

The steps are as follows:

  1. We start by creating an Exercise 12.02.html file in the web_components directory.
  2. Our starting point is the code resulting from Exercise 12.01.html. You can copy the following code into Exercise 12.02.html or make a copy of Exercise 12.01.html and rename that file. We have changed the title to Exercise 12.02, Custom Element with Attributes, but nothing else has changed:

        <head>

             <meta charset="UTF-8">

                <title>Exercise 12.02: Custom Element with Attributes</title>

        </head>

  3. We will start by developing our articles. We will replace the two closed blog-headline elements with proper text content. For this exercise, we will add the text "Blog Post About Kittens" to the blog-heading element in the first article and we will add the text "Blog Post About Puppies" to the blog-heading element in the second article:

             <main class="blog-posts">

                <article>

                    <blog-headline>Blog Post About Kittens</blog-headline>

                </article>

                <article>

                    <blog-headline>Blog Post About Puppies</blog-headline>

                </article>

            </main>

  4. Next, we will add a type attribute to each blog-headline element and set the value to either "dogs" or "cats", depending on the content of the article:

             <main class="blog-posts">

                <article>

                    <blog-headline type="cats">Blog Post About Kittens</blog-headline>

                </article>

                <article>

                    <blog-headline type="dogs">Blog Post About Puppies</blog-headline>

                </article>

            </main>

  5. We can now update the BlogHeadline class to handle the different values for the type attribute. We will keep the constructor but remove the line that sets the text content to "Headline". The text content will now be set when the element is added to the web page:

         <script>

             class BlogHeadline extends HTMLElement {

                    constructor() {

                            super();

                 }

             }

             window.customElements.define("blog-headline", BlogHeadline);

        </script>

  6. Next, we add a getter and setter to handle the type attribute. Getters and setters are special methods that can control how a property or attribute is updated. Rather than a property being set directly with the value it is given, we can do checks to make sure the property is valid or change the value with a method. The code is as follows:

         <script>

             class BlogHeadline extends HTMLElement {

                    constructor() {

                            super();

                 }

                get type() {

                     return this.getAttribute("type");

                  }

                 set type(newValue) {

                    this.setAttribute("type", newValue);

                 }

             }

             window.customElements.define("blog-headline", BlogHeadline);

         </script>

    When the type attribute is set, we want to add an icon to the headline. Depending on whether the type attribute value is dogs or cats, the icon will either be a dog or a cat. We will use the Unicode values ? and ? to create our cat and dog icons, respectively. You can try out other Unicode characters too; search online for a Unicode version of a symbol that you can copy into the code in place of the cat or dog.

  7. Next, we will create a setIcon method. This block of code will be called whenever we set the type of the custom element. It will check whether we have set the type attribute to cats or dogs and will change the text content of the custom element accordingly:

             class BlogHeadline extends HTMLElement {

                    constructor() {

                            super();

                 }

                get type() {

                     return this.getAttribute("type");

                  }

                 set type(newValue) {

                    this.setAttribute("type", newValue);

                    this.setIcon(this.type);

                 }

                setIcon(type) {

                     if (type === "cats") {

                         this.textContent = "? " + this.textContent + " ?";

                       }

                     else if (type === "dogs") {

                        this.textContent = "? " + this.textContent + " ?";

                     }

                 }

             }

  8. Next, we will check for the type attribute in the constructor using hasAttribute to make sure the icon gets set when the custom element is instantiated:

                    constructor() {

                            super();

                    if (this.hasAttribute("type")) {

                         this.setIcon(this.type);

                      }

                 }

  9. Now, we will add another attribute, that is, the newpost attribute. We will add this to the blog-headline element for the first article, that is, the one about kittens. This attribute will be a boolean, so we are only interested in whether it exists on the element. We add it to the blog-headline element:

             <main class="blog-posts">

                <article>

                    <blog-headline type="cats" newpost>Blog Post About Kittens</blog-                headline>    

                </article>

                <article>

                    <blog-headline type="dogs">Blog Post About Puppies</blog-headline>

                </article>

            </main>

  10. To handle the newpost attribute in our BlogHeadline class, we add a check for newpost at the end of the constructor.

                    constructor() {

                            super();

                    if (this.hasAttribute("type")) {

                         this.setIcon(this.type);

                      }

                     this.newpost = this.hasAttribute("newpost");

                 }

  11. Next, we will add getters and setters for the newpost attribute. We can add these just above the setIcon method:

                get newpost() {

                     return this.hasAttribute("newpost");

                  }

                 set newpost(newValue) {

                     if (newValue) {

                         this.setAttribute("newpost", "");

                     }

                     else {

                        this.removeAttribute("newpost");

                     }

                     this.setIsNewPostIcon(this.newpost);

                 }

  12. Next, we want to create a setIsNewPostIcon method to handle the newpost attribute value:

                    setIsNewPostIcon(isNewPost) {

                        if (isNewPost) {

                            this.classList.add("new-post");

                        }

                        else {

                            this.classList.remove("new-post");

                        }

                    }

    The setIsNewPostIcon method will add a class called "newpost" to the blog-headline element if the newpost attribute exists on the element. We have a boolean (true or false) value called isNewPost, which is the argument for the setIsNewPostIcon method. This isNewPost value is set to true or false based on whether the this.hasAttribute('type') check is true or false. The class will add a clock icon to elements with the newpost attribute via CSS.

  13. Finally, we will add some CSS to the head element of our HTML document to style the new-post class:

            <style>

                .new-post::before {

                    font-weight: bold;

                    content: "⏰ NEW! ⏰";

                }

            </style>

If you now right-click on the filename in VSCode on the left-hand side of the screen and select open in default browser, you will see the following image that shows what this would look like in the browser:

Figure 12.5: A custom styled text element

Figure 12.6: blog-headline version 2 with extra icons!

There are a couple of problems with the blog-headline element that we have created.

One problem that may cause us issues is that the element is not encapsulated; it could influence and be influenced by the rest of the web page. For example, we have added the new-post class to the head element, which means it can be used throughout the HTML document. We want our web components to be more contained.

We will look at the Shadow DOM later in this chapter and learn how we can use that technology to protect a component from outside influences.

Equally problematic is the fact that we are only checking attributes in the constructor of our BlogHeadline class. This means that if we change attributes with JavaScript, they will not be reflected in the blog-headline instances on the page.

To handle the second problem and many other issues, custom elements give us some life cycle methods, which we will look at in the next section.

Custom Element Life Cycle

A custom element has several life cycle methods that can be used to call blocks of code at the right time in the custom elements life cycle.

The constructor of a custom element is called when the element is instantiated, but there are several ways to create new instances of an element for a web page: adding them in an HTML document or creating them with JavaScript using document.createElement. This means we can't guarantee that a custom element has been connected to the web page when the constructor is called. This is why custom elements provide a connectedCallback method.

The connectedCallback method is invoked when a custom element is added into the HTML document. It can happen more than once, for example, if an element is connected and then disconnected and then reconnected to the document.

The disconnectedCallback method is invoked when the element is disconnected from the HTML document.

The adoptedCallback method is invoked if the custom element moves to a new document. An example of this callback being invoked is when we move a custom element between an HTML document and an iframe.

The attributeChangedCallback method is invoked when an element's attributes change, that is, whether the value changes or the attribute is added or removed. The callback receives three arguments:

  • name: The name of the attribute
  • oldValue: The old value of the attribute before the change
  • newValue: The new value of the attribute after the change

Not all changes to attributes will trigger attributeChangedCallback; rather, we, as developers, are responsible for maintaining a whitelist of attributes we wish to observe. We do this with the static observedAttributes method.

For example, if I wanted to know when the type attribute changed in our previous exercise, I would set the array returned by the observedAttributes method to include "type". I would then handle the change to the type attribute with attributeChangedCallback like so:

static get observedAttributes() {

     return ["type"];

}

attributeChangedCallback(name, oldValue, newValue) {

    if (name === "type") {

         // handle changes to the value of the type attribute

     }

}

Something else to consider is that oldValue and newValue may not necessarily be different. The callback is invoked because the attribute has been set, but depending on what we want to do when the value changes, we may want to check whether the value has actually changed. We can do this by comparing the old and new value and escaping the function as early as possible if the two values are the same:

attributeChangedCallback(name, oldValue, newValue) {

    if (oldValue === newValue) { return; }

In the next exercise, we will make changes to the blog-headline element. This time, we will use the life cycle methods to improve our custom element. We will add some functionality to trigger the different life cycle methods so that we can experience them for ourselves in a better way.

Exercise 12.03: Custom Element Life Cycle

We will start from where we left off in the previous exercise. In this exercise, we will make further improvements to the blog-headline element and we will add some functionality so that we can test the life cycle of our custom element by adding and removing the element and changing attributes dynamically.

Here are the steps to follow:

  1. We will start by creating an Exercise 12.03.html file in the web_components directory.
  2. Our starting point is the code resulting from Exercise 12.02.html, so we will make a copy of Exercise 12.02.html and rename that file to Exercise 12.03.html. We will change the title to Exercise 12.03: Custom Element Life Cycle.
  3. Next, we will add a test-ui style to give the UI a bit more whitespace between items:

                .test-ui {

                    margin-top: 2rem;

                }

  4. We want to know when the type attribute and the newpost attribute have changed. To do this, we will add them to the array that's returned by the observedAttributes function:

                 static get observedAttributes() {

                     return ["type", "newpost"];

                 }

  5. Next, we will add attributeChangedCallback to where we can handle any changes to the values of the type attribute and the newpost attribute. We'll check that the old value and the new value are actually different and then handle changes to type or newpost with the setIcon and setIsNewPostIcon methods, respectively:

                    attributeChangedCallback(name, oldValue, newValue) {

                        if (oldValue === newValue) { return; }

                        if (name === "type") {

                            this.setIcon(this.type);

                        }

                        else if (name === "newpost") {

                            this.setIsNewPostIcon(this.newpost);

                        }

                    }

  6. We are going to add a getter for the _heading property so that we can access it as the variable heading:

                    get heading() {

                        return this._heading;

                    }

  7. Now that we are responding to changes to the blog-headline elements' attributes, we don't need to do this in the constructor of our BlogHeading class. For now, we will simplify constructor so that it calls super and stores the initial text content of the heading:

                    constructor() {

                            super();

                     this._heading = this.textContent;

                    }

  8. At this point, we have made our custom element respond to attribute changes. We can test this by adding a UI to allow us to change the attributes dynamically. To do this, we will add the following HTML beneath the main element:

                <div class="test-ui">

                      <button id="swap-type-1">Swap type attribute</button>

                       <button id="swap-newpost-1">Swap newpost attribute</button>

               </div>

  9. We want these two buttons to swap the attribute values of the first blog-headline element when they are clicked. To do that, we will need a reference to the buttons and the blog-headline element, and we will add the following to the bottom of our script element:

    const headline = document.querySelector("blog-headline");

    const swapTypeButton = document.getElementById("swap-type-1");

    const swapNewpostButton = document.getElementById("swap-newpost-1");

    swapTypeButton.addEventListener("click", function() {

         const type = headline.getAttribute("type");

         headline.setAttribute("type", type == "cats" ? "dogs" : "cats");

    });

    swapNewpostButton.addEventListener("click", function() {

         const newpost = headline.hasAttribute("newpost");

         if (newpost) {

             headline.removeAttribute("newpost");

         }

         else  {

             headline.setAttribute("newpost", "");

        }

    });

    With those buttons, we can test whether the attribute value changes are reflected in our blog-headline element. For example, if we click the Swap type attribute button when the blog-headline has cats, it will swap to dogs and the icon should change to dogs.

    So far, the code we have written will result in the following output:

    Figure 12.7: Custom headline elements with buttons to update attributes

    Figure 12.7: Custom headline elements with buttons to update attributes

    By clicking the left button, we can change the type attribute of the top blog-headline element, as shown in the following image:

    Figure 12.8: Swapping the type attribute value from cats to dogs

    Figure 12.8: Swapping the type attribute value from cats to dogs

    By clicking the right button, we can toggle the newpost attribute of the top blog-headline element and remove the new-post class attribute, as shown in the following image:

    Figure 12.9: Top blog-headline element without the new-post class

    Figure 12.9: Top blog-headline element without the new-post class

  10. We will add another button to connect and disconnect the blog-headline element from the HTML document. First, we will add the button to div.test-ui:

                <div class="test-ui">

                      <button id="swap-type-1">Swap type attribute</button>

                       <button id="swap-newpost-1">Swap newpost attribute</button>

                 <button id="toggle-connect-1">Toggle connection</button>

               </div>

  11. Next, we want to get a reference to the button. We also want to get a reference to the parent element that hosts the headline. This will make it easier to add and remove the headline element from the DOM:

    const headline = document.querySelector("blog-headline");

    const swapTypeButton = document.getElementById("swap-type-1");

    const swapNewpostButton = document.getElementById("swap-newpost-1");

    const toggleConnectionButton = document.getElementById("toggle-connect-1");

    const headlineParent = headline.parentElement;

  12. When the Toggle Connection button is clicked, it will remove or add the headline element to the DOM:

    toggleConnectionButton.addEventListener("click", function() {

         if (headline.parentElement) {

              headlineParent.removeChild(headline);

            }

             else {

                  headlineParent.insertBefore(headline, headlineParent.firstChild);

            }

    });

  13. Using the console.log method, we can output a message to the JavaScript console. We will add this to connectedCallback and disconnectedCallback so that we can see when these methods have been triggered. We can see the message in the console tab of the developer tools (see Chapter 1, Introduction to HTML and CSS, for an introduction to the developer tools):

                    connectedCallback() {

                      console.log("Connected");

                      this.setIcon(this.type);

                    }

                    disconnectedCallback() {

                        console.log("Disconnected");

                    }

    If you now right-click on the filename in VSCode on the left-hand side of the screen and select open in default browser, you will see the results of the exercise that is shown in the following image. The buttons let us update the first headline by changing the element's attributes and connecting and disconnecting the element:

Figure 12.10: Custom headline elements with buttons to update the attributes and elements

Figure 12.10: Custom headline elements with buttons to update the attributes and elements

If we open the Chrome developer tools, we will see the messages Connected and Disconnected logged in the console each time we click the Toggle connection button, as shown in the following screenshot:

Figure 12.11: Disconnected and Connected messages logged in the console

Figure 12.11: Disconnected and Connected messages logged in the console

So far, we have looked at the life cycle of a custom element. Now, we will look at how we can extend built-in elements such as the anchor (<a />) element.

Extending a Built-in Element

Our custom element does not have to directly extend HTMLElement as long as it extends another built-in element that is a subclass of HTMLElement. This means we can customize the behavior of existing elements such as the p element via HTMLParagraphElement or the a element via HTMLAnchorElement.

We extend the custom element with the third optional argument of the customElements.define method. We pass the argument an options object with an extends property. The value we give the extends property is the name of the built-in element we wish to extend. For example, if we want to create a custom h1 element that restricts the size of the text content, we would set the extends property to "h1":

window.customElements.define("short-headline", ShortHeadline, { extends: "h1"});

The ShortHeadline constructor would now extend HTMLHeadingElement instead of HTMLElement:

class ShortHeadline extends HTMLHeadingElement {

     //… functionality for short headlines

}

To use this custom element, we would not create a short-headline element; instead, we would use the is attribute on an h1 element and set the value to our short-headline custom element:

<h1 is="short-headline">Headline</h1>

If we extend an element, we can use all the properties and attributes available to that element. For example, we could extend the anchor tag based on its attributes. We could add information about a link, such as whether it opens in a new browser window.

In the next exercise, we will look at how we can extend the anchor element to provide this information.

Exercise 12.04: Custom Element Extending HTMLAnchorElement

In this exercise, we are going to create a new custom element that extends the anchor element and provides information about a link before a user clicks it.

We are going to take advantage of the target attribute of the anchor element. The target attribute can be set to _self, _blank, _parent, or _top. The default behavior, _self, is for a URL that's navigated to from the anchor element to load in the current browser context. When the target is set to _blank, the URL will launch in a new tab or window (depending on your browser's configuration). The other two options relate to iframes, that is, launching in the parent or top-level browser context. They will both act the same as _self if there is no parent context.

Here are the steps:

  1. We start by creating an Exercise 12.04.html file in the web_components directory.
  2. We'll start with the following code, which creates a web page with a main element that has an unordered list with two list items. Each of these list items has a link to a URL on the Packt website. The first link opens in the current window, whereas the second link will open the URL in a new tab or window. Copy this code into your Exercise 12.04.html file and save it:

    <!DOCTYPE html>

    <html lang="en">

        <head>

            <title>Exercise 12.04: Extending HTMLAnchorElement</title>

        </head>

        <body>

            <main class="blog-post">

                 <ul class="links">

                    <li><a href="https://www.packtpub.com/web-development">Link that opens in current window</a></li>

                    <li><a href="https://www.packtpub.com/free-learning" target="_blank">Link that opens in a new window.</a></li>

                </ul>

            </main>

            <script>

                

            </script>

        </body>

    </html>

  3. In the script element, we are going to define a custom element and create a constructor that extends the anchor element with HTMLAnchorElement:

            <script>

              class AnchorInfo extends HTMLAnchorElement {

                    constructor() {

                        super();

                        this._originalText = this.textContent;

                    }

              }

                window.customElements.define("anchor-info", AnchorInfo, {extends: "a"});

            </script>

  4. We will check the target attribute of the custom element and see if it opens in a new window, that is, if the target attribute has a value of _blank and add an appropriate icon, ⧉, if it is. To do this, we will add an observedAttributes method to observe the "target" attribute:

                 static get observedAttributes() {

                        return ["target"];

                    }

  5. We then need to handle changes to the target attribute using attributeChangedCallback. When the value of the target attribute changes, we will trigger a method called checkTarget. This method will set a boolean value of opensInNewTab:

                    attributeChangedCallback(name, oldValue, newValue) {

                        if (oldValue === newValue) { return; }

                        switch (name) {

                            case "target":

                                this.checkTarget();

                                break;

                        }

                    }

                    checkTarget() {

                        this.opensInNewTab = this.target === "_blank";

                    }

  6. We will call a render method that checks the value of opensInNewTab and renders the component accordingly:

                    checkTarget() {

                        this.opensInNewTab = this.target === "_blank";

                        this.render();

                    }

                    render() {

                     const targetState = this.opensInNewTab ? " ⧉" : "";

                        this.textContent = this._originalText + targetState

                    }

  7. Finally, to make our anchor elements behave as the anchor-info custom element, we need to add the is attribute with the anchor-info value to them:

         <ul class="links">

                    <li><a is="anchor-info" href="https://www.packtpub.com/web-development">Link that opens in current window</a></li>

                    <li><a is="anchor-info" href="https://www.packtpub.com/free-learning" target="_blank">Link that opens in a new window.</a></li>    

         </ul>

    The second link on the page opens in a new window. If you now right-click on the filename in VSCode on the left-hand side of the screen and select open in default browser, you will see it will have the external link icon added, as shown in the following screenshot:

Figure 12.12: Custom anchor-info element that extends the anchor element

Figure 12.12: Custom anchor-info element that extends the anchor element

We've looked at creating custom elements but up until now, we haven't been able to encapsulate the element. We will look at the Shadow DOM and see how it can help protect our custom element from its context and vice versa.

Shadow DOM

The Shadow DOM is a feature that lets us control access to parts of the DOM.

If we think of our HTML document as the non-shadow, or light, DOM then all the objects and styles are accessible from the root of the document by traversing the DOM tree.

Even if we were to include a third-party script or stylesheet on our web page, we could still change the style or behavior with our own code. A user can even add their own stylesheets in many browsers and run scripts on your page through dev tools or via extensions.

The Shadow DOM lets us protect parts of our code from these outside influences, which is vital if we want our web components to work across various web pages or apps where we can't possibly know what the context is.

The Shadow DOM is an HTML fragment or DOM tree that is hidden from the rest of the light DOM. It needs to be attached to a shadow host, which is a node in the visible DOM. As with all DOM trees, the Shadow DOM will have a Shadow root from which the rest of the tree branches.

Attaching a Shadow DOM

To make use of the Shadow DOM, we need to use the attachShadow method on an element via JavaScript. When we call the attachShadow method, we can pass an options object with a mode property. There are two options available for the mode property: "open" and "closed".

Here's an example of attaching a shadow DOM in open mode to a div element with the "host" ID attribute. When we attach the shadow DOM in open mode, we are able to access the shadow root of the shadow DOM, which means we can access that DOM from the outside:

<div id="host"></div>

<script>

 const hostElement = document.getElementById("host");

 const openShadowDOM = hostElement.attachShadow({ mode: "open" });

 const shadowRoot = hostElement.shadowRoot;

 console.log(shadowRoot); // will return the shadow root

</script>

If we have a reference to the shadow root, we can manipulate the shadow DOM, which means we can append elements to it or remove or change elements within the DOM.

For example, we can append a paragraph to the shadow DOM via the root:

<div id="host"></div>

<script>

 const hostElement = document.getElementById("host");

 hostElement.attachShadow({ mode: "open" });

 const shadowRoot = hostElement.shadowRoot;

 const paragraph = document.createElement("p");

 paragraph.textContent = "Lorem ipsum, etcetera"

 shadowRoot.appendChild(paragraph);

</script>

The open mode does offer a degree of encapsulation. For example, if we try to apply a style to the head element of a web page that applies to all the paragraph elements, it will not apply to a paragraph within the Shadow DOM. We have a single point of access via the shadow root.

We can see this via the result of the following code, which is shown in the screenshot following this code:

Example 12.02.html

16           <script>

17           const hostElement = document.getElementById("host");

18            hostElement.attachShadow({ mode: "open" });

19            const shadowRoot = hostElement.shadowRoot;

20 

21            const paragraph = document.createElement("p");

22            paragraph.textContent = "Paragraph in the Shadow DOM";

23           shadowRoot.appendChild(paragraph);

24          </script>

In the following screenshot, we can see that the two paragraphs either side of our host div have been styled according to a style rule targeted at the p element selector. However, despite the shadow DOM having a paragraph element, it has not received the style.

This is an example of the encapsulation the Shadow DOM achieves, even in open mode.

We have included the dev tools representation of the elements in the following screenshot as this shows the shadow root (#shadow-root and the mode in brackets). We will explore the topic of using the dev tools to inspect the Shadow DOM further in the next section:

Figure 12.13: Shadow DOM with a style not applied to it

Figure 12.13: Shadow DOM with a style not applied to it

By changing the mode to closed, we change the output of the shadow root. It will now return null, which means we cannot access the Shadow DOM from the rest of the DOM through the shadowRoot property of an element:

<div id="host"></div>

<script>

 const hostElement = document.getElementById("host");

 hostElement.attachShadow({ mode: "closed" });

 const shadowRoot = hostElement.shadowRoot;

 console.log(shadowRoot); // will return null

</script>

This does not mean, however, that you cannot get a reference to the shadow root. attachShadow will still return a reference to shadowRoot. The idea is that you will use this inside a custom element or a web component and therefore will have a reference to the shadow root without it being accessible via an element in the main HTML document.

Inspecting the Shadow DOM

Most browsers come with a set of tools designed to help web developers when they are working on a web page. In the next exercise, we will use the Chrome dev tools to inspect the Shadow DOM of the browser's in-built input element.

It can be useful to inspect the Shadow DOM of in-built elements as it will give us a better idea of what these elements are doing and how to build our own web components.

To inspect the Shadow DOM of browser elements in the Chrome dev tools, we need to know a little bit about the dev tools and we need to set them up so that they let us inspect the Shadow DOM.

In Chrome, we can access the dev tools with the keyboard shortcuts Ctrl + Shift + I (on PC) and Cmd + Opt + I (on Mac). On most websites, you can also access the context menu by right-clicking and then choosing Inspect from the menu options.

The dev tools will appear like so:

Figure 12.14: The developer tools in Chrome

Figure 12.14: The developer tools in Chrome

The Chrome dev tools provide a vast array of tools for web development, most of which are beyond the scope of this chapter. Across the top panel in the preceding screenshot, we can see a set of tabs, including Elements, Console, Network, and so on. We will be focusing on the Elements tab here.

The Elements tab gives you access to the HTML document of your web page. You can hover over elements in the Elements display and they will be highlighted on the web page. You can also edit these elements; change the text or attributes to immediately see what effect those changes will have on your web page.

It is also possible to inspect the Shadow DOM via the Elements tab. To do this, we may need to change the settings of our dev tools. On the right of the top panel, next to the cross that closes the dev tools, there are three vertical dots that open the controls for the dev tools. Click this and select the Settings option. Alternatively, you can press the F1 key. This will bring up the Settings pane that's shown in the following screenshot:

Figure 12.15: Settings pane of Chrome dev tools

There are a great many settings available for you to change, but we want to find the Elements heading and toggle Show user agent shadow DOM so that it is checked. We can now close the settings pane and return to the Elements tab.

We should now have access to the shadow DOM of the browser. You can determine a shadow DOM because the shadow root is marked in the Elements panel. For example, given an input element with the type set to number, we will see the results shown in the following screenshot in the Elements panel:

<input type="number" name="tel">

Figure 12.16: Inspecting the Shadow DOM of a number input

Figure 12.16: Inspecting the Shadow DOM of a number input

In the preceding screenshot, we can see that the input element actually attaches a Shadow DOM. Attached to the shadow root, we can see a container div (with an ID of 'text-field-container') hosting a viewport and a spin button that lets you increment the number value of the input.

In the next exercise, we will go back to a custom element we worked on in Exercise 12.01, Creating a Custom Element, Exercise 12.02, Custom Element with Attributes, and Exercise 12.03, Custom Element Life Cycle, and look at how we can use the Shadow DOM with that custom element to encapsulate its structure and styles.

Exercise 12.05: Shadow DOM with a Custom Element

In this exercise, we will encapsulate the structure and styles of a custom element with the Shadow DOM.

Here are the steps:

  1. Start by creating an Exercise 12.05.html file in the web_components directory.
  2. We'll start with code based on the blog-headline custom element we worked on in Exercise 12.01, Creating a Custom Element, Exercise 12.02, Custom Element with Attributes, and Exercise 12.03, Custom Element Life Cycle. Copy and save the following code:

    <!DOCTYPE html>

    <html lang="en">

        <head>

            <meta charset="UTF-8">

            <title>Exercise 12.05: Shadow DOM</title>

            <style>

                .new-post::before {

                    font-weight: bold;

                    content: "⏰ NEW! ⏰";

                }

            </style>

        </head>

        <body>

            <main class="blog-posts">

                <article>

                  <blog-headline type="cats" newpost>Blog Post About Kittens</blog-              headline>

                </article>

                <article>

                    <blog-headline type="dogs">Blog Post About Puppies</blog-headline>  

                </article>

            </main>

            <script>

                class BlogHeadline extends HTMLElement {

                    static get observedAttributes() {

                        return ["type", "newpost"];

                    }

                    constructor() {

                        super();

                        this._heading = this.textContent;

                    }

                    connectedCallback() {

                      this.setIcon(this.type);

                    }

                    disconnectedCallback() {

                        console.log("Disconnected");

                    }

                    attributeChangedCallback(name, oldValue, newValue) {

                        if (oldValue === newValue) { return; }

                        switch(name) {

                            case "type":

                                this.setIcon(this.type);

                                break;

                            case "newpost":

                                this.setIsNewPostIcon(this.newpost);

                                break;

                        }

                    }

                    get heading() {

                        return this._heading;

                    }

                    get type() {

                        return this.getAttribute("type");

                    }

                    set type(newValue) {

                        this.setAttribute("type", newValue);

                        this.setIcon(this.type);

                    }

                    get newpost() {

                        return this.hasAttribute("newpost");

                    }

                    set newpost(newValue) {

                        if (newValue) {

                            this.setAttribute("newpost", "");

                        }

                        else {

                            this.removeAttribute("newpost");

                        }

                        this.setIsNewPostIcon(this.newpost);

                    }

                    setIcon(type) {

                        switch(type) {

                            case "cats":

                                this.textContent = "? " + this.heading + " ?";

                                break;

                            case "dogs":

                                this.textContent = "? " + this.heading + " ?";

                                break;

                        }

                    }

                    setIsNewPostIcon(isNewPost) {

                        if (isNewPost) {

                            this.classList.add("new-post");

                        }

                        else {

                            this.classList.remove("new-post");

                        }

                    }

                }

                

                window.customElements.define("blog-headline", BlogHeadline);

            </script>

        </body>

    </html>

  3. Here, we can see the effects of using the Shadow DOM. We are going to make the BlogHeadline element wrap its text content in an h1 element, which we'll create in the constructor and keep as a property called headingElement:

                    constructor() {

                        super();

                        this._heading = this.textContent;

                        this.textContent = "";

                        this.headingElement = document.createElement("h1");

                        this.headingElement.textContent = this._heading;

                        this.appendChild(this.headingElement);

                    }

  4. We need to update the setIcon and setIsNewPostIcon methods in order to update headingElement:

                    setIcon(type) {

                        if (type === "cats") {

                            this.headingElement.textContent = "? " + this.heading + " ?";

                        }

                        else if (type === "dogs") {

                            this.headingElement.textContent = "? " + this.heading + " ?";

                        }

                    }

                    setIsNewPostIcon(isNewPost) {

                        if (isNewPost) {

                            this.headingElement.classList.add("new-post");

                        }

                        else {

                            this.headingElement.classList.remove("new-post");

                        }

                    }

  5. Next, we'll add a normal h1 element and set a bold new style for h1 elements:

                h1 {

                    font-family: Arial, Helvetica, sans-serif;

                    color: white;

                    background:lightskyblue;

                    padding: 16px;

                }

         </style>

    </head>

    <body>

         <main class="blog-posts">

              <article>

                    <h1>Normal H1 Heading</h1>

              </article>

    If we look at the results of the code so far, we will see that the h1 style affects both our normal h1 element and the blog-headline custom elements, as shown in the following output:

    Figure 12.17: h1 style affecting custom elements

    Figure 12.17: h1 style affecting custom elements

  6. To prevent the h1 style leaking into our custom elements from the document, we will attach a shadow DOM to the custom element. We do this in the BlogHeadline constructor and then append the heading element to shadowRoot instead of the custom element itself:

                    constructor() {

                        super();

                        this._heading = this.textContent;

                        this.textContent = "";

                        this.attachShadow({ mode: "open" });

                        this.headingElement = document.createElement("h1");

                        this.headingElement.textContent = this._heading;

                        this.shadowRoot.appendChild(this.headingElement);

                    }

    The result will be the encapsulation of our blog-headline's DOM from the surrounding document, which we can see in the following screenshot:

    Figure 12.18: Encapsulated blog-headline custom elements

  7. By moving to a Shadow DOM, we have caused an issue. The blog-headline element no longer gets the style from adding the new-post class. We need to move that style out of the head of the document and make it part of the Shadow DOM. We will do this by recreating the style element in the constructor:

                        this.styleElement = document.createElement("style");

                        this.styleElement.innerText = `

                     .new-post::after {

                         font-weight: bold;

                          content: "⏰ NEW! ⏰";

                         margin-left: 8px;

                 }

                 `;

                        this.shadowRoot.appendChild(this.styleElement);

  8. Finally, we have an encapsulated blog-headline custom element. If you now right-click on the filename in VSCode on the left-hand side of the screen and select open in default browser, you will see the result is as shown in the following screenshot:
Figure 12.19: Encapsulated blog-headline custom elements

Figure 12.19: Encapsulated blog-headline custom elements

We've seen how we can encapsulate the structure and style of our custom elements using the Shadow DOM, but using DOM manipulation in JavaScript to work with the Shadow DOM is not as easy as working with HTML.

In the next section, we will see how we can improve the developer experience of working with web components using HTML templates. We will look at how we can use templates to create versatile HTML structures that we can use in an encapsulated component.

HTML Templates

HTML templates let you create flexible templates in HTML that you can use in multiple places. If you have an HTML structure that is used several times on a web page but with different content or data, you can create a template element that defines that structure without immediately showing it to the user on the web page. We can reference the template to create multiple copies of that HTML structure.

For example, we could create a very simple template for a styled button with the following code, which creates a template with an ID attribute of ok-button-template. The markup is simply a button element with the btn and ok-btn classes applied to it. The button is styled with a purple background color, white text, and a hover state that darkens the background:

<template id="ok-button-template">

    <style>

        .btn {-webkit-appearance: none;

            appearance: none;                    

            background-color: #3700B3;

            border: none;

            border-radius: 2px;

            color: white;

            cursor: pointer;

            min-width: 64px;

            outline: none;

            padding: 4px 8px;}

        .btn:hover {background-color: #6200EE;}

     </style>

     <button class="btn ok-btn">OK</button>

</template>

If we created this template in a web page, we wouldn't see it in the page. To use it, we have to attach it to the HTML document. We can easily do that by getting a reference to the template and appending it to the body of the HTML document, like so:

<script>

       const buttonTemplate = document.getElementById("ok-button-template");

    document.body.appendChild(buttonTemplate.content);

</script>

This code will get the content of our template and append it to the HTML document. The result is a button on the web page, as shown in the following screenshot:

Figure 12.20: OK button template in the web page

Figure 12.20: OK button template in the web page

We may come across a problem with adding templates using this method. If we tried to do the same again to create two buttons, we would only see one button. Templates are meant to be reusable, so what is going on here? The answer is: we need to clone the content of the template each time we want to use it. For example, we can create multiple buttons with cloneNode:

<script>

       const buttonTemplate = document.getElementById("ok-button-template");

    document.body.appendChild(buttonTemplate.content.cloneNode(true));

     document.body.appendChild(buttonTemplate.content.cloneNode(true));

</script>

Figure 12.21: Multiple OK buttons cloned from the template

Figure 12.21: Multiple OK buttons cloned from the template

HTML templates are particularly useful when paired with the Shadow DOM in web components because we can create a structure and attach it to the shadow DOM and minimize JavaScript DOM manipulation.

A minimal example of using ok-button-template and combining it with a custom element could look like this:

<script>

     class StyledButton extends HTMLElement {

        constructor() {

            super();

            this.attachShadow({ mode: "open" });

        }

        connectedCallback() {

             const buttonTemplate = document.getElementById("ok-button-             template");

            const node = document.importNode(buttonTemplate.content, true);

            this.shadowRoot.appendChild(node);

        }

    }

    window.customElements.define("styled-button", StyledButton);

</script>

We've attached a shadow DOM again and this time, when the custom element is connected to the DOM, we use importNode to import the node from one document into a subdocument (in this case, the Shadow DOM of our component). Then, we can create instances of our OK button in the HTML document with the <styled-button /> custom element.

As useful as it is to create multiple instances of a button with the OK label, it is perhaps even more useful to be able to use a template but vary the content when we create new instances. We can do that using the slot element in our template. This lets us set areas of the template that we want to be able to change.

For example, we can change our OK button to have any label by creating a label slot:

<template id="slotted-button-template">

     <button class="btn primary-btn"><slot>Label</slot></button>

</template>

Here, we have set a default placeholder as a label but if we instantiate the button with text content, the slot will be replaced with that text content.

Here is the complete code example of creating a default version of the button and two labeled versions:

Example 12.03.html

     <template id="styled-button-template">

        <style>

             .btn {

                 -webkit-appearance: none;

                appearance: none;                    

                background-color: #3700B3;

                border: none;

                border-radius: 2px;

                color: white;

                cursor: pointer;

                min-width: 64px;

                outline: none;

                padding: 4px 8px;

            }

            

             .btn:hover {

                  background-color: #6200EE;

            }

         </style>

         <button class="btn primary-btn"><slot>Label</slot></button>

    </template>

The output is as follows:

Figure 12.22: Multiple styled-button elements with different labels

Figure 12.22: Multiple styled-button elements with different labels

You can also create named slots in a template by adding the name attribute to a slot element, which can then be used to target multiple slots with different content. For example, a template could have named slots for a heading and content:

<template id="article-template">

     <article>

         <h1><slot name="heading">Heading goes here…</slot></h1>

         <slot name="content">Content goes here…</slot>

     </article>

</template>

If we used this template in a custom element called short-article, we would then populate it:

<short-article>

     <span slot="heading">HTML and CSS</span>

     <p slot="content">HTML and CSS are the foundations of a web page.</p>

</short-article>

We now have a reusable custom element; we can provide different content to each instance and because they are HTML elements, we can provide children that are also custom elements.

To see the power of HTML templates in action, we are going to try them out on a slightly more complex structure with slots in the next exercise.

Exercise 12.06: Templates

In this exercise, we will use HTML templates to create a modal, which we will turn into a component called simple-modal.

Here are the steps:

  1. We will start by creating an Exercise 12.06.html file in the web_components directory.
  2. Our starting point will be the following web page with a simple-modal element already attached. Copy and save it in Exercise 12.06.html:

    <!DOCTYPE html>

    <html lang="en">

        <head>

            <meta charset="UTF-8">

            <title>Exercise 12.06: HTML Template</title>

            <style>

                html, body { margin: 0; padding: 0; }

            </style>

        </head>

        <body>

        <simple-modal></simple-modal>

        </body>

    </html>

  3. Beneath the body element, we will create a template for simple-modal called simple-modal-template, and we will create an HTML structure with elements for container, overlay, and modal. The modal will have three parts (a header, body, and footer):

       <template id="simple-modal-template">

        <section class="modal-container">

            <div class="overlay"></div>

            <div class="modal">

                <header class="header">

                    <h1><!-- heading here --></h1>

                </header>

                <div class="body">

                    <!-- content here -->

                </div>

                <footer class="footer">

                    <button class="btn">OK</button>

                </footer>

            </div>

        </section>

       </template>

  4. Next, we will style the modal by adding a style element block in the template. We will make the overlay a fixed position, semi-transparent rectangle that takes up the whole window. We will center the content of the modal container and give the modal some padding and a white background. We will use the same button style that we used previously in this chapter:

            <style>

                .modal-container {

                    display: flex;

                    align-items: center;

                    justify-content: center;

                    height: 100vh;

                }

                .overlay {

                    position: fixed;

                    background: rgba(0, 0, 0, 0.5);

                    top: 0;

                    bottom: 0;

                    left: 0;

                    right: 0;

                }

                .modal {

                min-width: 250px;

                    position: relative;

                    background: white;

                    padding: 16px;

                 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.8);

                }

                .btn {

                    -webkit-appearance: none;

                    appearance: none;                    

                    background-color: #3700B3;

                    border: none;

                    border-radius: 2px;

                    color: white;

                    cursor: pointer;

                    min-width: 64px;

                    outline: none;

                    padding: 4px 8px;

                }

                .btn:hover {

                    background-color: #6200EE;

                     }

            </style>

  5. Next, we are going to add the script element and create our custom element definition. We add this block of code at the end of the web page after the template with the simple-modal-template ID has been defined. We attach the Shadow DOM and attach the template to it:

    <script>

        customElements.define("simple-modal", class SimpleModal extends HTMLElement {

                constructor() {

                    super();

                    this.attachShadow({ mode: "open" });

                }

                connectedCallback() {

                    const tmpl = document.getElementById("simple-modal-template");

                    const node = document.importNode(tmpl.content, true);

                    this.shadowRoot.appendChild(node);

                }

         });

    </script>

    So far, we have created our modal and an instance of it appears on our web page, like so:

    Figure 12.23: Modal with no content

    Figure 12.23: Modal with no content

  6. Our next task is to define slots in our template so that we can provide content when we instantiate the element. We will set up heading and content slots to replace comments in the h1 and body of the template:

       <template id="simple-modal-template">

        <section class="modal-container">

            <div class="overlay"></div>

            <div class="modal">

                <header class="header">

                    <h1><slot name="heading"></slot></h1>

                </header>

                <div class="body">

                    <slot name="content"></slot>

                </div>

                <footer class="footer">

                    <button class="btn">OK</button>

                </footer>

            </div>

        </section>

       </template>

  7. Finally, we can populate the modal when we instantiate it:

        <simple-modal>

            <span slot="heading">You've Opened a Modal!</span>

             <p slot="content">

                 Finished with the modal? Click OK.

              </p>

    </simple-modal>

    If you now right-click on the filename in VSCode on the left-hand side of the screen and select open in default browser, you will see the result of our component as shown in the following screenshot:

Figure 12.24: Modal with content

Figure 12.24: Modal with content

We have now looked at the three technologies that are vital for making web components available in the browser. It is time to put all three together and create a web component.

Creating a Web Component

By combining custom elements with a Shadow DOM and HTML templates, we have a convenient set of tools for making a reusable component – a web component – that is encapsulated from the document it appears in.

We are going to create our own reusable components in the upcoming activity. This component will add an avatar to a web page with a default placeholder image that can be replaced by our own avatar image.

Activity 12.01: Creating a Profile

We've been tasked with creating a profile component for the Films On Demand website.

The profile will be a web component. It will include the name and email of the user and an avatar, which will show a default placeholder image (a silhouette). We will be able to replace the image with a profile image of the user.

  1. We are creating a reusable component that can be used throughout the site and we want to make use of the techniques we've learned about to encapsulate the component.
  2. We want to create a template with slots for the profile image, name, and email address. We'll give the template the ID fod-profile-template. We will call the custom element for our component fod-profile.

    The avatar placeholder can be found at https://packt.live/2K0jcLT. We want create a profile component similar to the following screenshot:

    Figure 12.25: Design for the fod-profile component

    Figure 12.25: Design for the fod-profile component

  3. To connect the fod-profile custom element to our template with the ID fod-profile-template and to attach the shadowDom, we will copy and paste the following script at the end of the web page:

         <script>

            customElements.define("fod-profile", class FODProfile extends HTMLElement {

                constructor() {

                    super();

                    this.attachShadow({ mode: "open" });

                }

                connectedCallback() {

                    const tmpl = document.getElementById("fod-profile- template");

                    const node = document.importNode(tmpl.content, true);

                    this.shadowRoot.appendChild(node);

                }

            });

        </script>

    Note

    The solution to this activity can be found on page 627.

In this activity, we've learned how to create a reusable web component, but there are still some improvements we can make to the shareable nature of our component.

Sharing a Web Component

The great benefit of web components is that they are reusable and because of the protections that the Shadow DOM and templates afford, we can be fairly certain that they can be used on different websites with ease. This means we can share components and use third-party components with more confidence.

To make our component shareable, we really want to make it something that is self-contained in a single file that can be attached to a web page and then used as a component.

To do this, we can put any scripts in an external file; for example, working from the simple-modal component we created in Exercise 12.06, Template, we can create an external script file called simple-modal.js that wraps the custom element definition in an immediately executing function:

(function() {

    customElements.define("simple-modal", class SimpleModal extends HTMLElement {

        constructor() {

            super();

            this.attachShadow({ mode: "open" });

        }

        connectedCallback() {

            const tmpl = document.getElementById("simple-modal-template");

            const node = document.importNode(tmpl.content, true);

            this.shadowRoot.appendChild(node);    

        }

    });

})();

We still need to include the HTML template for the component in this file. The easiest way to do this is to create the template dynamically and use a template literal to populate it. We then add the template to the document body so that we can use it:

const template = document.createElement("template");

    template.id = "simple-modal-template";

    template.innerHTML = `

    <style>

        .modal-container {

            display: flex;

            align-items: center;

            justify-content: center;

            height: 100vh;

        }

        .overlay {

            position: fixed;

            background: rgba(0, 0, 0, 0.5);

            top: 0;

            bottom: 0;

            left: 0;

            right: 0;

        }

        .modal {

            min-width: 250px;

            position: relative;

            background: white;

            padding: 16px;

            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.8);

        }

        .btn {

            -webkit-appearance: none;

            appearance: none;                    

            background-color: #3700B3;

            border: none;

            border-radius: 2px;

            color: white;

            cursor: pointer;

            min-width: 64px;

            outline: none;

            padding: 4px 8px;

        }

        .btn:hover {

            background-color: #6200EE;

        }

    </style>

    <section class="modal-container">

        <div class="overlay"></div>

        <div class="modal">

            <header class="header">

                <h1><slot name="heading"></slot></h1>

            </header>

            <div class="body">

                <slot name="content"></slot>

            </div>

            <footer class="footer">

                <button class="btn">OK</button>

            </footer>

        </div>

    </section>

    `;

    document.body.appendChild(template);

The template and custom element definition can now be contained in one file, which is easy to add to a web page. For example, to use simple-modal.js, we would add the file and create an instance of the simple-modal element:

    <body>

        <simple-modal>

            <span slot="heading">You've Opened a Modal!</span>

            <p slot="content">

                Finished with the modal? Click OK.

            </p>

        </simple-modal>

    </body>

    <script src="simple-modal.js"></script>

Summary

In this chapter, through multiple exercises and activities, we have looked at the features that have been added to HTML5 that allow us to create web components. We have learned how to create a custom HTML element and how to define the behavior of that custom element. We have also learned about the Shadow DOM and how we can use it to encapsulate our custom elements; in other words, we have learned how to keep our custom elements safe from outside influences and prevented them, in turn, from polluting the rest of a web page. Finally, we have learned how to create HTML templates that make our custom elements more flexible and allows us to reuse components in more situations.

Combining all of these features of HTML5, we have applied our new knowledge to create a modal and a blog-headline element, and we have learned how to create web components that can interact with one another to make reusable, versatile UI components that can be used across multiple projects.

In the next chapter, we will be looking at new web technologies to see exciting and experimental features you may want to work with.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset