An open API service indexing awesome lists of open source software.

https://github.com/allmarkedup/stimulus-x

StimulusX brings the power of reactive programming to Stimulus JS.
https://github.com/allmarkedup/stimulus-x

dom dom-binding reactive-programming stimulus stimulusjs

Last synced: 9 days ago
JSON representation

StimulusX brings the power of reactive programming to Stimulus JS.

Awesome Lists containing this project

README

          



_Reactivity engine for Stimulus controllers_

![NPM Version](https://img.shields.io/npm/v/stimulus-x)
[![CI](https://github.com/allmarkedup/stimulus-x/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/allmarkedup/stimulus-x/actions/workflows/ci.yml)

---

_StimulusX_ brings modern **reactive programming paradigms** to [Stimulus](https://stimulus.hotwired.dev) controllers.

**Features include:**

❎  Automatic UI updates with reactive DOM bindings

❎  Declarative binding syntax based on Stimulus' [action descriptors](https://stimulus.hotwired.dev/reference/actions#descriptors)

❎  Chainable value modifiers

❎  Property watcher callback

❎  Extension API




**Who is StimulusX for?**

If you are a Stimulus user and are tired of writing repetitive DOM manipulation code then StimulusX's declarative, live-updating **controller→HTML bindings** might be just what you need to brighten up your day. _StimulusX_ will make your controllers cleaner & leaner whilst ensuring they are less tightly coupled to a specific markup structure.

However if you are _not_ currently a Stimulus user then I'd definitely recommend looking at something like [Alpine](https://alpinejs.dev), [VueJS](https://vuejs.org/) or [Svelte](https://svelte.dev/) first before considering a `Stimulus + StimulusX` combo, as they will likely provide a more elegant fit for your needs.

[ ↓ Skip examples and jump to the docs ↓](#installation)

### Example: A simple counter

Below is an example of a simple `counter` controller implemented using StimulusX's reactive DOM bindings.

> [!TIP]
> _You can find a [runnable version of this example on JSfiddle →](https://jsfiddle.net/allmarkedup/q293ay8v/)_

```html



of

⬆️
⬇️


```

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

export default class extends Controller {
initialize(){
this.count = 0;
this.max = 5;
}

increment(){
this.count++;
}

decrement(){
this.count--;
}

get displayClasses(){
return {
"text-green": this.count <= this.max,
"text-red font-bold": this.count > this.max,
}
}
}
```

---

## Installation

Add the `stimulus-x` package to your `package.json`:

#### Using NPM:

```
npm i stimulus-x
```

#### Using Yarn:

```
yarn add stimulus-x
```

#### Without a bundler

You can use StimulusX with native browser `module` imports by loading from it from [Skypack](https://skypack.dev):

```html



import { Application } from "https://cdn.skypack.dev/@hotwired/stimulus"
import StimulusX from "https://cdn.skypack.dev/stimulus-x"
// ...see docs below for usage info

```

## Usage

StimulusX hooks into your Stimulus application instance via the `StimulusX.init` method.

```js
import { Application, Controller } from "@hotwired/stimulus";
import StimulusX from "stimulus-x";

window.Stimulus = Application.start();

// You must call the `StimulusX.init` method _before_ registering any controllers.
StimulusX.init(Stimulus);

// Register controllers as usual...
Stimulus.register("example", ExampleController);
```

By default, **all registered controllers** will automatically have access to StimulusX's reactive features - including [attribute bindings](#️attribute-bindings) (e.g. class names, `data-` and `aria-` attributes, `hidden` etc), [text content bindings](#text-bindings), [HTML bindings](#html-bindings) and more.

Explicit controller opt-in

If you don't want to automatically enable reactivity for all of your controllers you can instead choose to _opt-in_ to StimulusX features on a controller-by-controller basis.

To enable individual controller opt-in set the `optIn` option to `true` when initializing StimulusX:

```js
StimulusX.init(Stimulus, { optIn: true });
```

To then enable reactive features on a per-controller basis, set the `static reactive` variable to `true` in the controller class:

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

export default class extends Controller {
static reactive = true; // enable StimulusX reactive features for this controller
// ...
}
```

Reactive DOM bindings - overview

[HTML attributes](#attribute-binding), [text](#text-binding) and [HTML content](#text-binding) can be tied to the value of controller properties using `data-bind-*` attributes in your HTML.

These bindings are _reactive_ which means the DOM is **automatically updated** when the value of the controller properties change.

### Binding descriptors

Bindings are specified declaratively in your HTML using `data-bind-(attr|text|html)` attributes where the _value_ of the attribute is a **binding descriptor**.

**Attribute** binding descriptors take the form `attribute~identifier#property` where `attribute` is the name of the **HTML attribute** to set, `identifier` is the **controller identifier** and `property` is the **name of the property** to bind to.

```html


```

📚 ***Read more: [Attribute bindings →](#attribute-binding)***

**Text** and **HTML** binding descriptors take the form `identifier#property` where `identifier` is the **controller identifier** and `property` is the **name of the property** to bind to.

```html


```

📚 ***Read more: [text bindings](#text-binding)*** _and_ ***[HTML bindings →](#html-binding)***

> [!NOTE]
> _If you are familiar with Stimulus [action descriptors](https://stimulus.hotwired.dev/reference/actions#descriptors) then binding descriptors should feel familiar as they have a similar role and syntax._

### Value modifiers

Binding _value modifiers_ are a convenient way to transform or test property values in-situ before updating the DOM.

```html

```

📚 ***Read more: [Binding value modifiers →](#binding-value-modifiers)***

### Negating property values

Boolean property values can be negated (inverted) by prefixing the `identifier#property` part of the binding descriptor with an exclaimation mark:.

```html

```

> [!NOTE]
> _The `!` prefix is really just an more concise alternative syntax for applying [the `:not` modifier](#binding-value-modifiers)._

### Shallow vs deep reactivity

By default StimulusX only tracks changes to **top level** controller properties to figure out when to update the DOM. This is _shallow reactivity_.

To enable _deep reactivity_ for a controller (i.e. the ability to track changes to **properties in nested objects**) you can can set the static `reactive` property to `"deep"` within your controller:

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

export default class extends Controller {
static reactive = "deep"; // enable deep reactivity mode
// ...
}
```

Alternatively you can enable deep reactivity for **all** controllers using the `trackDeep` option when [initializing StimulusX](#usage):

```js
StimulusX.init(Stimulus, { trackDeep: true });
```

Attribute bindings

Attribute bindings connect **HTML attribute values** to **controller properties**, and ensure that the attribute value is automatically updated so as to stay in sync with the value of the controller property at all times.

They are specified using `data-bind-attr` attributes with [value descriptors](#binding-descriptors) that take the general form `{attribute}~{identifier}#{property}`.

```html




```

```js
export default class extends Controller {
initialize(){
this.imageUrl = "https://placeholder.com/kittens.jpg";
}
}
```

In the attribute binding descriptor `src~lightbox#imageUrl` above:

* `src` is the **HTML attribute** to be added/updated/remove
* `lightbox` is the **controller identifier**
* `imageUrl` is the **name of the property** that the attribute value should be bound to

So the image `src` attribute will initially be set to the default value of the `imageUrl` property (i.e. `https://placeholder.com/kittens.jpg`). And whenever the `imageUrl` property is changed, the image `src` attribute value in the DOM will be automatically updated to reflect the new value.

```js
this.imageUrl = "https://kittens.com/daily-kitten.jpg"
//
```

### Boolean attributes

[Boolean attributes](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes) such as `checked`, `disabled`, `open` etc will be _added_ if the value of the property they are bound to is `true`, and _removed completely_ when it is `false`.

```html


submit

```

```js
export default class extends Controller {
initialize(){
this.incomplete = true;
}
}
```

Boolean attribute bindings often pair nicely with **[comparison modifiers](#comparison-modifiers)** such as `:is`:

```html



submit

```

```js
export default class extends Controller {
initialize(){
this.status = "incomplete";
}

// called when the text input value is changed
checkCompleted({ currentTarget }){
if (currentTarget.value?.length > 0) {
this.status === "complete"; // button will be enabled
}
}
}
```

### Binding classes

`class` attribute bindings let you set specific classes on an element based on controller state.

```html



...


```

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

export default class extends Controller {
initialize(){
this.count = 0;
}

get validityClasses(){
if (this.count > 10) {
return "text-red font-bold";
} else {
return "text-green";
}
}
}
```

In the example above, the value of the `validityClasses` property is a string of classes that depends on whether or not the value of the `count` property is greater than `10`:

* If `this.count > 10` then the element `class` attribute will be set to `"text-red font-bold"`.
* If `this.count < 10` then the element `class` attribute will be set to `"text-green"`.

The list of classes can be returned as a **string** or as an **array** - or as a special [class object](#class-objects).

#### Class objects

If you prefer, you can use a class object syntax to specify the class names. These are objects where the classes are the keys and booleans are the values.

The example above could be rewritten to use a class object as follows:

```js
export default class extends Controller {
// ...
get validityClasses(){
return {
"text-red font-bold": this.count > 10,
"text-green": this.count <= 10,
}
}
}
```

The list of class names will be resolved by merging all the class names from keys with a value of `true` and ignoring all the rest.

Text content bindings

Text content bindings connect the **`textContent`** of an element to a **controller property**. They are useful when you want to dynamically update text on the page based on controller state.

Text content bindings are specified using `data-bind-text` attributes where the value is a binding descriptor in the form `{identifier}#{property}`.

```html


Status:

```

```js
export default class extends Controller {
static values = {
status: {
type: String,
default: "in progress"
}
}
}
```

HTML bindings

HTML bindings are very similar to [text content bindings](#️text-bindings) except they update the element's `innerHTML` instead of `textContent`.

HTML bindings are specified using `data-bind-html` attributes where the value is a binding descriptor in the form `{identifier}#{property}`.

```html




```

```js
export default class extends Controller {
initialize(){
this.status = "in progress";
}

get statusIcon(){
if (this.status === "complete"){
return ``;
} else {
return ``;
}
}
}
```

Binding value modifiers

Inline _value modifiers_ are a convenient way to transform or test property values before updating the DOM.

Modifiers are appended to the end of [binding descriptors](#binding-descriptors) and are separated from the descriptor (or from each other) by a `:` colon.

The example below uses the `upcase` modifier to transform the title to upper case before displaying it on the page:

```html


```

> [!TIP]
> _Multiple modifiers can be piped together one after each other, separated by colons, e.g. `article#title:upcase:trim`_

String transform modifiers

String transform modifiers provide stackable output transformations for string values.

#### Available string modifiers:

* `:upcase` - transform text to uppercase
* `:downcase` - transform text to lowercase
* `:strip` - strip leading and trailing whitespace

:upcase

Converts the string to uppercase.

```html


```

:downcase

Converts the string to lowercase.

```html


```

:strip

Strips leading and trailing whitespace from the string value.

```html


```

Comparison modifiers

_Comparison modifiers_ compare the resolved **controller property value** against a **provided test value**.

```html

```

They are primarily intended for use with [boolean attribute bindings](#boolean-attributes) to conditionally add or remove attributes based on the result of value comparisons.

> [!TIP]
> _Comparison modifiers play nicely with other chained modifiers - the comparison will be done against the property value **after** it has been transformed by any other preceeding modifiers_:
> ```html
> `
> ```

#### Available comparison modifiers:

* `:is()` - equality test ([read more](#is-modifier))
* `:isNot()` - negated equality test ([read more](#is-not-modifier))
* `:gt()` - 'greater than' test ([read more](#gt-modifier))
* `:gte()` - 'greater than or equal to' test ([read more](#gte-modifier))
* `:lt()` - 'less than' test ([read more](#lt-modifier))
* `:lte()` - 'less than or equal to' test ([read more](#lte-modifier))

:is(<value>)

The `:is` modifier compares the resolved property value with the `` provided as an argument, returning `true` if they match and `false` if not.

```html

```

* **String** comparison: `:is('single quoted string')`, `:is("double quoted string")`
* **Integer** comparison: `:is(123)`
* **Float** comparison: `:is(1.23)`
* **Boolean** comparison: `:is(true)`, `:is(false)`

:isNot(<value>)

The `:isNot` modifier works exactly the same as the [`:is` modifier](#is-modifier), but returns `true` if the value comparison fails and `false` if the values match.

> [!IMPORTANT]
> _The `:is` and `:isNot` modifiers only accept simple `String`, `Number` or `Boolean` values. `Object` and `Array` values are not supported._

:gt(<value>)

The `:gt` modifier returns `true` if the resolved property value is **greater than** the numeric `` provided as an argument.

```html

+
```

:gte(<value>)

The `:gte` modifier returns `true` if the resolved property value is **greater than or equal to** the numeric `` provided as an argument.

```html

+
```

:lt(<value>)

The `:lt` modifier returns `true` if the resolved property value is **less than** the numeric `` provided as an argument.

```html

-
```

:lte(<value>)

The `:lte` modifier returns `true` if the resolved property value is **less than or equal to** the numeric `` provided as an argument.

```html

-
```

Other modifiers

* `:not` - negate (invert) a boolean value

> [!TIP]
> _You can add your own **custom modifiers** if required. See [Extending StimulusX](#extending) for more info._

Watching properties for changes

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

export default class extends Controller {
static watch = ["enabled", "userInput"];

connect(){
this.enabled = false;
this.userInput = "";
}

enabledPropertyChanged(currentValue, previousValue){
if (currentValue) {
console.log("Controller is enabled");
} else {
console.log("Controller has been disabled");
}
}

userInputPropertyChanged(currentValue, previousValue){
console.log(`User input has changed from "${previousValue}" to "${currentValue}"`);
}

// ...
}
```

🚧 _More docs coming soon..._

Extending StimulusX

### Custom modifiers

You can add your own modifiers using the `StimulusX.modifier` method:

```js
StimulusX.modifier("modifierName", (value) => {
// Do something to `value` and return the result of the transformation.
const transformedValue = doSomethingTo(value);
return transformedValue;
});
```

### Custom directives

🚧 _Documentation coming soon..._

## Known issues, caveats and workarounds

### ❌ Private properties and methods

Unfortunately it is not possible to use StimulusX with controllers that define [private methods or properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements) (i.e. with names using the `#` prefix). [See Lea Verou's excellent blog post on the topic for more details.](https://lea.verou.me/blog/2023/04/private-fields-considered-harmful/)

If you have existing controllers with private methods and want to add new StimulusX-based controllers alongside them then you should [enable explicit controller opt-in](#controller-opt-in) to prevent errors being thrown at initialization time.

## Credits and inspiration

StimulusX uses [VueJS's reactivity engine](https://github.com/vuejs/core/tree/main/packages/reactivity) under the hood and was inspired by (and borrows much of its code from) the excellent [Alpine.JS](https://alpinejs.dev/directives/bind) library.

## License

StimulusX is available as open source under the terms of the MIT License.