Stimulus Discourse

Identify stimulus controller from element?

I have nested Stimulus controllers for a list and the nested list items. I am triggering an event based on clicking one of the list items, that I want run by my outer (list) controller, and in it I need access to the targets from the inner (list item) controller. In reading Having an outer controller that controls inner controller and some other posts, I figured out one way to identify the inner controller with:
`this.application.getControllerForElementAndIdentifier(event.currentTarget, “list-item”)’

But, it feels like I shouldn’t have to query the entire application when I already have the specific element. Is there a way to identify the specific Stimulus controller based on the element directly, vs. needing to use getControllerForElementAndIdentifier?

My abbreviated HTML is:

<ul data-controller="list">
  <li data-controller="list-item" data-action="click->list#trigger">
    <p data-target="list-item.text"></p>
  </li>
</ul>

And I want to be able to identify the text from the inner list-item.text target but from the list/outer controller’s trigger function.

Thank you!!

2 Likes

You could make that element a target of both controllers:

<ul data-controller="list">
  <li data-controller="list-item" data-action="click->list#trigger">
    <p data-target="list-item.text list.itemText"></p>
  </li>
</ul>
// list_controller.js
export default class extends Controller {
  static targets = [ "itemText" ]
  
  trigger(event) {
    const target = this.itemTextTargets.find(element => element == event.currentTarget)
    console.log("text target", target)
  }
}

If you feel like sharing your relevant controller code, I might be able to offer more suggestions.

Thanks! That’s an interesting idea. It does seem that I can actually have only a list controller and not a list item controller that way, but there’s some duplication that I’m not sure about. I actually need to target a bunch of different elements from within the list item, so this works but feels inefficient:

<ul data-controller="list">
  <li data-action="click->list#trigger">
    <img src="..." data-target="list.itemImage"></img>
    <h1 data-target="list.itemHeading"></p>
    <p data-target="list.itemText"></p>
  </li>
</ul>
// list_controller.js
export default class extends Controller {
  static targets = ["itemImage", "itemHeading", "itemText"]
  
  trigger(event) {
    const image = this.itemImageTargets.find(element => element.parentElement == event.currentTarget)
    const heading = this.itemHeadingTargets.find(element => element.parentElement == event.currentTarget)
    const text = this.itemTextTargets.find(element => element.parentElement == event.currentTarget)
    
    // Do some things with the image, heading, and text
  }
}

Vs. my 2-controller way of doing it was:

<ul data-controller="list">
  <li data-controller="list-item" data-action="click->list#trigger">
    <img src="..." data-target="list-item.image"></img>
    <h1 data-target="list-item.heading"></p>
    <p data-target="list-item.text"></p>
  </li>
</ul>
// list_item_controller.js
export default class extends Controller {
  static targets = ["image", "heading", "text"]
}
// list_controller.js
export default class extends Controller {
  trigger(event) {
    const entryController = this.application.getControllerForElementAndIdentifier(event.currentTarget, "list-item");
    const image = entryController.imageTarget
    const heading = entryController.headingTarget
    const text = entryController.textTarget
    
    // Do some things with the image, heading, and text
  }
}

Any thoughts about these different approaches, or if there’s another way I’m not thinking of? Thanks so much!