DEV Community

Cover image for Create a Timeline component with the help of web components
Keyur Paralkar
Keyur Paralkar

Posted on • Updated on

Create a Timeline component with the help of web components

Introduction

Have you ever wondered what these components are and how components like these are made?

These are called timeline components. In this blog post, we'll explore what exactly these components are and how it's built. Instead of going with the usual React or other famous frameworks, we're gonna build this component with a little twist!

We are going to build it with web components technology. Yes, you heard me right web components!!. But don’t worry we will have a brief look into what web components are and then jump right into building our timeline component.

So, without any more delay, let's get right into it!

What is a timeline component?

A timeline component is a component that helps us to indicate an event’s summary at a particular time interval like below:

MUI timeline component
timeline-component

Features of our timeline component

Let’s first establish the essential features of our timeline component:

  • Our component will be able to align the entire timeline either to the left or right.
  • Each dot in the timeline can be changed from size and color perspective
  • Help component consumers add an arbitrary number of children elements for each timeline item.

What are web components?

Now that we know what we are building, let us understand how we are going to build it.

Web component is a standard that helps you to build re-useable custom elements. These custom elements have their own scope and styles, thus enabling encapsulation. With this, they can be used multiple times in your codebase. This leads to less code collision and your custom element code is unaffected from the rest of your code.

So essentially you will get to do something like the below in your HTML:

<body>
    <mycustomcomponent>Hello DOM</mycustomcomponent>
</body>
Enter fullscreen mode Exit fullscreen mode

Web components are built based on three main pillars:

  • Custom elements
  • Shadow DOM
  • HTML templates

To understand what these concepts are, I would highly recommend you guys to please refer to the MDN docs since these topics are out of the scope of this blog post. They have done a fantastic job of explaining them.

Proposed Architecture

Now we know what web components are and how they can be used to create our own custom element, therefore let us get started with the architecture of our component. This section will outline what we are trying to build.

MUI timeline component
Architecture diagram

The entire architecture of this component is as follows(please refer to the above diagram):

  • We are going to consider our timeline component to be an unordered list i.e. ul HTML element. We extend the features of ul. This HTML element serves exactly our purpose i.e. to show a list. We are just going to change some appearance of this ul element to achieve what we want.
  • Similar to the original unordered list which has li elements inside it, we are going to follow the same approach. Each item in the timeline component will make use of li element internally.
  • The ul element is going to be called as timeline-component
    • It takes in the following props:
      • mode = left | right
        • This prop helps to align all the items to the left or right.
        • This means that all the dots can appear to the left or to the right
  • The li elements will be represented by the component: timeline-item
    • Each timeline item can take an arbitrary number of children elements in it.
    • Each item will implicitly come with a div element that acts as a dot.
    • This component takes in the following attributes:
      • dotcolor: Can be any RGB value or hex value as string.
      • dotsize: larget medium small

So essentially what we want to do is something like below:

<body>
    <timeline-component mode="left">
        <timeline-item dotcolor="red" dotsize="large">
                <span>Event 1</span>
        </timeline-item>

        <timeline-item dotcolor="red" dotsize="large">
                <span>Event 2</span>
        </timeline-item>
</body>
Enter fullscreen mode Exit fullscreen mode

Now we got to know the basic architecture of the entire component let us start building it 😃

Create scaffoldings for the project

Before we start let us create a simple project. This project will have the following files:

  • src
    • index.js
    • styles.css
  • index.html

We should create the above files and directories in the project directory of your choice or you can simply create a codesandbox of vanilla Javascript and start hacking

Implementation details

Timeline-component

First, let us define the structure of our index.html file. Place the following content inside it:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link type="stylesheet" href="./src/styles.css"></link>
  </head>

  <body>
    <div id="app">
    </div>
        <script src="src/index.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

It is a common practice that for creating a web component we use the template element. The advantage of using it is that we can write it inside our HTML file but it won’t get rendered immediately on the actual DOM. We need to define it using javascript APIs.

