Smarter Use of Stimulus' Action Parameters

10 hours ago 3

This article is extracted from the book JavaScript for Rails Developers and edited for the web (use SUMMERSALE to get a 25% discount 🤫☀️).


Let’s imagine a typical text editor that has settings for the theme (a string), line numbers (boolean) and the font size (number).

Try to think how’d you set that up in a Stimulus controller? Create a separate method for each setting? updateTheme and setLineNumbers and so on? Not bad, but I’d like to provide a suggestion that is way more maintainable and applicable to any kind of settings set up.

As always, we follow the outside-in approach by adding the HTML first:

<div data-controller="editor"> </div>

The Stimulus controller could look something like this:

import { Controller } from "@hotwired/stimulus" export default class extends Controller { static values = { theme: { type: String, default: "githubLight" }, lineNumbersEnabled: { type: Boolean, default: false }, fontSize: { type: Number, default: 16 } } // … }

Let’s add the first setting option theme:

<div data-controller="editor"> <select data-action="editor#updateSetting" data-editor-key-param="theme">     <option value="githubLight">GitHub Light Theme</option>     <option value="githubDark">GitHub Dark Theme</option>   </select> </div>

Now instead of having a separate updateTheme method I will add a updateSetting method:

export default class extends Controller { // … updateSetting({ params: { key }, currentTarget }) { this[`${key}Value`] = currentTarget.value } }

This uses Stimulus’ action parameters, as seen in the data-editor-key-param="theme" attribute in the HTML. The updateSetting method takes one object argument and destructures two properties from it:

  • params (an object that includes the key property);
  • currentTarget (the actual

A more verbose version of this would be:

updateSetting(object) { const key = object.params.key; const currentTarget = object.currentTarget; }

The destructured version is preferred for its brevity. Then add the themeValueChanged callback method:

export default class extends Controller { // … themeValueChanged() { // put request done here } }

All that is needed is a PUT-request to your (Rails) back-end. I’ve already written an article how to send requests from Stimulus controllers, so please refer to it to see how that can be done.

Let’s add another option for enabling or disabling line numbers:

<div data-controller="editor"> <select data-action="editor#updateSetting" data-editor-key-param="theme">     <option value="githubLight">GitHub Light Theme</option>     <option value="githubDark">GitHub Dark Theme</option>   </select> <fieldset> <input type="checkbox" checked id="line_numbers_enabled" data-action="editor#updateSetting" data-editor-key-param="lineNumbersEnabled"> <label for="line_numbers_enabled">Enable linenumbers</label> </fieldset> </div>

Notice how reusable this setup is? Now, let’s extend editor_controller.js to respond to changes in the line number setting:

export default class extends Controller { // … + themeValueChanged() { + // put request done here + } + lineNumbersEnabledValueChanged() { + // put request done here + } // … }

If you try enabling or disabling line numbers now, you might notice nothing happens. That’s because the checkbox’s value (which is on) is passed to the values API. Under the hood, Stimulus decodes it using: !(value == "0" || value == "false").

So “on” will always be interpreted as true. Let’s fix that by updating the updateSetting method:

export default class extends Controller { import {valueFrom} from "./helpers" // … updateSetting({ params: { key }, currentTarget }) { this[`${key}Value`] = valueFrom(currentTarget) } // … }

Where is valueFrom coming from? It is as a new helper-method in a shared helper file, since it can likely be reused across other Stimulus controllers:

// app/javascript/controllers/helpers.js export function valueFrom(target) { const types = { checkbox: (target) => target.checked, number: (target) => Number(target.value), text: (target) => { const value = target.value return !isNaN(value) && value.trim() !== "" ? Number(value) : value } } return (types[target.type] || (target => target.value))(target) }

That might look a bit complex at first glance. Here is what’s happening:

  • types is a “lookup table” where each key corresponds to a type of HTML input;
  • each value in the table is a function that transforms the input appropriately;
  • checkbox returns a boolean;
  • number converts the input to a number;
  • text attempts to convert to a number if the string looks numeric, otherwise, it returns the raw string.

Then, the final line selects the appropriate function and calls it. If the type isn’t listed, it defaults to returning the input’s value as-is.

Now the values are correctly coerced. How to store the changed settings permanently? That’s up to you—but you already have all the necessary pieces in place. Pro-tip: consider extracting the logic from the various value callback methods into an #update() method so it can be reused.

And that is how I remove duplicated logic by moving “configuration logic_ into the HTML. This makes for a real reusable, and small Stimulus controller. Once you know this technique you might see use for it in more places.

Published at 10 July 2025. Have suggestions or improvements on this content? Do reach out.

Read Entire Article