Create a Timeline component with the help of web components
Tags: webdev, javascript, programming, beginners
Published: 2023-08-28 04:08:44

## Introduction

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

| <figure> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8w01nv97mthywbqrftsj.png" alt="Flowbite timeline component" style="width:100%"> <figcaption align = "center"><a href="https://www.flowbite-react.com/docs/components/timeline">Flowbite</a></figcaption> </figure> 	| <figure> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mue9din6u3fqp3z734rb.png" alt="MUI timeline component" style="width:100%"> <figcaption align = "center"><a href="https://mui.com/material-ui/react-timeline/">MUI</a></figcaption> </figure> 	|
|---	|---	|

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:

<figure> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v7a6172zdc8bq13bxyx7.png" alt="MUI timeline component" style="width:100%"> <figcaption align = "center">timeline-component</figcaption> </figure>

## 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:

```css
<body>
	<mycustomcomponent>Hello DOM</mycustomcomponent>
</body>
```

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](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) 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.

<figure> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/26n9pgrsl9bzfyi3fwfk.png" alt="MUI timeline component" style="width:100%"> <figcaption align = "center">Architecture diagram</figcaption> </figure>

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:

```html
<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>
```

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](https://codesandbox.io/) 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:

```html
<!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>
```

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:

```html
<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>
```

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](https://stackoverflow.com/a/42093747). You can read more about slots from the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot).

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:

```css
ul {
    list-style: none;
    padding-left: 0px;
    margin: 0px;
    position: relative;
  }
```

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:

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

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

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

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](https://developer.mozilla.org/en-US/docs/Web/CSS/:host). This selector is used to select the host node i.e. the `<timeline-component>` itself. 

| <figure> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdsqnt5m115o13352p4k.png" alt="When mode = left" style="width:100%"> <figcaption align = "center">When mode = left</figcaption> </figure> 	| <figure> <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kbmf6gv7kmk5djoz4eey.png" alt="When mode = right" style="width:100%"> <figcaption align = "center">When mode = right</figcaption> </figure> 	|
|---	|---	|

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:

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

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

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`.
    
    ```jsx
    const template = document.querySelector("#timeline-component").content;
    ```
    
- Next, we create an open`shadowRoot` using the below line:
    
    ```jsx
    const shadowRoot = this.attachShadow({ mode: "open" });
    ```
    
- Finally, we append all the contents of the template element to this shadow root:
    
    ```jsx
    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:

```jsx
customElements.define("timeline-component", TimelineComponent);
```

### 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. 

```jsx
<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>
```

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,

```jsx
<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>
```

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:

```jsx
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];
      }
    }
  }
}
```

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:
        
        ```jsx
        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:
        
        ```jsx
        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:
    
    ```jsx
    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`.
    
    ```jsx
    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:
            
            ```css
            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:
    
    ```jsx
    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:

```html
<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>
```


![usage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8v6begsqmpp4ecrtkjrj.png)

You can find all the code and the working example mentioned in this blog on this [codesandbox](https://codesandbox.io/s/timeline-prototype-cspvlv).

## 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](https://mui.com/material-ui/react-timeline/#customization).
    - Align the timeline component in center.
    - Opposite content functionality.

Thank you for reading!

Follow me on [Twitter](https://twitter.com/keurplkar), [GitHub](https://github.com/keyurparalkar), and [LinkedIn](https://www.linkedin.com/in/keyur-paralkar-494415107/).