Finding targets within another target

Is the use of querySelector discouraged in favor of always using targets? I’m having trouble understanding how to select groups of targets based on being inside, say, another target.

Given this markup:

<div data-controller="quiz">
  <div data-target="quiz.question">
    <input type="radio" data-target="quiz.answer" data-action="quiz#answer" />
    <input type="radio" data-target="quiz.answer" data-action="quiz#answer" />
  </div>
  <div data-target="quiz.question">
    <input type="radio" data-target="quiz.answer" data-action="quiz#answer" />
    <input type="radio" data-target="quiz.answer" data-action="quiz#answer" />
  </div>
</div>

When triggering quiz#answer, I want to select all quiz.answer targets within the same quiz.question. Is this possible with targets or do I have to use querySelectorAll?

2 Likes

I had a similar case, what I ended up doing was finding the closest question e.target.closest(".question") and then a querySelector. I don’t think the use of querySelector is discouraged when necessary.

Stimulus does rely on querySelectors anyhow -> Are Targets always in document order?

1 Like

HTML:

<div data-controller="quiz">
  <div data-controller="question" data-target="quiz.question" data-action="answered->quiz#checkQuestion">
    <input type="radio" data-target="question.answer" data-action="question#answer" />
    <input type="radio" data-target="question.answer" data-action="question#answer" />
  </div>
  <div data-controller="question" data-target="quiz.question" data-action="answered->quiz#checkQuestion">
    <input type="radio" data-target="question.answer" data-action="question#answer" />
    <input type="radio" data-target="question.answer" data-action="question#answer" />
  </div>
</div>

quiz_controller.js

import {Controller} from 'stimulus';

export default class Quiz extends Controller {
  static targets = ['question'];
  answered(e){
    let q = this.application.getControllerForElementAndIdentifier(e.currentTarget, 'question');

  }
}

question_controller.js

import {Controller} from 'stimulus';

export default Question extends Controller {
  static targets = ['answer'];
  answer(){
    this.element.dispatchEvent('answered');
  }
}

Maybe try to split your structure to 2 different controllers - quiz and question?

I almost every time end up with similar structure when i need to connect controller instances in parent>child relation. You have 2 controllers - ‘quiz’ and ‘question’. Question dispatch event ‘answered’ to quiz when answer is changed (question#answer). On quiz#answered i used method to find question instance on e.currentTarget (our changed question). I know - this is overkill but i find it useful when you have more complicated HTML structures. Maybe someone from Stimulus creators will figure out how to solve this problem in nicer manner :slight_smile:

3 Likes

Yeah I suppose splitting it into multiple controllers makes sense! Thanks.

I’d be happy to hear what the Stimulus creators think about this as well. As you say, splitting into two controllers might be overkill sometimes.

1 Like

I’ve run into this problem a bunch of times. The way I end up solving it most of the time is to use find to find the child target. Using OP’s code as an example, I’d do:

// Let's say I already have a specific question selected somehow. Maybe a click event on the quiz tells me which one to use, or maybe I'm looping over all questions, or something else. Regardless, question is defined
const question = ...
const answerTargetsForThisQuestion = this.answerTargets.find((answerTarget) => question.contains(answerTarget))

I like doing that because it leverages the Stimulus targets. However, I think it would be more efficient to do something like question.querySelector("data-target='quiz.answer'"). But I don't want to do that, because it means I have to know about the Stimulus API, which we've seen changes, because this is already incorrect for new Stimulus versions. The new version would be question.querySelector(“data-quiz-target=‘answer’”)`

I think it would be great if Stimulus provided a helper function to do that for us, like question.childTarget("answer").

Also, here’s a Stackoverflow talking about this same thing: javascript - Stimulus nested (child) targets - Stack Overflow