This week I installed StimulusJS into a Rails app that I've been developing. So far it seems like a good fit, helping me to organize and re-use Javascript functions without the hassle and overhead of a large framework. I'm a Rubyist at heart, so this minimal approach to Javascript fits me better than some of the other currently popular options. At least, that's my suspicion after using it for a few days.
If you haven't used StimulusJS, it revolves around controllers written in Javascript. These controllers access HTML elements specified by data attributes:
-
data-controller
indicates which controller contains the relevant JS code, -
data-target
indicates that an HTML element is relevant to the JS code and gives it a name, -
data-action
indicates that a change to this element triggers a specific controller action.
Here's an example similar to one that I coded yesterday. Our business uses a variety of billing contract types, each with its own monthly cost. There is a contract type model that contains the default cost for contracts of that type. In my form for making a new contract, I wanted the monthly_cost
of the Contract
to change to the default_cost
of the ContractType
whenever a new ContractType
is selected.
<!-- contracts/new.html.erb (not working) -->
<form data-controller="contracts">
<select name="contract[contract_type]" id="contract_contract_type"
data-target="contracts.contractType" data-action="contracts#updateCost">
<!-- options for each contract type, containing name & id -->
</select>
<input type="number" name="contract[monthly_cost]" id="contract_monthly_cost"
data-target="contracts.monthlyCost">
</form>
Here's the Stimulus contracts controller:
// contracts_controller.js (not working)
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "contractType", "monthlyCost" ]
updateCost() {
const contractTypeId = String(this.contractTypeTarget.value) // value from select box
const monthlyCost = this.monthlyCostTarget // field to fill in
const contractTypeCost = 'Uh-oh' // How do we get contractType's default cost?
monthlyCost.value = Number(contractTypeCost).toFixed(2)
}
}
A few things to notice:
- The
data-target
elements from the HTML are listed in the controller's targets array, and each element of that array is available in the class by adding theTarget
suffix to the element's name. For example,data-target="contracts.contractType"
is listed in thetarget
array of the contracts controller ascontractType
, which is then accessed in the JS code asthis.contractTypeTarget
. - Stimulus watches for changes to DOM elements that have a
data-action
attribute, so when the select box value is changed, the contracts controller will execute theupdateCost()
function. - There is a problem. The
default_cost
of eachContractType
is stored in the database and not immediately accessible to the client-side Javascript code. The select box only contains thename
andid
of each option. How could I fix this?
The first thing I tried to do was to pass parenthetical arguments via the data-action attribute. This did not work. Stimulus is not designed to be used in that way.
I considered setting up an API call to query the database, but that seemed like a lot of work. The primary purpose of frameworks is to make things simpler, and making additional HTTP requests to API endpoints that don't exist yet is not a simple solution.
Thankfully, there's a much easier way. I chose to add the relevant pricing information into the dataset of the select element.
First, I prepared the data in the Rails controller as a JSON object:
# contracts_controller.rb
# Create JSON object
# key = contract_type.id, value = contract_type.default_cost
@contract_types = ContractType.all.order :name
@contract_type_data = @contract_types.pluck(:id, :default_cost).to_h.to_json
... then I added it to the select box as a data-info attribute (there's nothing special about data-info; any unused data attribute would work):
<!-- contracts/new.html.erb -->
<form data-controller="contracts">
<select name="contract[contract_type]" id="contract_contract_type"
data-target="contracts.contractType" data-action="contracts#updateCost"
data-info="<%= @contract_type_data %>">
<!-- options for each contract type -->
</select>
<input type="number" name="contract[monthly_cost]" id="contract_monthly_cost"
data-target="contracts.monthlyCost">
</form>
... and finally, here it is in the Stimulus controller:
// contracts_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "contractType", "monthlyCost" ]
updateCost() {
const contractTypeId = String(this.contractTypeTarget.value) // value from select box
const contractTypesInfo = JSON.parse(this.contractTypeTarget.dataset.info) // data object
const monthlyCost = this.monthlyCostTarget // field to fill in
const contractTypeCost = contractTypesInfo[contractTypeId]
monthlyCost.value = Number(contractTypeCost).toFixed(2)
}
}
Now when a user selects a ContractType
in the select box, the value of that box is used as a key to access a JSON object containing the correct default_cost
.
Is there any problem with this solution? Not from my perspective, although a large enough data object would look pretty strange crammed into an HTML data-info attribute. If there was a huge data object or sensitive data involved, then an API call with a narrower query to the database would probably be better so as to be more specific about which data reaches the client. In this case however, I'm pleased with this solution, as well as with Stimulus in general.
Top comments (0)