How to Create Reusable Web Components

Reusable HTML components help streamline the development process, improve code maintainability, and enhance user experience by ensuring consistency across different parts of a website. By using custom elements, developers can create their own HTML tags with encapsulated functionality, making it easier to manage and reuse code.

Reusable components are essential for maintaining a clean and organized codebase, especially in large-scale projects. 

Getting Started with Custom Elements

Custom elements are a part of the Web Components standard that allows developers to define their own HTML tags. These elements encapsulate functionality, styles, and behaviour into reusable components, ensuring that their functionality and appearance are self-contained and do not interfere with the rest of the application.

Custom elements are defined using the class keyword in JavaScript, and their behaviour is encapsulated using the Shadow DOM. This approach ensures that styles and scripts within a custom element do not affect other parts of the page, allowing for modular and maintainable code.

By extending the capabilities of HTML, custom elements enable the creation of complex and interactive user interfaces. 

Basic Terminology and Concepts

  • Custom Elements: User-defined HTML elements with custom behaviour, created using JavaScript.
  • Shadow DOM: A scoped DOM tree that is attached to an element, encapsulating its internal structure and styles.
  • HTML Templates: A way to define reusable HTML fragments that can be instantiated using JavaScript.
  • Lifecycle Callbacks: Special methods that are called at different points in an element's lifecycle, such as when it is added or removed from the DOM.

Setting Up Your Development Environment

To get started with custom elements, you need a few essential tools and libraries:

  1. Code Editor: Choose a modern code editor like Visual Studio Code, Atom, or Sublime Text.
  2. Browser: Use a modern browser like Chrome or Firefox for testing and debugging.
  3. Node.js and npm: Install Node.js and npm for managing project dependencies.
  4. Web Server: A simple local web server like live-server to serve your files during development.

Setting Up a Simple Project

Open your terminal and create a new project directory:

mkdir custom-elements-project
cd custom-elements-project
npm init -y

Install a Local Web Server, live-server, to serve your project files:

npm install -g live-server

Create the following files in your project directory:

  • index.html: The main HTML file.
  • main.js: The JavaScript file where you will define your custom elements.
  • style.css: The CSS file for styling your custom elements.

In index.html, set up a basic HTML structure.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Custom Elements Project</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <script src="/main.js"></script>
</body>
</html>

Start the Web Server by running in the terminal:

live-server

This command starts a local server and opens your project in the browser. Any changes you make to the files will automatically refresh in the browser.

Creating Your First Custom Element

To define a custom element, you use the class keyword to create a new class that extends HTMLElement. This class defines the behaviour and structure of the new element. Once defined, you register the element using customElements.define, providing a tag name and the class.

Example: Creating a Simple Greeting Element

Let's create a simple custom element called <greeting-element> that displays a greeting message.

In your main.js file, define the GreetingElement class:

class GreetingElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `<p>Hello, World!</p>`;
  }
}

customElements.define('greeting-element', GreetingElement);

In this code, GreetingElement extends HTMLElement, and the constructor attaches a shadow DOM to the element. The shadow DOM contains a simple paragraph with the text "Hello, World!".

In your index.html file, add the custom element:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Custom Elements Project</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <greeting-element></greeting-element>
  <script src="/main.js"></script>
</body>
</html>

When you open index.html in the browser, you will see the greeting message rendered by the custom element.

Advanced Custom Element Features

  • Attributes and Properties
    Attributes and properties allow custom elements to accept external data and configure their behaviour. Attributes are set in HTML and are reflected as properties in JavaScript. To observe attribute changes, use the observedAttributes method.
  • Lifecycle Callbacks
    Custom elements have several lifecycle callbacks, such as connectedCallback, disconnectedCallback, attributeChangedCallback, and adoptedCallback. These methods allow you to execute code at specific points in the element's lifecycle.

Example: Creating an Interactive Button

Let's create a custom button element that changes text when clicked.

In your main.js file, create the InteractiveButton class:

class InteractiveButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        button {
          padding: 10px 20px;
          font-size: 16px;
        }
      </style>
      <button id="btn">Click me</button>
    `;
    this.button = this.shadowRoot.querySelector('#btn');
    this.button.addEventListener('click', this.handleClick.bind(this));
  }

  handleClick() {
    this.button.textContent = 'Clicked!';
  }

  connectedCallback() {
    console.log('InteractiveButton added to the DOM');
  }

  disconnectedCallback() {
    console.log('InteractiveButton removed from the DOM');
  }

  static get observedAttributes() {
    return ['label'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'label') {
      this.button.textContent = newValue;
    }
  }
}

customElements.define('interactive-button', InteractiveButton);

This class includes lifecycle callbacks and handles attribute changes. The handleClick method updates the button text when clicked.

In your index.html file, add the custom button element:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Custom Elements Project</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <interactive-button label="Press me"></interactive-button>
  <script src="/main.js"></script>
</body>
</html>

When you open index.html in the browser and click the button, it will change its text to "Clicked!". The label attribute can be used to set the initial button text.

Styling Custom Elements

The Shadow DOM provides a way to encapsulate the internal structure and styling of custom elements. This encapsulation ensures that the styles and scripts within the shadow DOM do not affect the rest of the page, and vice versa. By using the Shadow DOM, developers can create custom elements with isolated and protected styles, making them more modular and reusable.

To style a custom element, you can include CSS within the shadow DOM. This CSS will only apply to the elements inside the shadow DOM, ensuring that the styles are encapsulated. You can also use external stylesheets by linking them within the shadow DOM.

Example: Styling a Custom Card Component

Let's create a custom card component with encapsulated styles using the Shadow DOM.

In your main.js file, create the StyledCard class:

class StyledCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        .card {
          border: 1px solid #ccc;
          border-radius: 8px;
          padding: 16px;
          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        }
        .card h2 {
          margin-top: 0;
        }
        .card p {
          color: #666;
        }
      </style>
      <div class="card">
        <h2><slot name="title"></slot></h2>
        <p><slot name="content"></slot></p>
      </div>
    `;
  }
}

customElements.define('styled-card', StyledCard);

In this code, StyledCard extends HTMLElement and attaches a shadow DOM. The shadow DOM contains a <style> block that defines the styles for the card component, and the card's HTML structure is defined within the shadow DOM.

In your index.html file, add the custom card element.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Custom Elements Project</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <styled-card>
    <span slot="title">Card Title</span>
    <span slot="content">This is some card content.</span>
  </styled-card>
  <script src="/main.js"></script>
</body>
</html>

When you open index.html in the browser, the custom card element will be styled according to the CSS defined within its shadow DOM. The <slot> elements allow you to pass content into the custom element from the outside, making it flexible and reusable.

Using Custom Elements in Your Projects

To use custom elements in your projects, you need to ensure that the custom element definition is loaded before using it in your HTML. This can be done by importing the JavaScript file containing the custom element class in your HTML file.

Import Custom Elements:

<script src="/main.js" defer></script>

The defer attribute ensures that the script is executed after the HTML document has been completely parsed.

Simply use the custom element as you would any other HTML element:

<styled-card>
  <span slot="title">Card Title</span>
  <span slot="content">This is some card content.</span>
</styled-card>

Best Practices for Reusability

  1. Encapsulation: Use the Shadow DOM to encapsulate styles and structure.
  2. Slots: Use <slot> elements to create flexible content insertion points.
  3. Attributes and Properties: Use attributes and properties to make your custom elements configurable.
  4. Documentation: Document your custom elements to ensure they are easy to use and understand by other developers.
  5. Testing: Write unit tests to ensure your custom elements function as expected in different scenarios.

Example: Integrating Custom Elements into a Web Page

Here's a practical example of integrating a custom element into a web page.

In main.js, define a simple custom element:

class MyButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        button {
          background-color: #008CBA;
          color: white;
          padding: 15px 32px;
          text-align: center;
          text-decoration: none;
          display: inline-block;
          font-size: 16px;
        }
      </style>
      <button><slot></slot></button>
    `;
  }
}

customElements.define('my-button', MyButton);

In index.html, integrate the custom button:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Custom Elements Project</title>
</head>
<body>
  <my-button>Click Me!</my-button>
  <script src="/main.js" defer></script>
</body>
</html>

When you open index.html in the browser, the custom <my-button> element will be displayed with the encapsulated styles. This approach ensures that your custom elements are easy to integrate and use across different parts of your project, promoting reusability and maintainability.

Testing and Debugging Custom Elements

Testing and debugging custom elements is crucial to ensure they work correctly across different scenarios and environments. Here are some tools and techniques to help you:

Unit Testing

  • Jest: A JavaScript testing framework that works well with custom elements.
  • Mocha: Another testing framework that can be used in combination with Chai for assertions.

Example:

import { expect } from 'chai';
import { MyButton } from './main.js';

describe('MyButton', () => {
  it('should display default text', () => {
    const button = new MyButton();
    document.body.appendChild(button);
    expect(button.shadowRoot.querySelector('button').textContent).to.equal('Click Me!');
  });
});

Integration Testing

  • Selenium: Automates browser actions to test the functionality of custom elements.
  • Puppeteer: A Node.js library that provides a high-level API to control Chrome or Chromium for testing.

Linting and Static Analysis

  • ESLint: A tool to analyze your code for potential errors and ensure code quality.
  • HTMLHint: A static analysis tool to check your HTML code for issues.

Browser DevTools

  • Use browser developer tools to inspect and debug custom elements. The Elements panel in Chrome DevTools, for instance, lets you see the shadow DOM and styles of your custom elements.

Common Issues and Solutions

Issue: Custom Element Not Rendering
Solution: Ensure the custom element is defined and the script is correctly loaded. Check for typos in the customElements.define method.

Issue: Styles Not Applied
Solution: Verify that styles are correctly encapsulated within the shadow DOM. Ensure that the shadow DOM is properly attached using this.attachShadow({ mode: 'open' }).

Issue: Attributes Not Reflecting in Properties
Example:

attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'label') {
    this.shadowRoot.querySelector('button').textContent = newValue;
  }
}

Solution: Implement the attributeChangedCallback method to handle attribute changes and reflect them in properties.Issue: Event Listeners Not Working

Issue: Event Listeners Not Working
Solution: Ensure event listeners are attached correctly within the constructor or lifecycle methods like connectedCallback.

Issue: Element Not Updating
Solution: Check if the shadow DOM is being manipulated correctly. Use requestAnimationFrame for updates that need to happen after the DOM is rendered.

Resources and Further Reading

Need a Helping Hand with Your Project?

Whether you need continuous support through our Flexible Retainer Plans or a custom quote, we're dedicated to delivering services that align perfectly with your business goals.

Please enter your name

Please enter your email address

Contact by email or phone?

Please enter your company name.

Please enter your phone number

What is your deadline?

Please tell us a little about your project