Multiple controllers, one element
data-controller isn't limited to one identifier. Write two, separated by a space, and both controllers attach to the same element independently, each with its own targets, values and lifecycle. Neither knows the other exists unless something explicitly connects them. This app's real challenge UI, the Ruby editor on this course's own challenge lesson, does that.
Two real controllers sharing one element
The opening tag of the challenge block in app/views/catalog/lessons/show.html.erb:
<div class="mt-16 pt-12 border-t border-subtle"
data-controller="execution--irb execution--hints"
data-action="execution--irb:failed->execution--hints#attemptFailed"
data-execution--irb-challenge-id-value="<%= @challenge.id %>"
data-execution--hints-url-value="<%= execution_api_v1_challenge_solution_path(@challenge) %>"
data-execution--hints-failed-attempts-value="<%= @failed_attempts %>"
data-execution--hints-required-attempts-value="<%= Execution::SolutionAccessPolicy::REQUIRED_FAILED_ATTEMPTS %>"
data-execution--hints-revealed-value="<%= @solution_revealed %>">
That data-action line is the wiring that connects the two controllers. How it works is lesson 4's subject. For now, notice only that it's real, it sits on this exact element, and neither controller had to be told about it. One <div> carries two controller identifiers in data-controller: execution--irb and execution--hints. Stimulus instantiates both against this same element. execution--irb gets its own this.element (this div), and so does execution--hints: the same DOM node, two separate controller instances with no relationship to each other by default.
Each controller reads only its own values
The value attributes are prefixed per controller: data-execution--irb-challenge-id-value belongs to execution--irb, and data-execution--hints-failed-attempts-value belongs to execution--hints. The course "StimulusJS: values/classes" covered how a value's HTML attribute name is derived from its controller's identifier. That is why two controllers can share one element without their values colliding. execution--irb's challengeId value and execution--hints's url value live in differently prefixed attributes on the same tag, and each controller ignores the other's declarations.
Why put them on the same element at all
execution--irb runs the student's code in the browser, in ruby.wasm, as a preview, then submits it; the grade comes from the server, and the controller learns whether the attempt passed or failed from that verdict. execution--hints owns the hint reveal and the solution gate, the "hints unlock after failed attempts" mechanic a student sees on this course's own challenge lesson. Those are two separate responsibilities, and splitting them into two controllers keeps each one small. execution--irb never touches the hint DOM, and execution--hints never runs or submits code. Sharing one element is what makes lesson 4's mechanism possible. An event dispatched from one controller's this.element reaches anything listening on that same element, so no global broadcast is needed when both controllers live on the same node.
Key takeaways
data-controlleraccepts several space-separated identifiers; each one gets its own independent controller instance attached to the same element.- This app's real challenge UI (
show.html.erb) attachesexecution--irbandexecution--hintsto the same<div>, the element behind this course's own challenge. - Each controller's
static valuesattributes stay prefixed by its own identifier, so two controllers on one element never collide over attribute names. - Splitting
execution--irb(running code) fromexecution--hints(hint and solution UI) keeps each controller's responsibility narrow, even though both act on one shared element.
Keep going with lesson 2
this.dispatch(): a controller's own custom events
How the inherited dispatch() method fires custom events, with defaults for target, detail, bubbling and prefix.
A free account also gives you the quizzes, your progress and notes, and the discussion.