Hey folks,
Hopefully this is the right place for this sort of question, if not I'm happy to move it.
I've been playing around with Stimulus recently and wanted to see if the way I'm thinking of using it is by design.
So in most component-based frameworks, if I want to change part of the page that is being managed by some component, then I need to do that within the component hierarchy. One of the attractive things about Stimulus seems to be that you can achieve the same thing by just setting up a new Controller for that part of the page.
Concrete example:
I have a component that renders a form and validates it on submit.
In React this might look something like:
<Form>
<input type="text" name="myField" onChange={ this.onChange } />
<button type="submit" onClick={ this.onSubmit }>Save</button>
</Form>
This form and a few others in my app need to format a field as a phone number. So this might be accomplished by extracting a special component for phone number fields
<Form>
<PhoneInput onChange={ this.onChange }></PhoneInput>
<button type="submit" onClick={ this.onSubmit }>Save</button>
</Form>
By contrast, with Stimulus it appears that you would be able to simply add the new behavior as a Controller that would be used in addition to existing ones on the page. In other words, you could add the formatting behavior without making any changes at all to the file with the validating behavior - even though they would both be working with the same field.
For example:
First step: just form validations
/views/users/_form.html.erb
<%= form_with(model: user, local: true, data: { controller: "form" }) do |form| %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name, id: :user_name, data: { target: "form.required_field" } %>
</div>
<div class="field">
<%= form.label :phone_number %>
<%= form.text_field :phone_number, id: :user_phone_number, data: { target: "form.required_field" } %>
</div>
<div class="actions">
<%= form.submit data: { action: "form#validate" } %>
</div>
<% end %>
/src/controllers/form_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
validate(event) {
this.validateRequiredFields()
if (!this.valid) {
event.preventDefault()
}
}
private
get valid() {
return this.data.valid
}
set valid(value) {
this.data.valid = value
}
validateRequiredFields() {
let anyEmpty = false
this.targets.findAll("required_field").forEach((element) => {
let isEmpty = element.value === ""
element.parentElement.classList.toggle("field_with_errors", isEmpty)
element.parentElement.classList.toggle("field", !isEmpty)
if (isEmpty) {
anyEmpty = isEmpty
}
})
this.valid = !anyEmpty
}
}
Then if I want to add phone formatting, I can do so without changing that Controller that is handling form validations.
I just add a new controller to the specific part of the page where I need it:
/views/users/_form.html.erb
<div class="field" data-controller="format">
<%= form.label :phone_number %>
<%= form.text_field :phone_number, id: :user_phone_number, data: { target: "form.required_field", action: "format#phoneNumber" } %>
</div>
/src/controllers/format_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
phoneNumber(_event, target) {
target.value = target.value.split(" ").join("-")
}
}
Does this sound right?
In practice, would you expect to see multiple controllers handling different types of behaviors on a page or do you tend to organize things differently?
Does this sound right?
Definitely. It's perfectly reasonable to connect multiple controllers to the same element. We describe this pattern of usage almost exactly in the handbook:
Stimulus works by continuously monitoring the page, waiting for the magic
data-controllerattribute to appear. Like theclassattribute, you can put more than one value inside it. But instead of applying or removing CSS class names,data-controllervalues connect and disconnect Stimulus controllers.Think of it like this: in the same way that
classis a bridge connecting HTML to CSS,data-controlleris a bridge from HTML to JavaScript.
Great question!
Most helpful comment
Definitely. It's perfectly reasonable to connect multiple controllers to the same element. We describe this pattern of usage almost exactly in the handbook:
Great question!