Techniques Home ~ alanwsmith.com ~ other projects ~ socials

Inter-Component Communication

January 2025

Work In Progress

This is a draft post. Any feedback or ideas for improvement are welcome.

Introduction

This page presents a method to allow multiple instances of a web component to communicate with each other.

Code Example

Here's a full example of a component.js file designed to allow instances of web components to communicate with each other. (It's also what powers the examples on this page.)

class AlansWebComponent extends HTMLElement {

  static count = 0
  static instances = {}

  static increment(instance) {
    this.count += 1
    for (let checkId in this.instances) {
      if (checkId === instance.uuid) {
        this.instances[checkId].update(this.count)
      } else {
        this.instances[checkId].update("-")
      }
    }
  }

  static registerInstance(instance) {
    this.instances[instance.uuid] = instance
  }

  static removeInstance(instance) {
    delete this.instances[instance.uuid]
  }

  constructor() {
    super()
    this.uuid = self.crypto.randomUUID()
    this.attachShadow({mode: 'open'})
  }

  addContent() {
    const template = 
      this.ownerDocument.createElement('template')
    template.innerHTML = `<button>-</button>`
    const content = 
      template.content.cloneNode(true)
    this.shadowRoot.append(content)
  }

  addEventListeners() {
    this.button = this.shadowRoot.querySelector('button')
    this.button.addEventListener('click', (event) => {
      this.handleClick.call(this, event) 
    })
  }

  connectedCallback() {
    this.constructor.registerInstance(this)
    this.addContent()
    this.addEventListeners()
  }

  disconnectedCallback() {
    this.constructor.removeInstance(this)
  }

  handleClick(event) {
    this.constructor.increment(this)
  }

  update(value) {
    this.button.innerHTML = value
  }
}

customElements.define('alans-wc', AlansWebComponent)

The script is included on the page via:

<script type="module" src="component.js"></script>

Example

Four instances of the component are included here. Clicking one of them increments a global counter and displays the number. All the other instances of the component are set to display a dash.

Code

<alans-wc></alans-wc>
<alans-wc></alans-wc>
<alans-wc></alans-wc>
<alans-wc></alans-wc>

Result

Details

The Static Variables And Methods

This approach works by creating two static variables that act as a global counter and list of associated elements.

  static count = 0
  static instances = {}

Three static methods provide the core functionality. The registerInstance() method receives an instance of the component and adds it to the instances object using its UUID as the key.

  static registerInstance(instance) {
    this.instances[instance.uuid] = instance
  }

The increment method increments a counter then searches through the instances object to find one with a matching UUID of the instance it received. An update method is called on each instance with either a dash or the count if the UUID matches.

  static increment(instance) {
    this.count += 1
    for (let checkId in this.instances) {
      if (checkId === instance.uuid) {
        this.instances[checkId].update(this.count)
      } else {
        this.instances[checkId].update("-")
      }
    }
  }

Finally, the removeInstance() method provides the way to remove instances from the instances object.

  static removeInstance(instance) {
    delete this.instances[instance.uuid]
  }

The Instance Methods

The constructor() method preps new instances by generating a UUID, and attaching the shadowRoot.

  constructor() {
    super()
    this.uuid = self.crypto.randomUUID()
    this.attachShadow({mode: 'open'})
  }

The connectedCallback() method is called when new instances are added to the page. It registers the component by calling the static registerInstance() method through this.constructor. Next, it calls the instances addContent() and addEventListeners() methods to set up the element.

  connectedCallback() {
    this.constructor.registerInstance(this)
    this.addContent()
    this.addEventListeners()
  }

The addContent() method is responsible for populating the shadowRoot's HTML.

  addContent() {
    const template = 
      this.ownerDocument.createElement('template')
    template.innerHTML = `<button>-</button>`
    const content = 
      template.content.cloneNode(true)
    this.shadowRoot.append(content)
  }

The addEventListeners() method attaches the instance's handleClick() method to the HTML button. It's set up to use call so the instance itself can be accessed in the static method to get its UUID.

  addEventListeners() {
    this.button = this.shadowRoot.querySelector('button')
    this.button.addEventListener('click', (event) => {
      this.handleClick.call(this, event) 
    })
  }

The handleClick() method sends the instances UUID to the static increment() method through this.constructor.

  handleClick(event) {
    this.constructor.increment(this)
  }

The update method receives a new value and updates the button text to display it

  update(value) {
    this.button.innerHTML = value
  }

Finally, the disconnectedCallback() method fires when an instance is removed from the DOM. It calls the static removeInstance method via this.constructor.

  disconnectedCallback() {
    this.constructor.removeInstance(this)
  }

Conclusion

When I first started looking into this I thought coordinating between instances of a component would require adding a singleton or other object into the mix. Using the static abilities of a class turns out to be enough.

References