Idea for simpler parent/child relationships

I’m attempting to refactor our bookmarks code to use stimulus.js, and there’s just a couple edge cases where something needs to happen that concerns the state of all bookmarks (first bookmark added in a list of products, last bookmark removed in a list of products).

Also, someone had a similar pain point when making a todomvc example.

The most widely recommended strategy is to use custom events to hook everything together (here’s a nice example), but experience tells me event-based control flow should be a last resort.

Since the examples of needing to communicate between controllers I have, and have seen so far, is just managing changes in a list, what about a “Supervisor” that would let me do this:

import { Supervisor } from "stimulus"

export default class extends Supervisor {
  static supervise = [ "bookmark" ]

  bookmarkAdded(bookmarkController) {
    // I don't actually need this since the user can't add products to
    // bookmark, but other people want this
  }

  bookmarkRemoved(bookmarkController) {
    // I don't actually need this since the user can't add products to
    // bookmark, but other people want this
  }

  // I wouldn't need these parameters for my use case, but it makes sense
  // in general
  bookmarkChanged(bookmarkController, _attribute, _from, _to) {
    // `this.bookmarkList` is automatically provided by the Supervisor class
    // to reference the supervised controller instances
    const numberOfBookmarks =
      this.bookmarkList.filter(e => e.isBookmarked).length

    if (bookmarkController.isBookmarked && numberOfBookmarks === 1) {
      // The first bookmark for a list of products.
      // I have special stuff to do
    } else if (numberOfBookmarks === 0) {
      // No more bookmarked products on the page.
      // I have special stuff to do
    }
  }
}

Two notes. I like ‘Supervisor’ as a name because anything “parent/child” implies a relationship that might not exist for some reasonable use cases. A counter in the user’s profile tab, for example, doesn’t have any authority over the items its counting. ‘Observer’ would be an even better name, maybe, but that’s very similar to ‘MutationObserver’. Maybe more similarity would be a good thing?

Also, I considered implementing my own version of this using MutationObserver on body, it wouldn’t be too difficult to at least stand something up. However, getting it to be performant might be challenging. I suspect you don’t want a bunch of MutationObservers observing the whole DOM, which I assume stimulus.js already does. Which is why stimulus.js is probably in the best position to provide such a class, it’s already watching for changes to controllers, and Controller#application has access to all the controllers for getControllerForElementAndIdentifier to work, right?

I started to dig in to the code, and there’s a lot of subclassing and delegating going on, so the answers to these questions weren’t immediately obvious.

I’m going to implement my use case with events for now, but I’m excited to have a clearer way to do stuff with lists.

1 Like

A data-outlet attribute might be on the drawing board, allowing you to listen for changes on child controller properties in parent controllers.

I wouldn’t worry so much about performance. In our testing, the overhead of mutation observers is negligible.

@woahdae I have bundled a small package that I have extracted from several projects to manage Parent/Child controllers with just basic conventions

All controllers must be extended from the stimulus-conductor controller and the
convention to follow is that:

  • item controllers are conducted by an items controller
  • bookmark controllers are conducted by a bookmarks controller

and respective controllers have the following instance method available:

  • this.bookmarkControllers :point_right: an array of all children controllers
  • this.bookmarksController :point_right: the parent controller