Supercharge your Stimulus controllers with Custom APIs
Tags: stimulus, hotwire, rails, ruby
Published: 2023-07-27 13:20:33

---
title: "Supercharge your Stimulus controllers with Custom APIs"
published: true
description: Learn how to create a Stimulus Elements API from scratch, allowing you to easily access DOM elements using CSS selectors
tags: stimulus, hotwire, rails, ruby
cover_image: https://marcoroth.dev/images/heros/stimulus_custom_api.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-07-27 05:00:00 +0000
---

Since its initial release in 2017, Stimulus has undergone minimal breaking changes and has been known for its stability. Even a Stimulus controller written for version 1.0.0 still works flawlessly with the latest 3.2.1 release, using the same syntax.

Considering the fast-paced world of JavaScript, this stability might seem unexpected. While Stimulus has added new features over the years, its primary strength lies in the various APIs introduced with each new release.

* Stimulus 1.0.0: **Targets API**
* Stimulus 2.0.0: **Values API** and the **CSS classes API**
* Stimulus 3.2.0: **Outlets API**

While also other features were added in these releases, it's the Stimulus APIs that truly make Stimulus and the releases special.

In this article, we will explore the process of creating a new Stimulus API that can be used in any Stimulus Controller.

### The Stimulus Elements API

Most Stimulus controllers should ideally be designed to be used on multiple DOM elements. However, there are also cases where we need to reference elements outside of the controller's scope or cases where you design a controller which is meant to control a specific element on the page.

The use of the Targets and/or Outlets API also might seem logical for these cases, but the Elements API might better suited if ...

* ... you either can't control the elements you want to reference and/or can't define them as targets
* ... they are outside of the controller scope and you don't need them to be full-blown outlets
* ... or the controller specifically needs to control a special element on the page, independent from it's own controller element and it's children

A common pattern emerges in such situations: defining a `get` function on the controller with names like `[name]Element` or `[name]Elements`. These functions return a single element or a set of elements using [`document.querySelector(...)`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) or [`document.querySelectorAll(...)`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) respectively.


```js
import { Controller } from "@hotwired/stimulus"
import tippy from "tippy.js"

export default class extends Controller {
  connect() {
    this.backdropElement.classList.remove("hidden")
    this.itemElements.forEach(element => ...)
    this.tippyElements.forEach(element => tippy(element))
  }

  // ...

  get backdropElement() {
    return document.querySelector("#backdrop")
  }

  get itemElements() {
    return document.querySelectorAll(".item")
  }

  get tippyElements() {
    return document.querySelectorAll("[data-tippy]")
  }
}
```

To avoid repetition and improve code readability, we will build an API that abstracts this pattern into a Stimulus API, similar to the Targets or Outlets API.


### Creating the Elements API


Stimulus internally uses so-called `Blessings`, which enhance the `Controller` class with new functionality. Each API is a separate `Blessing`, maintaining a modular design.

The Stimulus `Controller` class has a `static blessings` Array which holds all of the `Blessings` the controller should be blessed with.

In Stimulus 3.2, the `blessings` array contains:

```js
// @hotwired/stimulus - src/core/controller.ts

export class Controller {
  static blessings = [
    ClassPropertiesBlessing,
    TargetPropertiesBlessing,
    ValuePropertiesBlessing,
    OutletPropertiesBlessing,
  ]

  // ...
}
```


With that knowledge we can create our own `ElementPropertiesBlessing` and extend Stimulus with our Elements API by adding it to the `blessings` array.

### Proposed API

Inspired by other Stimulus APIs, we'll declare a `static` property called `elements`, defining the elements we want to reference along with their CSS selectors.

```js
import { Controller } from "@hotwired/stimulus"
import tippy from "tippy.js"

export default class extends Controller {
  static elements = {
    backdrop: "#backdrop",
    item: ".item",
    tippy: "[data-tippy]"
  }

  connect() {
    this.backdropElement.classList.remove("hidden")
    this.itemElements.forEach(element => ...)
    this.tippyElements.forEach(element => tippy(element))
  }

  // ...
}
```

By calling `[name]Element` or `[name]Elements`, the API will determine whether to use `document.querySelector(...)` or `document.querySelectorAll(...)`, streamlining the process.

### Implementing the `ElementPropertiesBlessing`

Let's dive into creating the `ElementPropertiesBlessing` function that will power our Elements API. We'll create a new file called `element_properties.js` exporting the `ElementPropertiesBlessing` constant:

```js
// app/javascript/element_properties.js

export function ElementPropertiesBlessing(constructor) {
  const properties = {}

  return properties
}
```

The `ElementPropertiesBlessing` function takes the `constructor` (controller) as an argument. Inside this function, we initialize an empty `properties` object to store the properties which should get added to the `constructor`.

In context of our Elements API we want `properties` to contain the functions for our `[name]Element` and `[name]Elements` getters, which should look something like in the end:

```js
{
  'backdropElement': {
    get() {
      return document.querySelector(...)
    }
  },
  'backdropElements': {
    get() {
      return document.querySelectorAll(...)
    }
  },
  'itemElement':   { get() { /* ... */ } },
  'itemElements':  { get() { /* ... */ } },
  'tippyElement':  { get() { /* ... */ ) },
  'tippyElements': { get() { /* ... */ } }
}
```

In order to construct that object we want to read the `static elements` property of the controller.

Stimulus (privately) exposes two functions we can use for this. Depending on the structure of the definition we can either use `readInheritableStaticArrayValues()` for an Array structure (like the Targets API) or `readInheritableStaticObjectPairs()` for an Object structure (like the Values API).


