lamplightdev

How to pass data between Web Components?

A common question to those first experimenting with Web Components is how to communicate between different components on the same page. It might seem at first sight that the very thing Web Components are designed for - encapsulating DOM, CSS and behaviour inside of discrete elements - means that they are not suited to communicating outside their boundary with their fellow elements. But passing data is just a matter of using the standard JavaScript event model you’re already familiar with.

Part of the confusion may be due to the fact this topic is often left out of introductions to Web Components. Articles understandably concentrate on all the new functionality that Web Components provide, but the other great advantage they have is that they’re just an extension to the existing native web platform - all the techniques and APIs (old and new) of the web are available to you when building components.

JavaScript Event Model in Web Components

The native event model of the web consists simply of the dispatch and listening to of events using dispatchEvent and addEventListener, and the same methodology is used with Web Components.

A CustomEvent is a user defined event with a given name that can be sent from the component to the containing DOM using dispatchEvent:

// ...

onClick() {
  const myEvent = new CustomEvent('x-increment', {
    bubbles: true,  // bubble event to containing elements
    composed: true, // let the event pass through the shadowDOM boundary
    detail: {       // an object to hold any custom data that's attached to this event
      amount: 1,
    }
  });

  this.dispatchEvent(myEvent);
}

// ...

Note the composed property which needs to be set to enable the event to ‘escape’ the encapsulation of the ShadowDOM. Without this, the event would still be propagated up inside the shadowDOM, but no further.

The x-increment event can then be captured by any containing element using addEventListener, and any data extracted and used directly, or passed down to a child component:

// ...

constructor() {
  super();

  // attach shadowDOM here

  this.addEventListener('x-increment', (event) => {
    // extract data
    const { amount } = event.detail;

    console.log(amount); // 1

    // pass down to a child
    this.shadowRoot.querySelector('x-target').amount = amount;
  });
}

// ...

Managing state with a container component

A useful pattern to follow is to have a containing component that manages the application state by capturing child events and passing data back down the hierarchy. In this way state is always passed down the component tree in a uni-directional manner (a.k.a. one-way data binding) in a way that may be familiar from other UI frameworks.

Here’s a basic example that shows the above techniques in action to provide a simple counter. It consists of a x-controls component to hold the inputs, a x-counter component to show the output, and a x-container component to manage the application state and pass it down as properties to the other components.

In upcoming posts we’ll have a closer look at ways the above application can be improved by generalising the handling of properties, using a templating library to declaratively update the DOM and attach event handlers, managing styles in web components, and much more.

If you have any feedback or questions I’d love to hear them @lamplightdev.