Can't load child controller by target


#1

Codepen example here. If you open that up and look at the console, you’ll see “null” is the first thing that gets output to the console (when it should be a controller).

I’m pretty confused as to why this is not working. In my example, I have a simple structure of a controller with a nested controller inside of it:

<div data-controller="parent">
  <div data-controller="multi-word-child" data-target="parent.child">
    Some text
  </div>
</div>

As you can see, we have a “parent” controller, as well as a nested controller (“multi-word-child”) that is also a target of the “parent” controller.

In the initialize method of the “parent” controller, I have code that uses getControllerForElementAndIdentifier in an attempt to get the “multi-word-child” controller by passing in the “parent.child” target as well as the controller name.

class ParentController extends Stimulus.Controller {
  static targets = ["child"];

  initialize() {    
    console.log(this.getChild(this.childTarget))
  }
  
  getChild(target) {
    return this.application.getControllerForElementAndIdentifier(target, 'multi-word-child')
  }
}

For some reason though, the console.log() above returns null. Does anyone have any insight? I’m fairly lost here. In my “actual” code it’s even weirder, sometimes it works depending on the name of the child controller. Seems very obscure.


#2

@imjohnbon I think your are having a racing issue.
Controllers are evaluated by stimulus from top to bottom of the document. So when Parent controller initializes, children controllers are not yet initialized so they are not visible within the application context and cannot be found by getControllerForElementAndIdentifier().

here is a modified version of your codepen. If you click on the text you will see that the function getChild() works perfectly but it is called too soon in connect/initialize


#3

That would make sense. I was wondering if it was a race condition earlier considering how strange it was. Makes me wish there was some kind of initialize like function that ran once all the initial controllers on the page were fully loaded. Thanks for the help!


#4

That’s mostly true. Both controllers are initialized synchronously because they appear in the DOM at the same time. Here’s a very simplified of what Stimulus does:

[ ParentController, MultiWordChildController ].forEach(controllerConstructor => {
  const controller = new controllerConstructor()
  this.application.controllers.push(controller)
  controller.initialize()
  controller.connect()
})

Your ParentController instance can’t find the MultiWordChildController instance because the forEach loop hasn’t gotten to its next cycle yet.

Switching the registration order would happen to fix it:

 const application = Stimulus.Application.start()
-application.register('parent', ParentController)
 application.register('multi-word-child', MultiWordChildController)
+application.register('parent', ParentController)

I don’t recommend relying on that as a long term fix though since it’s not documented behavior and could change.

Instead, defer accessing the child controller:

initialize() {
  setTimeout(() => {
    console.log(this.getChild(this.childTarget))
  })
}

Or even

initialize() {
  Promise.resolve().then(() => {
    console.log(this.getChild(this.childTarget))
  })
}