> **Note:** About using privately exposed Stimulus functions:

> The next version of Stimulus makes it easier to access parts of the private API thanks to my [pull request](https://github.com/hotwired/stimulus/pull/686).

> If you want to make use of that today, you can install the latest dev-build using:
  ```bash
  yarn add @hotwired/stimulus@https://github.com/hotwired/dev-builds/archive/@hotwired/stimulus/7b810ec.tar.gz
  ```

> Otherwise, you need to wait for the upcoming Stimulus 3.3 release.

---

Since our Elements API uses an object to define the elements and selectors we want to use the `readInheritableStaticObjectPairs()` function.

```js
import { readInheritableStaticObjectPairs } from "@hotwired/stimulus/dist/core/inheritable_statics"

export function ElementPropertiesBlessing(constructor) {
  const properties = {}
  const definitions = readInheritableStaticObjectPairs(constructor, "elements")

  return properties
}
```

The `definitions` variable now holds an array of definitions, which looks like this:

```js
[
  ["backdrop", "#backdrop"],
  ["item", ".item"],
  ["tippy", "[data-tippy]"]
]
```


Let's define a `propertiesForElementDefinition()` function, which generates the corresponding properties for each element definition.

```js
import { readInheritableStaticObjectPairs } from "@hotwired/stimulus/dist/core/inheritable_statics"
import { namespaceCamelize } from "@hotwired/stimulus/dist/core/string_helpers"

export function ElementPropertiesBlessing(constructor) {
  const properties = {}
  const definitions = readInheritableStaticObjectPairs(constructor, "elements")

  return properties
}

function propertiesForElementDefinition(definition) {
  const [name, selector] = definition
  const camelizedName = namespaceCamelize(name)

  return {
    [`${camelizedName}Element`]: {
      get() {
        return document.querySelector(selector)
      }
    },

    [`${camelizedName}Elements`]: {
      get() {
        return document.querySelectorAll(selector)
      }
    }
  }
}
```

In this function, we extract the `name` and `selector` from the definition array. Then, using `namespaceCamelize()`, we create camel-cased versions of the element names.

The `propertiesForElementDefinition()` function returns an object containing two properties for each element definition: `[name]Element` and `[name]Elements`. The former returns a single element using `document.querySelector(...)`, while the latter returns a `NodeList` using `document.querySelectorAll(...)`.

For each element definition in the `definitions` array, we want to call the `propertiesForElementDefinition()` function to create the corresponding properties and merge them into the `properties` object using `Object.assign()`. Finally, we return the `properties` object containing all the element properties for the given controller.

```js
export function ElementPropertiesBlessing(constructor) {
  const properties = {}
  const definitions = readInheritableStaticObjectPairs(constructor, "elements")

  definitions.forEach(definition => {
    Object.assign(properties, propertiesForElementDefinition(definition))
  })

  return properties
}
```

By implementing the `ElementPropertiesBlessing`, we have constructed the backbone of our Elements API. This blessing will dynamically add the necessary properties to each controller instance, enabling easy access to the referenced DOM elements defined in the static `elements` property.

### Installing the API

To apply our new API, we need to tell Stimulus about it when starting the application. In Rails apps this is typically done in `app/javascript/controllers/application.js`.

We want to add our `ElementPropertiesBlessing` to the `blessings` array on the `Controller` class. By importing both `Controller` and `ElementPropertiesBlessing` we can push the function into the `blessings` array and Stimulus will add the properties to each new controller instance.

```js
// app/javascript/controllers/application.js

import { Application, Controller } from "@hotwired/stimulus"
import { ElementPropertiesBlessing } from "../element_properties"

Controller.blessings.push(ElementPropertiesBlessing)

const application = Application.start()

// Configure Stimulus development experience
application.warnings = true
application.debug = false
window.Stimulus = application

export { application }
```

With this, the Elements API is now available in every Stimulus controller within our application.

### More ideas

We could extend the API even more and define a `has[Name]Element` property which would check if an element exists for a given selector, similar to the Targets and Outlets API.

We could also think about extending the API to accept the selectors for the `elements` on the controller element using data attributes in the format of `data-[identifier]-[element]-element`. This could allow to defined selectors for the elements to be overridden:
```html
<div
  data-controller="test"
  data-test-backdrop-element=".backdrop"
  data-test-item-element=".item:not([data-disabled])"
  data-test-tippy-element="span.tippy"
></div>
```

### Conclusion

We successfully built a new Stimulus "Elements API," abstracting the pattern of referencing elements in controllers using CSS selectors. By leveraging Stimulus' modular architecture and the concept of Blessings, we extended the Controller class with our own API.

The Elements API enhances code readability by encapsulating the element lookup logic and reducing the need for repetitive code in controllers. It improves productivity, making it more elegant and efficient to work with DOM elements in various use cases.

Part of the reason for this post was to demonstrate that not every new API needs to ship with Stimulus itself to be useful in applications. Shipping custom APIs as application-specific code allows us build APIs to our specific needs without polluting the upstream framework with APIs not everyone might benefit from.

With that being said, I could also see a future where we ship ready-made `Blessings` and the Elements API could become an optional part of Stimulus itself or part of a package like [`stimulus-use`](https://github.com/stimulus-use/stimulus-use). These APIs wouldn't be enabled by default, but you could opt-in to enable the APIs you like to use in your application.

Feel free to experiment with my Elements API or potential new APIs you come up with and let me know your thoughts on [Twitter](https://twitter.com/marcoroth_) or [Mastodon](https://ruby.social/@marcoroth).

I would love to see what you come up with.

Happy coding!