Let us create a template element like below:

<template id="timeline-component">
      <style>
        ul {
          list-style: none;
          padding-left: 0px;
          margin: 0px;
          position: relative;
        }

        :host([mode="left"]) ul {
          direction: ltr;
          border-left: 2px solid #cacaca;
          border-right: 0;
        }

        :host([mode="right"]) ul {
          border-right: 2px solid #cacaca;
          border-left: 0;
          direction: rtl; 
        }
      </style>
      <ul>
        <slot></slot>
      </ul>
    </template>
Enter fullscreen mode Exit fullscreen mode

This template element is for our timeline-component. From the proposed architecture section, we discussed that we are going to use an unordered list tag ul for this component. So we are going to do the same.

We used the ul element inside the template element and we have placed the slot element inside this tag. Slots are placeholders that take in the markups from light DOM. You can read more about slots from the MDN docs.

We make use of default slots here. Default slots are the slots that don’t have a name to it. It gets all of its nodes from the light DOM that are not slotted elsewhere.

Inside this template element, we add a style tag as well. In its style, we define the styles for the ul element. This style will get loaded whenever the timeline-component is defined. Styles defined here will remain inside the scope of the timeline-component itself. Thus styles from outside do not affect these styles. Hence custom elements help us to achieve scoped styles.

This ul element has base styles as follows:

ul {
    list-style: none;
    padding-left: 0px;
    margin: 0px;
    position: relative;
  }
Enter fullscreen mode Exit fullscreen mode

The style of the ul element is also based on the mode attribute. Whenever this attribute is set as left we want the following CSS to get applied to the ul element:

:host([mode="left"]) ul {
          direction: ltr;
          border-left: 2px solid #cacaca;
          border-right: 0;
      }
Enter fullscreen mode Exit fullscreen mode

And when mode is set to right we want it to have the following CSS:

:host([mode="right"]) ul {
          border-right: 2px solid #cacaca;
          border-left: 0;
          direction: rtl; 
   }
Enter fullscreen mode Exit fullscreen mode

This helps us to move the content along with the dot to either left or right. We define these styles in the template component itself. We make use of the :host CSS selector. This selector is used to select the host node i.e. the <timeline-component> itself.

When mode = left
When mode = left
When mode = right
When mode = right

Now it's time to define this template element. Let us create a file named: index.js inside the src folder and place the below content in it:

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

    const template = document.querySelector("#timeline-component").content;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.cloneNode(true));
  }
}
Enter fullscreen mode Exit fullscreen mode

To define the timeline-component we need to create a class inside the index.js file. The responsibility of this class is to:

  • Find the template element during initialization/page load
  • Create a Shadow root.
  • Attach the template node to this shadow root.

Let us first understand what we will do on page load or during the initialization of the component. As we created a new class called: TimelineComponent we made use of its constructor to do the following things:

  • We extend this component from HTMLElement class.
  • Then we make use of the super function to call all the properties and functions of the HTMLElement.
  • Next, we grab all the content of the template component we created with id: timeline-component.

    const template = document.querySelector("#timeline-component").content;
    
  • Next, we create an openshadowRoot using the below line:

    const shadowRoot = this.attachShadow({ mode: "open" });
    
  • Finally, we append all the contents of the template element to this shadow root:

    shadowRoot.appendChild(template.cloneNode(true));
    

Now that we have our component ready, let us define this custom element. You can do this via calling the define method of the customElements API. This method takes in the name of the component that you or consumer of the component will use inside his HTML and maps it to the above class that we created. We do this as follows:

customElements.define("timeline-component", TimelineComponent);
Enter fullscreen mode Exit fullscreen mode

Timeline-item

This component is used to represent the li element as seen in our proposed architecture. The component will help the consumer to display an arbitrary number of children elements. This element will consist of a div element that corresponds to the dot that we saw earlier. This component has the capability to control the behavior of this dot element.

