The Values and Classes APIs #202

This pull request introduces two new APIs to Stimulus: Values and Classes. These APIs are designed to improve upon, and ultimately obviate, the current Data Map API. We plan to ship them together in the upcoming Stimulus 2.0 release.

Values

Most uses of the Data Map API in Basecamp fall under the following categories:

  • Storing small strings, such as URLs, dates, or color values
  • Keeping track of a numeric index into a collection
  • Bootstrapping a controller with a JSON object or array
  • Conditioning behavior on a per-controller basis

However, the Data Map API only works with string values. That means we must manually convert to and from other types as needed. The Values API handles this type conversion work automatically.

Value properties

The Values API adds support for a static values object on controllers. The keys of this object are Data Map keys, and the values declare their data type:

export default class extends Controller {
  static values = {
    url: String,
    refreshInterval: Number,
    loadOnConnect: Boolean
  }

  connect() {
    if (this.loadOnConnectValue) {
      this.load()
    }
  }

  async load() {
    const response = await fetch(this.urlValue)
    // ...
    setTimeout(() => this.load(), this.refreshIntervalValue)
  }
}

Supported types and defaults

This pull request implements support for five built-in types:

Type Serialized attribute value Default value
Array JSON.stringify(array) []
Boolean boolean.toString() false
Number number.toString() 0
Object JSON.stringify(object) {}
String Itself ""

Each type has a default value. If a value is declared in a controller but its associated data attribute is missing, the getter property will return its type's default.

Controller properties

Stimulus automatically generates three properties for each entry in the object:

Type Kind Property name Effect
Boolean, Number, Object, String Getter this.[name]Value Reads data-[identifier]-[name]-value
Array Getter this.[name]Values Reads data-[identifier]-[name]-values
Boolean, Number, Object, String Setter this.[name]Value= Writes data-[identifier]-[name]-value
Array Setter this.[name]Values= Writes data-[identifier]-[name]-values
Boolean, Number, Object, String Existential this.has[Name]Value Tests for presence of data-[identifier]-[name]-value
Array Existential this.has[Name]Values Tests for presence of data-[identifier]-[name]-values

Note that array values are always pluralized, both as properties and as attributes.

Value changed callbacks

In addition to value properties, the Values API introduces value changed callbacks. A value changed callback is a specially named method called by Stimulus whenever a value's data attribute is modified.

To observe changes to a value, define a method named [name]ValueChanged(). For example, a slideshow controller with a numeric index property might define an indexValueChanged() method to display the specified slide:

export default class extends Controller {
  static values = { index: Number }

  indexValueChanged() {
    this.showSlide(this.indexValue)
  }

  // ...
}

Stimulus invokes each value changed callback once when the controller is initialized, and again any time the value's data attribute changes.

Even if a value's data attribute is missing when the controller is initialized, Stimulus will still invoke its value changed callback. Use the existential property to determine whether the data attribute is present.


Classes

Another common use of the Data Map API is to store CSS class names.

For example, Basecamp's copy-to-clipboard controller applies a CSS class to its element after a successful copy. To avoid inlining a long BEM string in our controller, and to keep things loosely coupled, we declare the class in a data-clipboard-success-class attribute:

<div data-controller="clipboard"
     data-clipboard-success-class="copy-to-clipboard--success">

and access it using this.data.get("successClass") in the controller:

this.element.classList.add(this.data.get("successClass"))

The Classes API formalizes and refines this pattern.

Class properties

The Classes API adds a static classes array on controllers. As with targets, Stimulus automatically adds properties for each class listed in the array:

// clipboard_controller.js
export default class extends Controller {
  static classes = [ "success", "supported" ]

  initialize() {
    if (/* ... */) {
      this.element.classList.add(this.supportedClass)
    }
  }

  copy() {
    // ...
    this.element.classList.add(this.successClass)
  }
}
Kind Property name Effect
Getter this.[name]Class Reads the data-[identifier]-[name]-class attribute
Existential this.has[Name]Class Tests whether the data-[identifier]-[name]-class attribute is present

Declarations are assumed to be present

When you access a class property in a controller, such as this.supportedClass, you assert that the corresponding data attribute is present on the controller element. If the declaration is missing, Stimulus throws a descriptive error:

Screenshot showing error message: "Missing attribute 'data-clipboard-supported-class'"

If a class is optional, you must first use the existential property (e.g. this.hasSupportedClass) to determine whether its declaration is present.



Unifying target attributes

We've made a change to the target attribute syntax to align them with values and classes, and also to make the controller identifier more prominent by moving it into the attribute name.

The original syntax is:

<div data-target="[identifier].[name]">

and the updated syntax is:

<div data-[identifier]-target="[name]">

The original syntax is supported but deprecated

Stimulus 2.0 will support both syntaxes, but using the original syntax will display a deprecation message in the developer console. We intend to remove the original syntax in Stimulus 3.0.


Try it out in your application

Update the Stimulus entry in package.json to point to the latest development build:

"stimulus": "https://github.com/stimulusjs/dev-builds/archive/b8cc8c4/stimulus.tar.gz"