A few updates below. For context, the page we’re working on is a search form, allowing the user to cherry pick result items to include in their project/whatever.
We had several stimulus controllers managing the search form itself:
- to verify that required fields were populated before enabling the submit button
- to verify that invalid fields were changed before enabling the submit button
- to perform a logic operation (AND) on the results of the 2 said controllers (required fields and valid fields)
- to clear the error state of an invalid field in response to a change event on that field
We ended up consolidating much of this into a single “FormController”.
Also, we noticed that between these controllers and others, there was a pattern emerging:
- monitor a group of input fields (eg input & change events)
- form a conclusion about the state of something else based on the state of those inputs
- affect that something else
For example (a controller ensuring that required fields are populated before being allowed to submit a form):
- monitor inputs with the
.required
class - detect a blank value of any of those elements
- disable the submit button if any required inputs were blank; enable it otherwise.
Or this one (a badge displaying the count of active filters in a section)
- monitor inputs under a collapsing section header (use targets for this one)
- count the number of non-blank elements
- set the innerText of an outputTarget to equal the count
We extracted a map-reduce style mix-in that can be composed into stimulus controllers, and configured with callback functions for:
- input (return elements to monitor)
- map (transform element into information of interest)
- reduce (combine the array of values of interest into a final answer)
- output (change the state of other elements based on the final answer)
Here’s an example of it being used to implement the required fields controller logic:
useMapReduce(this, {
input: this.requiredFields, // searches the form.elements for fields having `required` class
map: elementValue, // get the value of each element
reduce: this.requiredConditionsSatisfied, // yield true if all values are present?
output: requiredConditionsSatisfied => setButtonEnabled(this.outputTarget, requiredConditionsSatisfied)
})
This ended up reducing a lot of our boiler-plate.
Along with carefully choosing a more course-grained stimulus controller (a single form controller instead of 3 or 4 sewn together), the team is much happier with the maintenance job remaining.