Targetless Controllers New

by Julian Rubisch

In general, there are two types of controllers:

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 targets 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.