Targetless Controllers New
by Julian Rubisch
In general, there are two types of controllers:
- those that act on the element the controller is attached on itself,
- those that act on one or several
target
s.
You should avoid mixing the two.
<form data-controller="form"> <span data-form-target="indicator"></span> <input type="number" data-action="change->form#submit" /> </form>
Bad
// form_controller.js import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static targets = ["indicator"]; submit() { this.indicatorTarget.textContent = "Saving..."; this.element.requestSubmit(); } }
Good
<form data-controller="form form-indicator" data-action="submit->form-indicator#display"> <span data-form-indicator-target="indicator"></span> <input type="number" data-action="change->form#submit" /> </form>
// form_indicator_controller.js import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static targets = ["indicator"]; display() { this.indicatorTarget = "Saving..."; } } // form_controller.js import { Controller } from "@hotwired/stimulus"; export default class extends Controller { submit() { this.element.requestSubmit(); } }
Rationale
As already hinted above, mixing targetless controllers with “targetful” controllers is a smell that points at a possible Single Responsibility violation.
If you’re unsure, ask “What would be reasons for this controller to change”? If you come up with one for the target
s and one for this.element
, that’s an instance of divergent change, and you should decouple it into two or more controllers, and use outlets or events to communicate between them.
In the improved example above, this is done by splitting the controller responsible for the indicator from the form controller. A form element emits submit
whenever requestSubmit
is called, so we can utilize this for triggering the display of the “Saving” indicator.