timeline-item will take in the following props:

  • dotsize: large medium small
  • dotcolor: Can be any RGB value or hex value as string.

Before we start creating and defining this custom element first let us create a template element for the same.

<template id="timeline-item">
      <style>
        li {
          margin-left: 1rem;
          margin-bottom: 2.5rem;
        }
        .dot {
          border: 7px solid #cacaca;
          display: inline-block;
          border-radius: 50%;
          position: absolute;
        }
      </style>
      <li>
        <div class="dot"></div>
        <slot></slot>
      </li>
    </template>
Enter fullscreen mode Exit fullscreen mode

This template element consists of the li element as we have seen in the proposed architecture. This element has a div element that acts as a dot for the timeline-item component. We change the size and color of this dot element with the dotsize and dotcolor attribute.

We again make use of the style element to have scoped styles for this element.

We make use of the default slot mechanism to place all the children elements of timeline-item component in the slot defined above. For example,

<timeline-item dotcolor="blue" dotsize="large">
        <h1>23rd Jan 2023</h1>
        <p>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Est
            aliquid nesciunt reiciendis esse accusamus laudantium, assumenda
            praesentium illo nemo ratione at itaque voluptatum cum. Veniam at
            magnam itaque harum expedita.
        </p>
</timeline-item>
Enter fullscreen mode Exit fullscreen mode

Here the h1 and the p elements will be part of the slot that we defined above.

Now that we have created the template element for this component, let us start off with creating and defining this custom element.

Our pathway to create this component will be similar to that of the . In the same index.js file, create a new class named: TimelineItem and paste the following content inside it:

const DOT_SIZE = {
  small: "7px",
  medium: "10px",
  large: "12px"
};

const DOT_SIZE_TO_POS_MAP = {
  small: "-0.5rem",
  medium: "-0.7rem",
  large: "-0.8125rem"
};

class TimelineItem extends HTMLElement {
  static get observedAttributes() {
    return ["dotcolor", "dotsize"];
  }

  constructor() {
    super();
    const template = document.querySelector("#timeline-item").content;
    const shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.appendChild(template.cloneNode(true));
  }

