Refactored Telegram

It’s all just ones and zeros under the cover

Communication Among Stimulus Controllers: Part 1

This is part one of a two part series on Stimulus controller communication. The link to part two will be located here once it has been published.

From the time I’ve started playing around with Stimulus, I’ve been thinking about the best way that controllers can communicate with each other.

The nice thing about Stimulus is that the state of the controller is representable from within the DOM itself, meaning that if a property of a controller needs to change, it can be done so by simply modifying the attribute value of the element the controller is bound to (see here for how that works).

This, however, doesn’t extend nicely to instances when you only need to notify the controller of an event or send a message that is more one shot: “I did a thing”, or “please do that thing”. It’s not always possible to translate these interactions to one that is based on modifying attribute values, and most attempts to do so always end up as a bit of a code smell.

After a bit of experimentation, I think I’ve found a method which works for me.

Existing Solutions

Stimulus does not have a way of doing this using their API, at least at the time this post was written. I’ve found some examples of how to achieve this using the private API which, although they may work, I found slightly ugly. In general, I try to avoid dealing with the unexposed inners of a library, even if it’s implicitly available to me1. Doing so usually results in constant maintenance of the integration, as the original library authors assume that they are free to change or refactor the internals as they see fit. It then falls on you to keep up, and fix any breakages as they occur.

One could argue that going to these extremes indicate that support for this form of communication is missing in the library. This might be true, but the fact remains that it’s not available to me now. So an alternative will have to be considered.

The Approach

For a while I’ve been wondering this could be done at all. But after a bit of playing around, I’ve settled on a solution that uses Custom Events — the ability to create, and receive, events that are completely user definable — to achieve this.

Custom events have have some nice properties that work well with Stimulus:

  1. It’s already available, and well supported by modern browser.
  2. It works with the Stimulus data-action attribute.
  3. It doesn’t assume any internal workings of Stimulus itself.

The general approach is as follows: a controller that wants to send a message to another controller creates a custom event, and sends it using the dispatchEvent() method on a DOM element:

element.dispatchEvent(new CustomEvent("eventName", {
  detail: {
	// optional details
  },
}));

A custom event is made by calling the CustomEvent constructor and providing it with an event name. Data can be associated with the event by putting it inside the detail object.

Controllers interested in receiving the event can use data-action to register a handler, just like any other browser event. The optional details can be retrieved through the detail attribute on the event object passed into the handler:

<div data-controller="listener" data-action="eventName->listener#eventHandler">...</div>
export default class  extends Controller {
  eventHandler(ev) {
	let details = ev.detail;
	
	// Handle the event
  }
}

That’s pretty much all there is to it. The only thing that changes is which element actually dispatches the event, along with some specifics on how the event is created. These relate closely on the relationship of the various controllers, which are explored in the following sections.

Pattern 1: From Child To Parent Controllers

This pattern applies for events that are sent from the child to the parent. There are some cases where this can come in useful, such as when a child controller wants to propagate the value of input fields to the parent controller bound to a form, potentially useful when determining whether it’s safe to submit it.

First, it’s probably important to highlight what this is not. It’s not simply referring to the parent controller in an element nested within a child controller:

<div data-controller="parent">
  <div data-controller="child">
	<!-- The child is not sending the event.  The button is contacting the parent directly -->
    <button data-action="click->parent#doSomething">Do Something</button>
  </div>
</div>

The child controller is available but the actual handler in the example above is actually handled by the parent. As such, the child is not notifying the parent of the event: the button element is.

Instead, this pattern is for cases whereby the child controller itself sends the event, and it works when the event is configured to “bubble up”.

Event bubbling is a mechanism of event propagation whereby the browser will pass the event up through the DOM, running each of the event handlers that are capable of handling the event. This means that when an event is dispatching from the element bound to the child controller, and that event is configured to bubble up, it will pass through he parent controller on it’s way up to the root of the DOM. The parent just needs to listen for it by setting up a data-action handler.

Here’s an example:

<div data-controller="parent" data-action="messageFromChild->parent#handleMessage">
  <span data-cp-parent-target="message"></span>
	
  <div data-controller="child">
  	<button data-action="child#sendMessageToParent">Send Message</button>
  </div>
</div>

The child:

export default class extends Controller {
  sendMessageToParent() {
	this.element.dispatchEvent(new CustomEvent("messageFromChild", {
	  detail: {
		message: "Hello from child"
	  },
	  bubbles: true		// This needs to be explicitly set to enable event bubbling
	}));
  }
}

The parent:

export default class  extends Controller {
  static targets = ["message"];

  handleMessage(ev) {
	console.log("Received event");
	let message = ev.detail.message;
	
	this.messageTarget.textContent = `Message from child: ${message}`;
  }
}

In this example, clicking the button will cause the child controller to dispatch the messageFromChild event. That event will bubble up the DOM and be caught by the handleMessage event handler configured on the parent. The parent is free to react to the event and display the message sent from the child.

The parent handler can also include a call to the stopPropogation() method on ev to prevent the event bubbling further up the DOM if necessary.


In the next part, the patterns of communication from parent to child controllers, and from two controllers unrelated to each other will be explored. A link to the next part will be placed here once it has been published.


  1. This is easy in a language like JavaScript with relatively weak boundaries between modules. ↩︎