Passing Arguments to StimulusJS Controllers
Tags: stimulus, javascript, rails
Published: 2018-06-08 20:16:46

---
title: Passing Arguments to StimulusJS Controllers
published: true
tags: StimulusJS, Stimulus, Javascript, Rails
canonical_url: https://www.briankephart.com/passing-arguments-to-stimulusjs-controllers
description: tl;dr – You can't, but there are other ways of getting data into your functions.
---

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.

```erb
<!-- 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:
```js
// 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 the `Target` suffix to the element's name. For example, `data-target="contracts.contractType"` is listed in the `target` array of the contracts controller as `contractType`, which is then accessed in the JS code as `this.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 the `updateCost()` function.
- There is a problem. The `default_cost` of each `ContractType` is stored in the database and not immediately accessible to the client-side Javascript code. The select box only contains the `name` and `id` 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:

```ruby
# 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):
```erb
<!-- 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:
```js
// 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.