Skip to content

Repository files navigation

stimulus-elements

Declarative, controller-scoped element lookups for Stimulus — like targets, but backed by any CSS selector. Built on the controller-blessing pattern created by Marco Roth, with code adapted from his article Supercharge your Stimulus controllers with custom APIs.

Important

This library is currently used internally at OpenProject and is under active development. There is no public roadmap yet — expect APIs to change without notice.

Install

bun add @openproject/stimulus-elements @hotwired/stimulus

Setup

Register the blessing once, where you start your Stimulus application:

import { installElements } from "@openproject/stimulus-elements"

installElements()

installElements() must run before you register any controllers / call Application.start() — blessings are snapshotted per controller at registration time, so installing afterward yields controllers without the accessors.

Usage

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static elements = {
    backdrop: "#backdrop",
    item: ".item",
    menuItem: ".menu > li",
  }

  connect() {
    this.backdropElement?.classList.remove("hidden") // Element | null
    this.itemElements.forEach((el) => (el.dataset.ready = "true")) // Element[]
    if (this.hasMenuItemElement) { /* ... */ } // boolean
  }
}

For each entry foo: "<selector>" (key camelized) you get:

Accessor Type Behaviour
this.fooElement Element | null first match, scoped to the controller element
this.fooElements Element[] all matches, as a real array
this.hasFooElement boolean whether a match exists

Lookups are scoped to the controller's own element, read live on every access, and never throw — an invalid selector warns once and yields null / [].

Overriding selectors from the DOM

Any declared element's selector can be overridden per instance from the controller element, without changing the controller:

<div data-controller="test"
     data-test-backdrop-element=".backdrop"
     data-test-menu-item-element=".item:not([data-disabled])"></div>

The attribute is data-[identifier]-[name]-element, where [name] is the element name in kebab-case (so menuItem becomes menu-item). When present and non-empty it wins over the static elements selector; an empty or whitespace-only value falls back to the static selector. Overrides are read live, like all lookups.

Keep element names to simple camelCase words — an embedded acronym like htmlURL dasherizes to html-u-r-l, which is hard to predict in the attribute.

TypeScript

import { Controller } from "@hotwired/stimulus"
import type { WithElements } from "@openproject/stimulus-elements"

interface MyController extends WithElements<{ backdrop: string }> {}
class MyController extends Controller {
  static elements = { backdrop: "#backdrop" }
}

WithElements camelizes keys exactly like the runtime, so you can pass your static elements keys verbatim — menu_item and menuItem both yield menuItemElement / menuItemElements / hasMenuItemElement.

Releasing

Releases are driven by changesets. When your PR changes published behaviour, run bunx changeset and commit the generated file alongside your change. On merge to main, a "Release Tracking" PR collects pending changesets; merging that PR bumps the version, updates CHANGELOG.md, and publishes to npm (via OIDC trusted publishing — no tokens).

Credits

Adapted from Marco Roth's article Supercharge your Stimulus controllers with custom APIs, which introduced the blessing pattern this library builds on.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages