Web Components

Web Components are a powerful web standard that allows developers to encapsulate and package functionality into custom HTML elements. This capability enables the creation of reusable, self-contained components that can work across different frameworks, or with plain JavaScript. The use of Web Components brings the benefit of leveraging browser-native technology, which often results in better performance and lower overhead compared to JavaScript-based solutions.

Our adoption of Web Components is a strategic choice. Their framework-agnostic nature fits well into our development philosophy of minimizing dependencies. We have also found that Web Components are extremely lightweight, allowing us to maintain a smaller codebase and better application performance. Furthermore, the Web Components standard has matured and is well-documented, offering substantial support for developers.

Our Web Component Utilities

In order to make working with Web Components more efficient and developer-friendly, we have created several utility functions, found under src/utilities/web-components.ts:

  • defineComponent: This function is our safe way to define a custom element. It checks whether a component with the given name already exists before attempting to define it, thereby avoiding errors related to redefining an existing component.

  • html: This utility creates a DOM branch from a string template, providing a straightforward way to create DOM elements. Compared to manually creating individual DOM nodes via the DOM API, which can be cumbersome and verbose, this helper function allows for more readable and maintainable code.

  • component: This function is at the heart of our Web Components definition. It provides a simplified and type-safe API for creating Web Components.

Example: Creating a Simple Alert Component

Let's take a look at how we can use these utilities to create a basic Web Component:

import { component } from '~/utilities/web-components';

const Component = component('x-alert')<{ message: string }>((self, attributes) => {
  alert(attributes.message);
});

declare global {
  namespace JSX {
    interface IntrinsicElements extends Id<typeof Component> {}
  }
}

In this example, the component function creates a new Web Component called 'x-alert'. The function takes two arguments: the name of the component and a callback function that gets executed when the component is attached to the DOM. The callback has access to the component's attributes, which are type-checked thanks to TypeScript.

You can then use this new component in your HTML code like this:

<x-alert message="Hello, World!" />

Once this component gets attached to the DOM, it triggers an alert dialog with the message "Hello, World!".

The TypeScript declare global block ensures that our JSX typing is aware of the new component and its attributes, providing a better developer experience with features like autocompletion and error checking.

Components can also clean up after themselves when they are removed from the DOM. This is particularly useful when you register event listeners or create objects that need to be cleaned up when they are no longer needed. Here's how:

const Component = component('x-alert')<{ message: string }>((self, attributes) => {
  alert(attributes.message);
  return () => {
    alert('Goodbye, World!');
  };
});

The cleanup function will be called when the component is removed from the DOM.

Making Components Interactive with Visibility Handling

We can make our components more dynamic by having them react to their visibility on the page. This is where our visible utility comes in handy.

const Component = component('x-alert')<{ message: string }>(async (self, attributes) => {
  await visible(self);
  alert(attributes.message);
});

In this example, the alert dialog only opens when the 'x-alert' component enters the viewport. This visibility-driven action could be leveraged in a variety of ways, from lazy-loading content to animating elements when they come into view.

The visible utility becomes even more powerful when paired with dynamic imports, allowing us to only load large libraries or data when a component is about to be visible on the screen:

const Component = component('x-chart')(async (self, attributes) => {
  await visible(self, { rootMargin: '150px' });
  const chart = await import('chart.js');
  // use chart
});

In this case, the chart library is only loaded when the 'x-chart' component is 150 pixels away from the viewport, optimizing resource usage and improving page load performance.

Web Components offer a flexible and performant way to encapsulate and reuse functionality across different parts of your application. With our utilities and best practices, working with Web Components becomes even more efficient and enjoyable. As you continue exploring this documentation, you'll see how we leverage these principles in different areas of our application.

Powered by Doctave