  attributeChangedCallback(e, oldValue, newValue) {
    const dotElement = this.shadowRoot.querySelector(".dot");
    const modeAttribute = this.parentElement.getAttribute("mode"); // get parent element attribute;
    if (e === "dotcolor") {
      dotElement.style.borderColor = newValue;
    }

    if (e === "dotsize") {
      if (modeAttribute === "left") {
        dotElement.style.borderWidth = DOT_SIZE[newValue];
        dotElement.style.left = DOT_SIZE_TO_POS_MAP[newValue];
      } else if (modeAttribute === "right") {
        dotElement.style.borderWidth = DOT_SIZE[newValue];
        dotElement.style.left = "";
        dotElement.style.right = DOT_SIZE_TO_POS_MAP[newValue];
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Similar to what we did in the timeline-component we do the following in the TimelineItem class:

  • We make use of the constructor, to do the following:

    • Call the super method to initialize all the elements of its base class.
    • Get all the content of the template element we created like below:

      const template = document.querySelector("#timeline-item").content;
      
    • We create an open shadowRoot node and append all the content of the template node in this shadowRoot:

      const shadowRoot = this.attachShadow({ mode: "open" });
      shadowRoot.appendChild(template.cloneNode(true));
      
  • In order for the timeline-item to observe the dotsize and dotcolor attributes, we need to make use of observedAttribute static method that returns the array of attributes to observe. We have to do this as follows:

    static get observedAttributes() {
        return ["dotcolor", "dotsize"];
      }
    

    We make use of this static getter method because we want to perform certain actions when these attributes changes. To perform these actions we make use of the attributeChangedCallback callback. This callback will get executed whenever the observed attributes changes which in this case are dotcolor and dotsize.

    static get observedAttributes() {
        return ["dotcolor", "dotsize"];
      }
    
    attributeChangedCallback(e, oldValue, newValue) {
        const dotElement = this.shadowRoot.querySelector(".dot");
        const modeAttribute = this.parentElement.getAttribute("mode"); // get parent element attribute;
        if (e === "dotcolor") {
          dotElement.style.borderColor = newValue;
        }
    
        if (e === "dotsize") {
          if (modeAttribute === "left") {
            dotElement.style.borderWidth = DOT_SIZE[newValue];
            dotElement.style.left = DOT_SIZE_TO_POS_MAP[newValue];
          } else if (modeAttribute === "right") {
            dotElement.style.borderWidth = DOT_SIZE[newValue];
            dotElement.style.left = "";
            dotElement.style.right = DOT_SIZE_TO_POS_MAP[newValue];
          }
        }
      }
    
    • One thing to note here is that we access the parent element’s mode attribute to style our component. Here the parent component will be timeline-component
    • Whenever the dotcolor changes we set the dot element’s border color to the attribute value.
    • Whenever the dotsize changes we do the following changes:

      • If the parent element’s mode attribute is left then, we set the dot elements border width and its left CSS property.
      • We make use of the DOT_SIZE and DOT_SIZE_TO_POS_MAP maps to manage the borderWidth and position of the element.
      • These maps have the following default values:

        const DOT_SIZE = {
          small: "7px",
          medium: "10px",
          large: "12px"
        };
        
        const DOT_SIZE_TO_POS_MAP = {
          small: "-0.5rem",
          medium: "-0.7rem",
          large: "-0.8125rem"
        };
        
  • Finally, we make use of the define method of the customElements API to define this element like below:

    customElements.define("timeline-item", TimelineItem);
    

Usage

Now that we have implemented these components, it is time for us to use these components. Place the below code in your index.html file:

<timeline-component mode="left">
        <timeline-item dotcolor="blue" dotsize="large">
            <h1>23rd Jan 2023</h1>
            <p>
              Lorem ipsum dolor sit amet consectetur adipisicing elit. Est
              aliquid nesciunt reiciendis esse accusamus laudantium, assumenda
              praesentium illo nemo ratione at itaque voluptatum cum. Veniam at
              magnam itaque harum expedita.
            </p>
            <p>
              Lorem ipsum dolor sit amet consectetur adipisicing elit. Est
              aliquid nesciunt reiciendis esse accusamus laudantium, assumenda
              praesentium illo nemo ratione at itaque voluptatum cum. Veniam at
              magnam itaque harum expedita.
            </p>
        </timeline-item>
        <timeline-item dotcolor="orange" dotsize="medium">
            <h2>Event 2</h2>
            <p>
              Lorem ipsum dolor sit amet consectetur adipisicing elit. Est
              aliquid nesciunt reiciendis esse accusamus laudantium, assumenda
              praesentium illo nemo ratione at itaque voluptatum cum. Veniam at
              magnam itaque harum expedita.
            </p>
        </timeline-item>
</timeline-component>
Enter fullscreen mode Exit fullscreen mode

usage

You can find all the code and the working example mentioned in this blog on this codesandbox.

Summary

So in this way you can build your own timeline component by leveraging the already existing ul HTML element. We also touched some basics of the web component technology: custom elements, slots and templates. But I would highly recommend you guys to go through the MDN documentation for these concepts.

Future work and further reading:

  • You can think about extending this component with functionality like:
    • Creating a custom dot element that accepts a SVG icon, something similar to what MUI did here.
    • Align the timeline component in center.
    • Opposite content functionality.

Thank you for reading!

Follow me on TwitterGitHub, and LinkedIn.

Top comments (1)

Collapse
 
dannyengelman profile image
Danny Engelman • Edited
const template = document.querySelector("#timeline-item").content;
super()
 .attachShadow({ mode: "open" })
 .append(template.cloneNode(true));
```

`
Enter fullscreen mode Exit fullscreen mode