Why values exist: reading config out of the DOM

Free preview 3 min to read

A Stimulus controller's element can carry configuration in plain data-* attributes before static values enters the picture. this.element.dataset.dismissAfter reads a string, always a string, with no declared name to check typos against and no type to convert it into. This app's flash controller needs a number, not a string, and static values is the mechanism that closes that gap.

A controller that needs a number

app/javascript/controllers/ui/flash_controller.js, the value and the method that reads it:

export default class extends Controller {
  static values = { dismissAfter: { type: Number, default: 0 } }

  #scheduleAutoDismiss() {
    if (this.dismissAfterValue <= 0) return
    this.#dismissTimer = setTimeout(() => this.dismiss(), this.dismissAfterValue)
  }
}

setTimeout needs a number of milliseconds, and this.dismissAfterValue <= 0 is a numeric comparison. Declaring dismissAfter as a Number value means this.dismissAfterValue is a JavaScript number every time this method reads it. The string-to-number conversion already happened, in a place this method never has to think about.

Where the number comes from

app/views/layouts/_flash.html.erb renders the element this controller attaches to:

data-ui--flash-dismiss-after-value="<%= duration_map.fetch(base_type, 5000) %>"

with duration_map = { "notice" => 4000, "alert" => 7000, "unlock" => 10000 }. A success flash stays on screen for 4 seconds, an error for 7, and an achievement unlock for 10. The HTML attribute is still text: "4000" is digits inside a string, like any other HTML attribute. HTML attributes didn't change. What changed is that static values declares, once, that whatever text lands in that attribute reaches this.dismissAfterValue already converted to a number.

The attribute name isn't a second thing to remember

dismissAfter (the JavaScript property) and dismiss-after-value (the HTML attribute suffix) aren't two separate choices. Stimulus derives the second from the first with a fixed transform: camelCase becomes dash-case, and -value is appended. dismissAfter becomes dismiss-after-value, and with the controller's identifier (ui--flash) in front you get the full data-ui--flash-dismiss-after-value this app's partial writes. Get the JavaScript name right and the HTML name follows, with no second name to keep in sync by hand.

Without static values

Reading the same attribute by hand would look like this:

const raw = this.element.dataset.dismissAfter   // "4000" — a string, always
const ms  = raw ? parseInt(raw, 10) : 0          // conversion, written out by hand, every time

That's two lines this app's controller never writes, repeated in every controller that reads a number from the DOM, and each call site free to spell the conversion, the default or the property name a little differently. static values turns that boilerplate into a declaration. Name the value once, state its type once, and every controller that declares dismissAfter: Number gets the same, correct behavior.

Key takeaways

  • this.element.dataset.xxx always returns a string (or undefined). Nothing converts it, and nothing checks that it exists.
  • static values declares a value's name and type once. Stimulus generates a typed accessor (this.dismissAfterValue) that reads the DOM attribute and converts it automatically.
  • This app's flash controller needs dismissAfterValue as a number for a setTimeout call and a <= 0 comparison, and a Number value guarantees that.
  • The HTML attribute name (dismiss-after-value) is derived from the JavaScript value name (dismissAfter): dash-case plus a -value suffix, with the controller identifier in front.
Lesson 1 of 7 · free

Keep going with lesson 2

Declaring values: shorthand vs. the full object form

The shorthand type constructor versus the full type and default object, with all five value types in use.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...