Stimulus Discourse

Ensure sibling controller readiness?


#1

Hi there.

The following is a simplified version of what I’m trying to do.

Given the following HTML:

<div data-controller="editor">
  <div data-controller="outlet" data-action="updatePreview->outlet#updatePreview" data-shared-id="10"></div>
  <div data-controller="generator" data-shared-id="10"></div>
</div>

The following outlet controller (CoffeeScript):

# outlet-controller
import { Controller } from "stimulus"
export default class extends Controller
  updatePreview: (event) ->
    @element.innerHTML = event.detail.preview

And the following generator controller (CoffeeScript):

# generator-controller
import { Controller } from "stimulus"
export default class extends Controller
  @property 'sharedId',
    get: ->
      @element.dataset.sharedId

  @property 'outlet',
    get: ->
      if (foundOutlets = document.querySelectorAll("[data-controller='outlet'][data-shared-id='#{@sharedId}']")).length
        foundOutlets[0]

  connect: ->
    @outlet.dispatchEvent(new CustomEvent('updatePreview', detail: { preview: "<strong>Hiya</strong>" }))

Even though I’m certain the outlet controller is initialized and connected before the fields controller. When the fields controller connects and emits the updatePreview event, the outlet controller isn’t ready to do anything with the message.

If I add a setTimeout to the generator controller’s connect, this works as expected.

Is there a better way to handle this? I’ve tried using the getControllerForElementAndIdentifier method in the generator controller’s connect method (To call updatePreview directly on the outlet controller) and also learned it couldn’t find the outlet’s controller.

How might I delay the generator controller’s attempt to send a preview update until I’m sure everything is ready?

Thank you!


#2

You have to ensure the receiving controller is connected and setTimeout is a perfectly valid way to do that. Another solution is to wait on an empty promise:

connect: ->
  Promise.resolve().then =>
     @outlet.dispatchEvent(new CustomEvent('updatePreview', detail: { preview: "<strong>Hiya</strong>" }))

#3

Thanks for the reply. I was banging my head against the wall on this.

I took a look at the “Cant load child controller by target” post you referenced, and modified the codepen (https://codepen.io/anon/pen/mvRxEQ?editors=1010) mentioned, using the Promise strategy you referenced above. It does indeed fix the issue.

How in the world does this work though!? How does creating and immediately resolving a promise ensure that the child controller is ready?

It’s described (In a simplification) that the controllers are initialized in a loop and that the parent couldn’t find the child because the loop hadn’t reached the child yet to initialize it. But wouldn’t the Promise fired in the parent’s initialize still be a part of its turn in the loop?

Thanks again!


#4

You’re welcome! I have to admit that I’m no expert on microtask queues but Sam touches it briefly here:

Stimulus responds to document changes in the next tick of the microtask queue .

In JavaScript, the easiest way to queue a microtask is to wait on an empty promise

What I think is happening, is that the loop which is responsible for registering the controllers is a part of the current tick of the microtask queue. According to this (incredibly well written) guide, any additional microtasks queued during microtasks are added to the end of the queue. This explains why the loop gets to finish registering the controllers before your empty promise (which is enqueued as a microtask) is resolved.


#5

Brilliant. Thanks so much.