├── .node-version
├── test
├── support
│ ├── setup.js
│ ├── test-controller.js
│ ├── helpers.js
│ └── test-context.js
├── directives
│ ├── text.test.js
│ └── attr.test.js
├── opt-in.test.js
├── negation.test.js
├── modifiers.test.js
├── watch.test.js
├── modifiers
│ ├── compare-numbers.test.js
│ ├── is.test.js
│ └── is-not.test.js
├── turbo.test.js
└── reactivity.test.js
├── .github
├── assets
│ ├── logo.png
│ └── counter.gif
└── workflows
│ └── ci.yml
├── .gitignore
├── src
├── modifiers
│ ├── not.js
│ ├── strip.js
│ ├── upcase.js
│ ├── downcase.js
│ ├── gt.js
│ ├── lt.js
│ ├── gte.js
│ ├── lte.js
│ ├── is.js
│ └── is-not.js
├── directives
│ ├── text.js
│ └── attr.js
├── options.js
├── index.js
├── utils.js
├── scheduler.js
├── modifiers.js
├── classes.js
├── reactivity.js
├── attributes.js
├── controller.js
├── lifecycle.js
├── directives.js
└── mutation.js
├── vitest.config.js
├── .release-it.json
├── LICENSE
├── package.json
├── README.md
└── dist
├── stimulus-x.js
└── stimulus-x.js.map
/.node-version:
--------------------------------------------------------------------------------
1 | 24.0.0
--------------------------------------------------------------------------------
/test/support/setup.js:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom/vitest";
2 |
--------------------------------------------------------------------------------
/.github/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allmarkedup/stimulus-x/HEAD/.github/assets/logo.png
--------------------------------------------------------------------------------
/.github/assets/counter.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allmarkedup/stimulus-x/HEAD/.github/assets/counter.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | .DS_Store
3 | .parcel-cache
4 | .wip
5 | /demo
6 | /dist/demo.*
7 | *.code-workspace
--------------------------------------------------------------------------------
/src/modifiers/not.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 |
3 | modifier("not", (value) => !value);
4 |
--------------------------------------------------------------------------------
/src/modifiers/strip.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 |
3 | modifier("strip", (value) => value.toString().trim());
4 |
--------------------------------------------------------------------------------
/src/modifiers/upcase.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 |
3 | modifier("upcase", (value) => value.toString().toUpperCase());
4 |
--------------------------------------------------------------------------------
/src/modifiers/downcase.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 |
3 | modifier("downcase", (value) => value.toString().toLowerCase());
4 |
--------------------------------------------------------------------------------
/test/support/test-controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 |
3 | export default class TestController extends Controller {}
4 |
--------------------------------------------------------------------------------
/test/support/helpers.js:
--------------------------------------------------------------------------------
1 | export function nextTick(fn) {
2 | return new Promise((resolve) =>
3 | setTimeout(async () => {
4 | resolve(fn ? await fn() : undefined);
5 | }, 0)
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/vitest.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | environment: "happy-dom",
6 | globals: true,
7 | setupFiles: "./test/support/setup.js",
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/release-it/schema/release-it.json",
3 | "github": {
4 | "release": true
5 | },
6 | "hooks": {
7 | "before:init": ["npm test"],
8 | "after:bump": "npm run build"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/modifiers/gt.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 |
3 | modifier("gt", (value, args = []) => {
4 | if (args.length === 0) {
5 | console.warn("Missing argument for `:gt` modifier");
6 | return false;
7 | }
8 |
9 | return value > args[0];
10 | });
11 |
--------------------------------------------------------------------------------
/src/modifiers/lt.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 |
3 | modifier("lt", (value, args = []) => {
4 | if (args.length === 0) {
5 | console.warn("Missing argument for `:lt` modifier");
6 | return false;
7 | }
8 |
9 | return value < args[0];
10 | });
11 |
--------------------------------------------------------------------------------
/src/modifiers/gte.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 |
3 | modifier("gte", (value, args = []) => {
4 | if (args.length === 0) {
5 | console.warn("Missing argument for `:gte` modifier");
6 | return false;
7 | }
8 |
9 | return value >= args[0];
10 | });
11 |
--------------------------------------------------------------------------------
/src/modifiers/lte.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 |
3 | modifier("lte", (value, args = []) => {
4 | if (args.length === 0) {
5 | console.warn("Missing argument for `:lte` modifier");
6 | return false;
7 | }
8 |
9 | return value <= args[0];
10 | });
11 |
--------------------------------------------------------------------------------
/src/modifiers/is.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 | import { isEqual } from "../utils";
3 |
4 | modifier("is", (value, args = []) => {
5 | if (args.length === 0) {
6 | console.warn("Missing argument for `:is` modifier");
7 | return false;
8 | } else {
9 | return isEqual(value, args[0]);
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/src/modifiers/is-not.js:
--------------------------------------------------------------------------------
1 | import { modifier } from "../modifiers";
2 | import { isEqual } from "../utils";
3 |
4 | modifier("isNot", (value, args = []) => {
5 | if (args.length === 0) {
6 | console.warn("Missing argument for `:isNot` modifier");
7 | return false;
8 | } else {
9 | return !isEqual(value, args[0]);
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/src/directives/text.js:
--------------------------------------------------------------------------------
1 | import { directive } from "../directives";
2 | import { mutateDom } from "../mutation";
3 |
4 | directive("text", (el, { property, modifiers }, { effect, evaluate, modify }) => {
5 | effect(() =>
6 | mutateDom(() => {
7 | const value = modify(evaluate(property), modifiers);
8 | el.textContent = value?.toString();
9 | })
10 | );
11 | });
12 |
--------------------------------------------------------------------------------
/src/directives/attr.js:
--------------------------------------------------------------------------------
1 | import { directive } from "../directives";
2 | import { mutateDom } from "../mutation";
3 | import { bind } from "../attributes";
4 |
5 | directive("attr", (el, { property, subject, modifiers }, { effect, evaluate, modify }) => {
6 | effect(() => {
7 | mutateDom(() => {
8 | const value = modify(evaluate(property), modifiers);
9 | bind(el, subject, value);
10 | });
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/options.js:
--------------------------------------------------------------------------------
1 | const defaultOptions = {
2 | optIn: false,
3 | compileDirectives: true,
4 | trackDeep: false,
5 | };
6 |
7 | let options = defaultOptions;
8 |
9 | export function getOption(key) {
10 | return options[key];
11 | }
12 |
13 | export function getOptions() {
14 | return options;
15 | }
16 |
17 | export function setOptions(opts) {
18 | options = Object.assign({}, defaultOptions, opts);
19 | return options;
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | test:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v2
14 | - name: Set up Node.js
15 | uses: actions/setup-node@v3
16 | with:
17 | node-version: "22"
18 | - name: Install dependencies
19 | run: npm install
20 | - name: Run tests
21 | run: npm test
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { init } from "./lifecycle";
2 | import { modifier } from "./modifiers";
3 | import { directive } from "./directives";
4 | import { nextTick } from "./scheduler";
5 |
6 | import "./modifiers/downcase";
7 | import "./modifiers/gt";
8 | import "./modifiers/gte";
9 | import "./modifiers/is";
10 | import "./modifiers/is-not";
11 | import "./modifiers/lt";
12 | import "./modifiers/lte";
13 | import "./modifiers/not";
14 | import "./modifiers/strip";
15 | import "./modifiers/upcase";
16 |
17 | import "./directives/attr";
18 | import "./directives/text";
19 |
20 | const StimulusX = {
21 | init,
22 | modifier,
23 | directive,
24 | nextTick,
25 | };
26 |
27 | export default StimulusX;
28 |
29 | export { nextTick };
30 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function camelCase(subject) {
2 | return subject
3 | .replace(/:/g, "_")
4 | .split("_")
5 | .map((word, index) => (index === 0 ? word : word[0].toUpperCase() + word.slice(1)))
6 | .join("");
7 | }
8 |
9 | export function walk(el, callback) {
10 | let skip = false;
11 | callback(el, () => (skip = true));
12 | if (skip) return;
13 |
14 | let node = el.firstElementChild;
15 | while (node) {
16 | walk(node, callback, false);
17 | node = node.nextElementSibling;
18 | }
19 | }
20 |
21 | export function isEqual(x, y) {
22 | const ok = Object.keys,
23 | tx = typeof x,
24 | ty = typeof y;
25 | return x && y && tx === "object" && tx === ty
26 | ? ok(x).length === ok(y).length && ok(x).every((key) => isEqual(x[key], y[key]))
27 | : x === y;
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Mark Perkins
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stimulus-x",
3 | "version": "0.6.0",
4 | "description": "Reactivity engine for Stimulus",
5 | "author": "Mark Perkins",
6 | "license": "MIT",
7 | "bugs": {
8 | "url": "https://github.com/allmarkedup/stimulus-x/issues"
9 | },
10 | "homepage": "https://github.com/allmarkedup/stimulus-x#readme",
11 | "repository": {
12 | "type": "git",
13 | "url": "git+ssh://git@github.com/allmarkedup/stimulus-x.git"
14 | },
15 | "source": "src/index.js",
16 | "main": "dist/stimulus-x.js",
17 | "type": "module",
18 | "keywords": [
19 | "stimulus",
20 | "stimulusjs",
21 | "reactive",
22 | "bindings",
23 | "dom"
24 | ],
25 | "scripts": {
26 | "watch": "parcel watch",
27 | "build": "parcel build",
28 | "serve": "parcel serve --target=demo",
29 | "test": "vitest run",
30 | "test:watch": "vitest",
31 | "release": "release-it"
32 | },
33 | "dependencies": {
34 | "@hotwired/stimulus": "^3.2.0",
35 | "@vue/reactivity": "^3.5.17",
36 | "dot-prop": "^9.0.0"
37 | },
38 | "devDependencies": {
39 | "@hotwired/turbo": "^8.0.13",
40 | "@testing-library/dom": "^10.4.0",
41 | "@testing-library/jest-dom": "^6.6.3",
42 | "@testing-library/user-event": "^14.6.1",
43 | "babel-jest": "^30.0.4",
44 | "happy-dom": "^18.0.1",
45 | "parcel": "^2.15.2",
46 | "release-it": "^19.0.3",
47 | "vitest": "^3.2.4"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/scheduler.js:
--------------------------------------------------------------------------------
1 | let flushPending = false;
2 | let flushing = false;
3 | let queue = [];
4 | let lastFlushedIndex = -1;
5 | let tickStack = [];
6 | let isHolding = false;
7 |
8 | export function scheduler(callback) {
9 | queueJob(callback);
10 | }
11 |
12 | export function queueJob(job) {
13 | if (!queue.includes(job)) queue.push(job);
14 |
15 | queueFlush();
16 | }
17 |
18 | export function dequeueJob(job) {
19 | let index = queue.indexOf(job);
20 |
21 | if (index !== -1 && index > lastFlushedIndex) queue.splice(index, 1);
22 | }
23 |
24 | function queueFlush() {
25 | if (!flushing && !flushPending) {
26 | flushPending = true;
27 |
28 | queueMicrotask(flushJobs);
29 | }
30 | }
31 |
32 | export function flushJobs() {
33 | flushPending = false;
34 | flushing = true;
35 |
36 | for (let i = 0; i < queue.length; i++) {
37 | queue[i]();
38 | lastFlushedIndex = i;
39 | }
40 |
41 | queue.length = 0;
42 | lastFlushedIndex = -1;
43 |
44 | flushing = false;
45 | }
46 |
47 | export function nextTick(callback = () => {}) {
48 | queueMicrotask(() => {
49 | isHolding ||
50 | setTimeout(() => {
51 | releaseNextTicks();
52 | });
53 | });
54 |
55 | return new Promise((res) => {
56 | tickStack.push(() => {
57 | callback();
58 | res();
59 | });
60 | });
61 | }
62 |
63 | export function releaseNextTicks() {
64 | isHolding = false;
65 |
66 | while (tickStack.length) tickStack.shift()();
67 | }
68 |
69 | export function holdNextTicks() {
70 | isHolding = true;
71 | }
72 |
--------------------------------------------------------------------------------
/src/modifiers.js:
--------------------------------------------------------------------------------
1 | const modifierHandlers = [];
2 |
3 | export function modifier(name, handler) {
4 | modifierHandlers.push({
5 | name,
6 | handler,
7 | });
8 | }
9 |
10 | export function applyModifiers(value, modifiers = []) {
11 | return modifiers.reduce((value, modifier) => {
12 | const { name, args } = modifier;
13 | if (modifierExists(name)) {
14 | return applyModifier(value, name, args);
15 | } else {
16 | console.error(`Unknown modifier '${modifier}'`);
17 | return value;
18 | }
19 | }, value);
20 | }
21 |
22 | function applyModifier(value, name, args = []) {
23 | return getModifier(name).handler(value, args);
24 | }
25 |
26 | function modifierExists(name) {
27 | return !!getModifier(name);
28 | }
29 |
30 | function getModifier(name) {
31 | return modifierHandlers.find((modifier) => modifier.name === name);
32 | }
33 |
34 | export function parseModifier(modifier) {
35 | const matches = modifier.match(/^([^\(]+)(?=\((?=(.*)\)$)|$)/);
36 |
37 | if (matches && typeof matches[2] !== "undefined") {
38 | const argStr = matches[2].trim();
39 | const firstChar = argStr[0];
40 | const lastChar = argStr[argStr.length - 1];
41 | let argValue = null;
42 |
43 | if (
44 | (firstChar === "'" && lastChar === "'") ||
45 | (firstChar === "`" && lastChar === "`") ||
46 | (firstChar === `"` && lastChar === `"`)
47 | ) {
48 | argValue = argStr.slice(1, argStr.length - 1);
49 | } else {
50 | argValue = JSON.parse(argStr);
51 | }
52 |
53 | return { name: matches[1], args: [argValue] };
54 | } else {
55 | return { name: modifier, args: [] };
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/classes.js:
--------------------------------------------------------------------------------
1 | export function setClasses(el, value) {
2 | if (Array.isArray(value)) {
3 | return setClassesFromString(el, value.join(" "));
4 | } else if (typeof value === "object" && value !== null) {
5 | return setClassesFromObject(el, value);
6 | }
7 | return setClassesFromString(el, value);
8 | }
9 |
10 | function setClassesFromString(el, classString) {
11 | classString = classString || "";
12 | let missingClasses = (classString) =>
13 | classString
14 | .split(" ")
15 | .filter((i) => !el.classList.contains(i))
16 | .filter(Boolean);
17 |
18 | let classes = missingClasses(classString);
19 | el.classList.add(...classes);
20 |
21 | return () => el.classList.remove(...classes);
22 | }
23 |
24 | function setClassesFromObject(el, classObject) {
25 | let split = (classString) => classString.split(" ").filter(Boolean);
26 |
27 | let forAdd = Object.entries(classObject)
28 | .flatMap(([classString, bool]) => (bool ? split(classString) : false))
29 | .filter(Boolean);
30 | let forRemove = Object.entries(classObject)
31 | .flatMap(([classString, bool]) => (!bool ? split(classString) : false))
32 | .filter(Boolean);
33 |
34 | let added = [];
35 | let removed = [];
36 |
37 | forRemove.forEach((i) => {
38 | if (el.classList.contains(i)) {
39 | el.classList.remove(i);
40 | removed.push(i);
41 | }
42 | });
43 |
44 | forAdd.forEach((i) => {
45 | if (!el.classList.contains(i)) {
46 | el.classList.add(i);
47 | added.push(i);
48 | }
49 | });
50 |
51 | return () => {
52 | removed.forEach((i) => el.classList.add(i));
53 | added.forEach((i) => el.classList.remove(i));
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/src/reactivity.js:
--------------------------------------------------------------------------------
1 | import {
2 | effect as vueEffect,
3 | stop as release,
4 | reactive as vueReactive,
5 | shallowReactive as vueShallowReactive,
6 | isReactive as vueIsReactive,
7 | } from "@vue/reactivity/dist/reactivity.esm-browser.prod.js";
8 | import { scheduler } from "./scheduler";
9 |
10 | const isReactive = vueIsReactive;
11 | const reactive = vueReactive;
12 | const shallowReactive = vueShallowReactive;
13 |
14 | const effect = (callback) =>
15 | vueEffect(callback, {
16 | scheduler: scheduler((task) => task),
17 | });
18 |
19 | export function elementBoundEffect(el) {
20 | let cleanup = () => {};
21 |
22 | let wrappedEffect = (callback) => {
23 | let effectReference = effect(callback);
24 |
25 | if (!el.__stimulusX_effects) {
26 | el.__stimulusX_effects = new Set();
27 | }
28 |
29 | el.__stimulusX_effects.add(effectReference);
30 |
31 | cleanup = () => {
32 | if (effectReference === undefined) return;
33 |
34 | el.__stimulusX_effects.delete(effectReference);
35 |
36 | release(effectReference);
37 | };
38 |
39 | return effectReference;
40 | };
41 |
42 | return [
43 | wrappedEffect,
44 | () => {
45 | cleanup();
46 | },
47 | ];
48 | }
49 |
50 | export function watch(getter, callback) {
51 | let firstTime = true;
52 | let oldValue;
53 |
54 | let effectReference = effect(() => {
55 | let value = getter();
56 |
57 | // JSON.stringify touches every single property at any level enabling deep watching
58 | JSON.stringify(value);
59 |
60 | if (!firstTime) {
61 | // We have to queue this watcher as a microtask so that
62 | // the watcher doesn't pick up its own dependencies.
63 | queueMicrotask(() => {
64 | callback(value, oldValue);
65 |
66 | oldValue = value;
67 | });
68 | } else {
69 | oldValue = value;
70 | }
71 |
72 | firstTime = false;
73 | });
74 |
75 | return () => release(effectReference);
76 | }
77 |
78 | export { effect, release, reactive, shallowReactive, isReactive };
79 |
--------------------------------------------------------------------------------
/test/directives/text.test.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { createTestContext } from "../support/test-context";
3 |
4 | let context;
5 |
6 | beforeAll(async () => {
7 | context = await createTestContext();
8 | });
9 |
10 | afterAll(() => context.teardown());
11 |
12 | describe("text content", async () => {
13 | beforeAll(() =>
14 | context.subject(
15 | class extends Controller {
16 | static values = {
17 | string: {
18 | type: String,
19 | default: "the string default value",
20 | },
21 | };
22 | }
23 | )
24 | );
25 |
26 | test("applies the default value from controller", async () => {
27 | const { getTestElement, subjectController } = await context.testDOM(`
28 |
31 | `);
32 |
33 | expect(getTestElement("target").textContent).toBe(subjectController.stringValue);
34 | });
35 |
36 | // test("applies the default value from a value attribute", async () => {
37 | // const stringAttrValue = "overridden default value";
38 |
39 | // const { getTestElement } = await context.testDOM(`
40 | //
43 | // `);
44 |
45 | // expect(getTestElement("target")).toHaveAttribute("data-output", stringAttrValue);
46 | // });
47 |
48 | // test("overrides the existing attribute value", async () => {
49 | // const { getTestElement, subjectController } = await context.testDOM(`
50 | //
53 | // `);
54 |
55 | // expect(getTestElement("target")).toHaveAttribute("data-output", subjectController.stringValue);
56 | // });
57 | });
58 |
--------------------------------------------------------------------------------
/test/opt-in.test.js:
--------------------------------------------------------------------------------
1 | import { Application, Controller } from "@hotwired/stimulus";
2 | import StimulusX from "../src";
3 | import { isReactive } from "../src/reactivity";
4 |
5 | let app;
6 |
7 | describe("controller opt-in", async () => {
8 | beforeAll(async () => {
9 | vi.useFakeTimers();
10 | app = Application.start();
11 | StimulusX.init(app, { optIn: true });
12 |
13 | document.body.innerHTML = `
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | `;
22 |
23 | app.register(
24 | "unreactive",
25 | class extends Controller {
26 | static values = {
27 | count: Number,
28 | };
29 | }
30 | );
31 |
32 | app.register(
33 | "reactive",
34 | class extends Controller {
35 | static reactive = true;
36 | static values = {
37 | count: Number,
38 | };
39 | }
40 | );
41 | });
42 |
43 | afterAll(() => {
44 | document.body.innerHTML = "";
45 | app.unload();
46 | vi.useRealTimers();
47 | });
48 |
49 | describe("controllers that have _not_ opted in to reactivity", () => {
50 | test("are not reactive", () => {
51 | const el = document.querySelector(`[data-controller="unreactive"]`);
52 | const controller = app.getControllerForElementAndIdentifier(el, "unreactive");
53 |
54 | expect(controller).toBeInstanceOf(Controller);
55 | expect(isReactive(controller)).toBe(false);
56 | });
57 | });
58 |
59 | describe("controllers that have opted in to reactivity", () => {
60 | test("are reactive", () => {
61 | let el = document.querySelector(`[data-controller="reactive"]`);
62 | const controller = app.getControllerForElementAndIdentifier(el, "reactive");
63 |
64 | expect(controller).toBeInstanceOf(Controller);
65 | expect(isReactive(controller)).toBe(true);
66 | });
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/test/support/test-context.js:
--------------------------------------------------------------------------------
1 | import { Application } from "@hotwired/stimulus";
2 | import StimulusX from "../../src";
3 | import { nextTick } from "./helpers";
4 | import userEvent from "@testing-library/user-event";
5 |
6 | export async function createTestContext() {
7 | const app = Application.start();
8 | await nextTick(() => StimulusX.init(app));
9 |
10 | function subject(ControllerClass) {
11 | app.register("subject", ControllerClass);
12 | }
13 |
14 | async function testDOM(html) {
15 | document.body.innerHTML = html;
16 | return nextTick(() => getUtilities());
17 | }
18 |
19 | async function performTurboStreamAction(action, target, content = "") {
20 | let [actualAction, method] = action === "morph" ? ["replace", "morph"] : [action, null];
21 |
22 | const streamTag = document.createElement("turbo-stream");
23 | streamTag.setAttribute("action", actualAction);
24 | streamTag.setAttribute("target", target);
25 | if (method) streamTag.setAttribute("method", method);
26 | streamTag.innerHTML = `${content} `;
27 |
28 | document.body.appendChild(streamTag);
29 |
30 | return nextTick(() => getUtilities());
31 | }
32 |
33 | async function getUtilities() {
34 | return nextTick(() => ({
35 | get subjectElement() {
36 | return getSubjectElement();
37 | },
38 |
39 | get subjectController() {
40 | return getSubjectController();
41 | },
42 |
43 | clickOnTestElement,
44 | getTestElement,
45 | }));
46 | }
47 |
48 | function getSubjectElement() {
49 | return document.querySelector(`[data-controller~="subject"]`);
50 | }
51 |
52 | function getSubjectController(element) {
53 | return app.getControllerForElementAndIdentifier(getSubjectElement(), "subject");
54 | }
55 |
56 | function getTestElement(name) {
57 | return document.querySelector(`[data-test-element="${name}"]`);
58 | }
59 |
60 | async function clickOnTestElement(name) {
61 | const user = userEvent.setup();
62 | return await user.click(getTestElement(name));
63 | }
64 |
65 | async function teardown() {
66 | document.body.innerHTML = "";
67 | await nextTick(() => app.unload());
68 | }
69 |
70 | return { testDOM, subject, teardown, performTurboStreamAction, StimulusX };
71 | }
72 |
--------------------------------------------------------------------------------
/src/attributes.js:
--------------------------------------------------------------------------------
1 | import { setClasses } from "./classes";
2 |
3 | // As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute
4 | const booleanAttributes = new Set([
5 | "allowfullscreen",
6 | "async",
7 | "autofocus",
8 | "autoplay",
9 | "checked",
10 | "controls",
11 | "default",
12 | "defer",
13 | "disabled",
14 | "formnovalidate",
15 | "inert",
16 | "ismap",
17 | "itemscope",
18 | "loop",
19 | "multiple",
20 | "muted",
21 | "nomodule",
22 | "novalidate",
23 | "open",
24 | "playsinline",
25 | "readonly",
26 | "required",
27 | "reversed",
28 | "selected",
29 | ]);
30 |
31 | const preserveIfFalsey = ["aria-pressed", "aria-checked", "aria-expanded", "aria-selected"];
32 |
33 | export function bind(element, name, value) {
34 | switch (name) {
35 | case "class":
36 | bindClasses(element, value);
37 | break;
38 |
39 | case "checked":
40 | case "selected":
41 | bindAttributeAndProperty(element, name, value);
42 | break;
43 |
44 | default:
45 | bindAttribute(element, name, value);
46 | break;
47 | }
48 | }
49 |
50 | function bindClasses(element, value) {
51 | if (element.__stimulusX_undoClasses) element.__stimulusX_undoClasses();
52 | element.__stimulusX_undoClasses = setClasses(element, value);
53 | }
54 |
55 | function bindAttribute(el, name, value) {
56 | if ([null, undefined, false].includes(value) && attributeShouldntBePreservedIfFalsy(name)) {
57 | el.removeAttribute(name);
58 | } else {
59 | if (isBooleanAttr(name)) value = name;
60 | setIfChanged(el, name, value);
61 | }
62 | }
63 |
64 | function bindAttributeAndProperty(el, name, value) {
65 | bindAttribute(el, name, value);
66 | setPropertyIfChanged(el, name, value);
67 | }
68 |
69 | function setIfChanged(el, attrName, value) {
70 | if (el.getAttribute(attrName) != value) {
71 | el.setAttribute(attrName, value);
72 | }
73 | }
74 |
75 | function setPropertyIfChanged(el, propName, value) {
76 | if (el[propName] !== value) {
77 | el[propName] = value;
78 | }
79 | }
80 |
81 | function isBooleanAttr(attrName) {
82 | return booleanAttributes.has(attrName);
83 | }
84 |
85 | function attributeShouldntBePreservedIfFalsy(name) {
86 | return !preserveIfFalsey.includes(name);
87 | }
88 |
--------------------------------------------------------------------------------
/test/negation.test.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { createTestContext } from "./support/test-context";
3 |
4 | const context = await createTestContext();
5 |
6 | afterAll(() => context.teardown());
7 |
8 | describe("value negation", async () => {
9 | beforeAll(() =>
10 | context.subject(
11 | class extends Controller {
12 | static values = {
13 | yes: {
14 | type: Boolean,
15 | default: true,
16 | },
17 | no: {
18 | type: Boolean,
19 | default: false,
20 | },
21 | status: {
22 | type: String,
23 | default: "done",
24 | },
25 | };
26 | }
27 | )
28 | );
29 |
30 | describe("with !", () => {
31 | test("with no modifiers", async () => {
32 | const { getTestElement } = await context.testDOM(`
33 |
37 | `);
38 |
39 | expect(getTestElement("hidden")).toHaveAttribute("hidden");
40 | expect(getTestElement("not-hidden")).not.toHaveAttribute("hidden");
41 | });
42 |
43 | test("with modifier(s) present", async () => {
44 | const { getTestElement } = await context.testDOM(`
45 |
49 | `);
50 |
51 | expect(getTestElement("hidden1")).toHaveAttribute("hidden");
52 | expect(getTestElement("hidden2")).toHaveAttribute("hidden");
53 | });
54 | });
55 |
56 | test("with :not modifier", async () => {
57 | const { getTestElement } = await context.testDOM(`
58 |
62 | `);
63 |
64 | expect(getTestElement("hidden")).toHaveAttribute("hidden");
65 | expect(getTestElement("not-hidden")).not.toHaveAttribute("hidden");
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/test/modifiers.test.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { createTestContext } from "./support/test-context";
3 | import { beforeAll } from "vitest";
4 |
5 | let context = await createTestContext();
6 |
7 | afterAll(() => context.teardown());
8 |
9 | describe("applying modifiers", async () => {
10 | beforeAll(() =>
11 | context.subject(
12 | class extends Controller {
13 | static values = {
14 | lowerString: {
15 | type: String,
16 | default: "default string",
17 | },
18 | upperString: {
19 | type: String,
20 | default: "DEFAULT STRING",
21 | },
22 | mixedString: {
23 | type: String,
24 | default: "DEFAULT string",
25 | },
26 | };
27 | }
28 | )
29 | );
30 |
31 | test("single modifier", async () => {
32 | const { getTestElement } = await context.testDOM(`
33 |
36 | `);
37 |
38 | expect(getTestElement("output").textContent).toBe("DEFAULT STRING");
39 | });
40 |
41 | test("chained modifiers", async () => {
42 | const { getTestElement } = await context.testDOM(`
43 |
46 | `);
47 |
48 | expect(getTestElement("output").textContent).toBe("default string");
49 | });
50 | });
51 |
52 | describe("custom modifiers", async () => {
53 | beforeAll(() => {
54 | context.StimulusX.modifier("reverse", (value) => {
55 | if (typeof value === "string") {
56 | return value.split("").reverse().join("");
57 | } else if (Array.isArray(value)) {
58 | return value.reverse();
59 | } else {
60 | console.warn("only strings or arrays can be reversed");
61 | return value;
62 | }
63 | });
64 |
65 | context.subject(
66 | class extends Controller {
67 | static values = {
68 | title: {
69 | type: String,
70 | default: "Default Title",
71 | },
72 | };
73 | }
74 | );
75 | });
76 |
77 | test("custom modifier is available for use", async () => {
78 | const { getTestElement } = await context.testDOM(`
79 |
80 |
81 |
82 | `);
83 |
84 | expect(getTestElement("title").textContent).toBe("eltiT tluafeD");
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/test/watch.test.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { createTestContext } from "./support/test-context";
3 |
4 | const { spyOn, waitFor, restoreAllMocks } = vi;
5 |
6 | let context = await createTestContext();
7 |
8 | afterEach(() => restoreAllMocks());
9 | afterAll(() => context.teardown());
10 |
11 | describe("watched properties", async () => {
12 | beforeAll(() =>
13 | context.subject(
14 | class extends Controller {
15 | static values = {
16 | count: Number,
17 | };
18 |
19 | static watch = ["countValue", "units"];
20 |
21 | connect() {
22 | this.units = "MB";
23 | this.summed = 0;
24 | this.unwatchedProperty = "foo";
25 | }
26 |
27 | watchedPropertyChanged(propertyName, value) {
28 | if (propertyName === "countValue") {
29 | this.summed = this.summed + value;
30 | }
31 | }
32 |
33 | unitsPropertyChanged(value, oldValue) {
34 | // do something here...
35 | }
36 | }
37 | )
38 | );
39 |
40 | describe("watchedPropertyChanged method", () => {
41 | test("is called when any watched property changes", async () => {
42 | const { subjectController } = await context.testDOM(`
43 |
44 | `);
45 | const spy = spyOn(subjectController, "watchedPropertyChanged");
46 |
47 | subjectController.countValue++;
48 | await waitFor(() => {
49 | expect(spy).toHaveBeenCalledWith("countValue", 1, 0, { initial: false });
50 | expect(subjectController.summed).toBe(1);
51 | expect(spy).toHaveBeenCalledTimes(1);
52 | });
53 |
54 | subjectController.units = "KB";
55 | await waitFor(() => {
56 | expect(spy).toHaveBeenCalledWith("units", "KB", "MB", { initial: false });
57 | expect(spy).toHaveBeenCalledTimes(2);
58 | });
59 | });
60 |
61 | test("isn't called when an unwatched property changes", async () => {
62 | const { subjectController } = await context.testDOM(`
63 |
64 | `);
65 | const spy = spyOn(subjectController, "watchedPropertyChanged");
66 |
67 | subjectController.unwatchedProperty = "bar";
68 | await waitFor(() => {
69 | expect(spy).not.toHaveBeenCalled();
70 | });
71 | });
72 | });
73 |
74 | describe("PropertyChanged method", () => {
75 | test("is called when the relevant property changes", async () => {
76 | const { subjectController } = await context.testDOM(`
77 |
78 | `);
79 | const spy = spyOn(subjectController, "unitsPropertyChanged");
80 |
81 | subjectController.units = "KB";
82 | await waitFor(() => {
83 | expect(spy).toHaveBeenCalledWith("KB", "MB", { initial: false });
84 | expect(spy).toHaveBeenCalledTimes(1);
85 | });
86 | });
87 |
88 | test("isn't called when a different watched property changes", async () => {
89 | const { subjectController } = await context.testDOM(`
90 |
91 | `);
92 | const spy = spyOn(subjectController, "unitsPropertyChanged");
93 |
94 | subjectController.count++;
95 | await waitFor(() => {
96 | expect(spy).not.toHaveBeenCalled();
97 | });
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/src/controller.js:
--------------------------------------------------------------------------------
1 | import { getProperty } from "dot-prop";
2 | import { application } from "./lifecycle";
3 | import { reactive, shallowReactive, watch } from "./reactivity";
4 | import { mutateDom } from "./mutation";
5 | import { nextTick } from "./scheduler";
6 | import { initTree } from "./lifecycle";
7 | import { getOption } from "./options";
8 |
9 | export function createReactiveControllerClass(ControllerClass) {
10 | return class extends ControllerClass {
11 | constructor(context) {
12 | super(context);
13 |
14 | // Override the attribute setter so that our mutation observer doesn't pick up on changes
15 | // that are also already being handled directly by Stimulus.
16 | const setData = this.data.set;
17 | this.data.set = (key, value) => {
18 | mutateDom(() => setData.call(this.data, key, value));
19 | };
20 |
21 | // Create a reactive controller object
22 | const trackDeep = getOption("trackDeep") || this.constructor.reactive === "deep";
23 | const reactiveSelf = trackDeep ? reactive(this) : shallowReactive(this);
24 |
25 | // Initialize watched property callbacks
26 | const watchedProps = this.constructor.watch || [];
27 | watchedProps.forEach((prop) => watchControllerProperty(reactiveSelf, prop));
28 |
29 | // Return the reactive controller instance
30 | return reactiveSelf;
31 | }
32 |
33 | connect() {
34 | // Initialize the DOM tree and run directives when connected
35 | super.connect();
36 | nextTick(() => initTree(this.element));
37 | }
38 | };
39 | }
40 |
41 | export function getClosestController(el, identifier) {
42 | const controllerElement = el.closest(`[data-controller~="${identifier}"]`);
43 | if (controllerElement) {
44 | return application.getControllerForElementAndIdentifier(controllerElement, identifier);
45 | }
46 | }
47 |
48 | export function evaluateControllerProperty(controller, property) {
49 | let value = getProperty(controller, property);
50 | if (typeof value === "function") {
51 | value = value.apply(controller);
52 | }
53 | return value;
54 | }
55 |
56 | export function watchControllerProperty(controller, propertyRef) {
57 | const getter = () => evaluateControllerProperty(controller, propertyRef);
58 | const cleanup = watch(getter, (value, oldValue) => {
59 | callCallbacks(controller, propertyRef, value, oldValue, false);
60 | });
61 |
62 | // Run once on creation
63 | callCallbacks(controller, propertyRef, getter(), undefined, true);
64 |
65 | const rootElement = controller.element;
66 | if (!rootElement.__stimulusX_cleanups) rootElement.__stimulusX_cleanups = [];
67 | rootElement.__stimulusX_cleanups.push(cleanup);
68 | }
69 |
70 | function callCallbacks(controller, propertyRef, value, oldValue, initial) {
71 | // Generic callback, called when _any_ watched property changes
72 | if (typeof controller.watchedPropertyChanged === "function") {
73 | controller.watchedPropertyChanged(propertyRef, value, oldValue, { initial });
74 | }
75 |
76 | // Property-specific change callback
77 | const propertyWatcherCallback =
78 | controller[`${getCamelizedPropertyRef(propertyRef)}PropertyChanged`];
79 | if (typeof propertyWatcherCallback === "function") {
80 | propertyWatcherCallback.call(controller, value, oldValue, { initial });
81 | }
82 | }
83 |
84 | function getCamelizedPropertyRef(propertyRef) {
85 | return camelCase(propertyRef.replace(".", " "));
86 | }
87 |
88 | function camelCase(subject) {
89 | return subject.toLowerCase().replace(/-(\w)/g, (match, char) => char.toUpperCase());
90 | }
91 |
--------------------------------------------------------------------------------
/test/modifiers/compare-numbers.test.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { createTestContext } from "../support/test-context";
3 |
4 | let context = await createTestContext();
5 |
6 | afterAll(() => context.teardown());
7 |
8 | describe("number comparison modifiers", async () => {
9 | beforeAll(() =>
10 | context.subject(
11 | class extends Controller {
12 | static values = {
13 | count: {
14 | type: Number,
15 | default: 50,
16 | },
17 | };
18 | }
19 | )
20 | );
21 |
22 | describe("`gt` modifier", () => {
23 | test("performs 'greater than' comparison", async () => {
24 | const { getTestElement } = await context.testDOM(`
25 |
26 | +
27 | +
28 | +
29 |
30 | `);
31 |
32 | expect(getTestElement("target1")).toHaveAttribute("disabled");
33 | expect(getTestElement("target2")).not.toHaveAttribute("disabled");
34 | expect(getTestElement("target3")).not.toHaveAttribute("disabled");
35 | });
36 | });
37 |
38 | describe("`gte` modifier", () => {
39 | test("performs 'greater than or equal to' comparison", async () => {
40 | const { getTestElement } = await context.testDOM(`
41 |
42 | +
43 | +
44 | +
45 |
46 | `);
47 |
48 | expect(getTestElement("target1")).toHaveAttribute("disabled");
49 | expect(getTestElement("target2")).toHaveAttribute("disabled");
50 | expect(getTestElement("target3")).not.toHaveAttribute("disabled");
51 | });
52 | });
53 |
54 | describe("`lt` modifier", () => {
55 | test("performs 'less than' comparison", async () => {
56 | const { getTestElement } = await context.testDOM(`
57 |
58 | +
59 | +
60 | +
61 |
62 | `);
63 |
64 | expect(getTestElement("target1")).not.toHaveAttribute("disabled");
65 | expect(getTestElement("target2")).not.toHaveAttribute("disabled");
66 | expect(getTestElement("target3")).toHaveAttribute("disabled");
67 | });
68 | });
69 |
70 | describe("`lte` modifier", () => {
71 | test("performs 'less than or equal to' comparison", async () => {
72 | const { getTestElement } = await context.testDOM(`
73 |
74 | +
75 | +
76 | +
77 |
78 | `);
79 |
80 | expect(getTestElement("target1")).not.toHaveAttribute("disabled");
81 | expect(getTestElement("target2")).toHaveAttribute("disabled");
82 | expect(getTestElement("target3")).toHaveAttribute("disabled");
83 | });
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/src/lifecycle.js:
--------------------------------------------------------------------------------
1 | import { nextTick } from "./scheduler";
2 | import { createReactiveControllerClass } from "./controller";
3 | import { walk } from "./utils";
4 | import {
5 | startObservingMutations,
6 | onAttributesAdded,
7 | onElAdded,
8 | onElRemoved,
9 | cleanupAttributes,
10 | cleanupElement,
11 | mutateDom,
12 | } from "./mutation";
13 | import { deferHandlingDirectives, directives } from "./directives";
14 | import { setOptions } from "./options";
15 |
16 | let markerCount = 1;
17 | let application = null;
18 |
19 | export function init(app, opts = {}) {
20 | const { optIn } = setOptions(opts);
21 | application = app;
22 |
23 | // Override controller registration to insert a reactive subclass instead of the original
24 | application.register = function (identifier, ControllerClass) {
25 | let controllerConstructor;
26 | if (optIn === false || ControllerClass.reactive === true) {
27 | controllerConstructor = createReactiveControllerClass(ControllerClass, application);
28 | } else {
29 | controllerConstructor = ControllerClass;
30 | }
31 |
32 | application.load({
33 | identifier,
34 | controllerConstructor,
35 | });
36 | };
37 |
38 | // Handle re-initializing reactive effects after Turbo morphing
39 | document.addEventListener("turbo:before-morph-element", beforeMorphElementCallback);
40 | document.addEventListener("turbo:morph-element", morphElementCallback);
41 |
42 | // start watching the dom for changes
43 | startObservingMutations();
44 |
45 | onElAdded((el) => {
46 | // Controller root elements init their own tree when connected so we can skip them.
47 | // if (el.hasAttribute("data-controller")) return;
48 | nextTick(() => initTree(el));
49 | });
50 |
51 | onElRemoved((el) => nextTick(() => destroyTree(el)));
52 |
53 | onAttributesAdded((el, attrs) => {
54 | handleValueAttributes(el, attrs);
55 | directives(el, attrs).forEach((handle) => handle());
56 | });
57 | }
58 |
59 | export function initTree(el) {
60 | deferHandlingDirectives(() => {
61 | walk(el, (el) => {
62 | if (el.__stimulusX_marker) return;
63 |
64 | directives(el, el.attributes).forEach((handle) => handle());
65 |
66 | el.__stimulusX_marker = markerCount++;
67 | });
68 | });
69 | }
70 |
71 | export function destroyTree(root) {
72 | walk(root, (el) => {
73 | cleanupElement(el);
74 | cleanupAttributes(el);
75 | delete el.__stimulusX_directives;
76 | delete el.__stimulusX_marker;
77 | });
78 | }
79 |
80 | export function beforeMorphElementCallback({ target, detail: { newElement } }) {
81 | if (!newElement && target.__stimulusX_marker) {
82 | return destroyTree(target);
83 | }
84 | delete target.__stimulusX_marker;
85 | }
86 |
87 | export function morphElementCallback({ target, detail: { newElement } }) {
88 | if (newElement) initTree(target);
89 | }
90 |
91 | // Changes to controller value attributes in the DOM do not call
92 | // any properties on the controller so changes are not detected.
93 | // To fix this any value attribute changes are registered by calling
94 | // the value setter on the proxy with the current value - the value is
95 | // unchanged but calling the getter triggers any related effects.
96 | function handleValueAttributes(el, attrs) {
97 | if (!el.hasAttribute("data-controller")) return;
98 |
99 | const controllerNames = el
100 | .getAttribute("data-controller")
101 | .trim()
102 | .split(" ")
103 | .filter((e) => e);
104 |
105 | const valueAttributeMatcher = new RegExp(
106 | `^data-(${controllerNames.join("|")})-([a-zA-Z0-9\-_]+)-value$`
107 | );
108 |
109 | for (let i = 0; i < attrs.length; i++) {
110 | const attr = attrs[i];
111 | const matches = attr.name.match(valueAttributeMatcher);
112 | if (matches && matches.length) {
113 | const identifier = matches[1];
114 | const valueName = matches[2];
115 | const controller = application.getControllerForElementAndIdentifier(el, identifier);
116 |
117 | mutateDom(() => {
118 | controller[`${valueName}Value`] = controller[`${valueName}Value`];
119 | });
120 | }
121 | }
122 | }
123 |
124 | export { application };
125 |
--------------------------------------------------------------------------------
/test/turbo.test.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { createTestContext } from "./support/test-context";
3 | import "@hotwired/turbo";
4 |
5 | let context = await createTestContext();
6 |
7 | afterAll(() => context.teardown());
8 |
9 | describe("turbo actions", async () => {
10 | beforeAll(() =>
11 | context.subject(
12 | class extends Controller {
13 | static values = {
14 | count: {
15 | type: Number,
16 | default: 0,
17 | },
18 | };
19 |
20 | increment() {
21 | this.countValue++;
22 | }
23 | }
24 | )
25 | );
26 |
27 | describe("turbo `replace` action", () => {
28 | test("is still interactive after replacement", async () => {
29 | const testDOM = `
30 |
34 | `;
35 | let { getTestElement, clickOnTestElement } = await context.testDOM(testDOM);
36 |
37 | await vi.waitFor(() => expect(getTestElement("count").textContent).toBe("0"));
38 | await clickOnTestElement("increment");
39 | expect(getTestElement("count").textContent).toBe("1");
40 |
41 | await context.performTurboStreamAction("replace", "subject", testDOM);
42 |
43 | await vi.waitFor(() => expect(getTestElement("count").textContent).toBe("0"));
44 | await clickOnTestElement("increment");
45 | expect(getTestElement("count").textContent).toBe("1");
46 | });
47 |
48 | test("reinitializes with HTML from the stream action", async () => {
49 | let { getTestElement } = await context.testDOM(`
50 |
53 | `);
54 |
55 | await vi.waitFor(() => expect(getTestElement("count").textContent).toBe("2"));
56 |
57 | await context.performTurboStreamAction(
58 | "replace",
59 | "subject",
60 | `
61 |
64 | `
65 | );
66 |
67 | await vi.waitFor(() => expect(getTestElement("count").textContent).toBe("5"));
68 | });
69 | });
70 |
71 | describe("turbo `morph` action", () => {
72 | test("is still interactive after replacement", async () => {
73 | const testDOM = `
74 |
78 | `;
79 | let { getTestElement, clickOnTestElement } = await context.testDOM(testDOM);
80 |
81 | await vi.waitFor(() => expect(getTestElement("count").textContent).toBe("0"));
82 | await clickOnTestElement("increment");
83 | expect(getTestElement("count").textContent).toBe("1");
84 |
85 | await context.performTurboStreamAction("morph", "subject", testDOM);
86 |
87 | await vi.waitFor(() => expect(getTestElement("count").textContent).toBe("0"));
88 | await clickOnTestElement("increment");
89 | expect(getTestElement("count").textContent).toBe("1");
90 | });
91 |
92 | test("reinitializes with HTML from the stream action", async () => {
93 | let { getTestElement } = await context.testDOM(`
94 |
97 | `);
98 |
99 | expect(getTestElement("count").textContent).toBe("2");
100 |
101 | await context.performTurboStreamAction(
102 | "morph",
103 | "subject",
104 | `
105 |
108 | `
109 | );
110 |
111 | await vi.waitFor(() => expect(getTestElement("count").textContent).toBe("5"));
112 | });
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/test/reactivity.test.js:
--------------------------------------------------------------------------------
1 | import { Application, Controller } from "@hotwired/stimulus";
2 | import StimulusX from "../src";
3 | import { createTestContext } from "./support/test-context";
4 |
5 | const context = await createTestContext();
6 |
7 | describe("shallow reactivity (default)", async () => {
8 | beforeAll(() =>
9 | context.subject(
10 | class extends Controller {
11 | connect() {
12 | this.foo = "bar";
13 | this.nested = {
14 | foo: {
15 | bar: "baz",
16 | },
17 | };
18 | }
19 | }
20 | )
21 | );
22 |
23 | afterAll(async () => await context.teardown());
24 |
25 | test("changes to top level properties are tracked", async () => {
26 | const { getTestElement, subjectController } = await context.testDOM(`
27 |
30 | `);
31 |
32 | expect(getTestElement("target").textContent).toBe("bar");
33 |
34 | subjectController.foo = "new value";
35 |
36 | expect(getTestElement("target").textContent).toBe("new value");
37 | });
38 |
39 | test("changes to nested properties are not tracked", async () => {
40 | const { getTestElement, subjectController } = await context.testDOM(`
41 |
44 | `);
45 |
46 | expect(getTestElement("target").textContent).toBe("baz");
47 |
48 | subjectController.nested.foo.bar = "new value";
49 | expect(getTestElement("target").textContent).toBe("baz");
50 | });
51 | });
52 |
53 | describe("deep reactivity (opt in)", async () => {
54 | beforeAll(() =>
55 | context.subject(
56 | class extends Controller {
57 | static reactive = "deep";
58 |
59 | connect() {
60 | this.foo = "bar";
61 | this.nested = {
62 | foo: {
63 | bar: "baz",
64 | },
65 | };
66 | }
67 | }
68 | )
69 | );
70 |
71 | afterAll(async () => await context.teardown());
72 |
73 | test("changes to top level properties are tracked", async () => {
74 | const { getTestElement, subjectController } = await context.testDOM(`
75 |
78 | `);
79 |
80 | expect(getTestElement("target").textContent).toBe("bar");
81 |
82 | subjectController.foo = "new value";
83 | expect(getTestElement("target").textContent).toBe("new value");
84 | });
85 |
86 | test("changes to nested properties are tracked", async () => {
87 | const { getTestElement, subjectController } = await context.testDOM(`
88 |
91 | `);
92 |
93 | expect(getTestElement("target").textContent).toBe("baz");
94 |
95 | subjectController.nested.foo.bar = "new value";
96 | expect(getTestElement("target").textContent).toBe("new value");
97 | });
98 | });
99 |
100 | describe("deep reactivity (enabled globally)", async () => {
101 | let app;
102 |
103 | beforeAll(async () => {
104 | vi.useFakeTimers();
105 | app = Application.start();
106 | StimulusX.init(app, { trackDeep: true });
107 |
108 | document.body.innerHTML = `
109 |
112 | `;
113 |
114 | app.register(
115 | "subject",
116 | class extends Controller {
117 | connect() {
118 | this.foo = "bar";
119 | this.nested = {
120 | foo: {
121 | bar: "baz",
122 | },
123 | };
124 | }
125 | }
126 | );
127 | });
128 |
129 | afterAll(() => {
130 | document.body.innerHTML = "";
131 | app.unload();
132 | vi.useRealTimers();
133 | });
134 |
135 | test("changes to nested properties are tracked", async () => {
136 | const subject = await vi.waitFor(() => document.querySelector(`[data-controller="subject"]`));
137 |
138 | const target = document.querySelector(`[data-test-element="target"]`);
139 | const controller = app.getControllerForElementAndIdentifier(subject, "subject");
140 |
141 | expect(target.textContent).toBe("baz");
142 |
143 | controller.nested.foo.bar = "new value";
144 | expect(target.textContent).toBe("new value");
145 | });
146 | });
147 |
--------------------------------------------------------------------------------
/src/directives.js:
--------------------------------------------------------------------------------
1 | import { onAttributeRemoved } from "./mutation";
2 | import { elementBoundEffect, isReactive } from "./reactivity";
3 | import { applyModifiers, parseModifier } from "./modifiers";
4 | import { getClosestController, evaluateControllerProperty } from "./controller";
5 | import { getOption } from "./options";
6 |
7 | let directiveHandlers = {};
8 | let isDeferringHandlers = false;
9 | let directiveHandlerStacks = new Map();
10 | let currentHandlerStackKey = Symbol();
11 |
12 | let attributePrefix = "data-bind-";
13 |
14 | export function directive(name, callback) {
15 | directiveHandlers[name] = callback;
16 | }
17 |
18 | export function directiveExists(name) {
19 | return Object.keys(directiveHandlers).includes(name);
20 | }
21 |
22 | export function directives(el, attributes) {
23 | let directives = [];
24 |
25 | if (el.__stimulusX_directives) {
26 | directives = el.__stimulusX_directives;
27 | } else {
28 | directives = Array.from(attributes).filter(isDirectiveAttribute).map(toParsedDirectives);
29 | if (getOption("compileDirectives") === true) el.__stimulusX_directives = directives;
30 | }
31 |
32 | return directives
33 | .flat()
34 | .filter((d) => d)
35 | .map((directive) => getDirectiveHandler(el, directive));
36 | }
37 |
38 | export function deferHandlingDirectives(callback) {
39 | isDeferringHandlers = true;
40 |
41 | let key = Symbol();
42 |
43 | currentHandlerStackKey = key;
44 | directiveHandlerStacks.set(key, []);
45 |
46 | let flushHandlers = () => {
47 | while (directiveHandlerStacks.get(key).length) directiveHandlerStacks.get(key).shift()();
48 | directiveHandlerStacks.delete(key);
49 | };
50 |
51 | let stopDeferring = () => {
52 | isDeferringHandlers = false;
53 | flushHandlers();
54 | };
55 |
56 | callback(flushHandlers);
57 | stopDeferring();
58 | }
59 |
60 | export function getElementBoundUtilities(el) {
61 | let cleanups = [];
62 | let cleanup = (callback) => cleanups.push(callback);
63 | let [effect, cleanupEffect] = elementBoundEffect(el);
64 |
65 | cleanups.push(cleanupEffect);
66 |
67 | let utilities = {
68 | effect,
69 | cleanup,
70 | };
71 |
72 | let doCleanup = () => {
73 | cleanups.forEach((i) => i());
74 | };
75 |
76 | return [utilities, doCleanup];
77 | }
78 |
79 | export function getDirectiveHandler(el, directive) {
80 | let handler = directiveHandlers[directive.type] || (() => {});
81 | let [utilities, cleanup] = getElementBoundUtilities(el);
82 |
83 | onAttributeRemoved(el, directive.attr, cleanup);
84 |
85 | let wrapperHandler = () => {
86 | let controller = getClosestController(el, directive.identifier);
87 | if (controller) {
88 | if (!isReactive(controller)) {
89 | console.warn(
90 | `StimulusX: Directive attached to non-reactive controller '${directive.identifier}'`,
91 | el
92 | );
93 | return;
94 | }
95 | handler = handler.bind(handler, el, directive, {
96 | ...utilities,
97 | evaluate: evaluator(controller),
98 | modify: applyModifiers,
99 | });
100 | isDeferringHandlers
101 | ? directiveHandlerStacks.get(currentHandlerStackKey).push(handler)
102 | : handler();
103 | } else {
104 | console.error(`Controller '${directive.identifier}' not found`);
105 | }
106 | };
107 |
108 | return wrapperHandler;
109 | }
110 |
111 | function evaluator(controller) {
112 | return (property) => evaluateControllerProperty(controller, property);
113 | }
114 |
115 | function matchedAttributeRegex() {
116 | return new RegExp(`${attributePrefix}(${Object.keys(directiveHandlers).join("|")})$`);
117 | }
118 |
119 | function isDirectiveAttribute({ name }) {
120 | return matchedAttributeRegex().test(name);
121 | }
122 |
123 | function toParsedDirectives({ name, value }) {
124 | const type = name.match(matchedAttributeRegex())[1];
125 | const bindingExpressions = value
126 | .trim()
127 | .split(/\s+(?![^\(]*\))/) // split string on all spaces not contained in parentheses
128 | .filter((e) => e);
129 |
130 | return bindingExpressions.map((bindingExpression) => {
131 | const subjectMatch = bindingExpression.match(/^([a-zA-Z0-9\-_]+)~/);
132 | const subject = subjectMatch ? subjectMatch[1] : null;
133 | let valueExpression = subject
134 | ? bindingExpression.replace(`${subject}~`, "")
135 | : bindingExpression;
136 |
137 | let modifiers = valueExpression.match(/\:[^:\]]+(?=[^\]]*$)/g) || [];
138 | modifiers = modifiers.map((i) => i.replace(":", ""));
139 |
140 | valueExpression = valueExpression.split(":")[0];
141 |
142 | if (valueExpression[0] === "!") {
143 | valueExpression = valueExpression.slice(1);
144 | modifiers.push("not");
145 | }
146 |
147 | modifiers = modifiers.map((m) => parseModifier(m));
148 |
149 | const identifierMatch = valueExpression.match(/^([a-zA-Z0-9\-_]+)#/);
150 | if (!identifierMatch) {
151 | console.warn(`Invalid binding descriptor ${bindingExpression}`);
152 | return null;
153 | }
154 |
155 | const identifier = identifierMatch[1];
156 | let property = identifier ? valueExpression.replace(`${identifier}#`, "") : valueExpression;
157 |
158 | return {
159 | type,
160 | subject,
161 | modifiers,
162 | identifier,
163 | property,
164 | attr: name,
165 | };
166 | });
167 | }
168 |
--------------------------------------------------------------------------------
/test/modifiers/is.test.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { createTestContext } from "../support/test-context";
3 |
4 | let context = await createTestContext();
5 |
6 | afterAll(() => context.teardown());
7 |
8 | describe("`is` modifier", async () => {
9 | beforeAll(() =>
10 | context.subject(
11 | class extends Controller {
12 | static values = {
13 | string: {
14 | type: String,
15 | default: "string with spaces",
16 | },
17 | integer: {
18 | type: Number,
19 | default: 12345,
20 | },
21 | float: {
22 | type: Number,
23 | default: 12.345,
24 | },
25 | booleanTrue: {
26 | type: Boolean,
27 | default: true,
28 | },
29 | booleanFalse: {
30 | type: Boolean,
31 | default: false,
32 | },
33 | };
34 | }
35 | )
36 | );
37 |
38 | describe("string comparisons", () => {
39 | test("single quoted string", async () => {
40 | const { getTestElement } = await context.testDOM(`
41 |
45 | `);
46 |
47 | expect(getTestElement("target1").hidden).toBe(true);
48 | expect(getTestElement("target2").hidden).toBe(false);
49 | });
50 |
51 | test("double quoted string", async () => {
52 | const { getTestElement } = await context.testDOM(`
53 |
57 | `);
58 |
59 | expect(getTestElement("target1").hidden).toBe(true);
60 | expect(getTestElement("target2").hidden).toBe(false);
61 | });
62 | });
63 |
64 | describe("number comparisons", () => {
65 | test("integer", async () => {
66 | const { getTestElement } = await context.testDOM(`
67 |
71 | `);
72 |
73 | expect(getTestElement("target1").hidden).toBe(true);
74 | expect(getTestElement("target2").hidden).toBe(false);
75 | });
76 |
77 | test("float", async () => {
78 | const { getTestElement } = await context.testDOM(`
79 |
83 | `);
84 |
85 | expect(getTestElement("target1").hidden).toBe(true);
86 | expect(getTestElement("target2").hidden).toBe(false);
87 | });
88 | });
89 |
90 | describe("boolean comparisons", () => {
91 | test("true", async () => {
92 | const { getTestElement } = await context.testDOM(`
93 |
98 | `);
99 |
100 | expect(getTestElement("target1").hidden).toBe(true);
101 | expect(getTestElement("target2").hidden).toBe(false);
102 | expect(getTestElement("target3").hidden).toBe(true);
103 | });
104 |
105 | test("false", async () => {
106 | const { getTestElement } = await context.testDOM(`
107 |
112 | `);
113 |
114 | expect(getTestElement("target1").hidden).toBe(false);
115 | expect(getTestElement("target2").hidden).toBe(true);
116 | expect(getTestElement("target3").hidden).toBe(true);
117 | });
118 |
119 | describe("multiple descriptors", () => {
120 | test("true", async () => {
121 | const { getTestElement } = await context.testDOM(`
122 |
123 |
129 |
130 |
131 | `);
132 |
133 | expect(getTestElement("target").hidden).toBe(true);
134 | expect(getTestElement("target").disabled).toBe(true);
135 | });
136 | });
137 | });
138 | });
139 |
--------------------------------------------------------------------------------
/test/modifiers/is-not.test.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { createTestContext } from "../support/test-context";
3 |
4 | let context = await createTestContext();
5 |
6 | afterAll(() => context.teardown());
7 |
8 | describe("`isNot` modifier", async () => {
9 | beforeAll(() =>
10 | context.subject(
11 | class extends Controller {
12 | static values = {
13 | string: {
14 | type: String,
15 | default: "string with spaces",
16 | },
17 | integer: {
18 | type: Number,
19 | default: 12345,
20 | },
21 | float: {
22 | type: Number,
23 | default: 12.345,
24 | },
25 | booleanTrue: {
26 | type: Boolean,
27 | default: true,
28 | },
29 | booleanFalse: {
30 | type: Boolean,
31 | default: false,
32 | },
33 | };
34 | }
35 | )
36 | );
37 |
38 | describe("string comparisons", () => {
39 | test("single quoted string", async () => {
40 | const { getTestElement } = await context.testDOM(`
41 |
45 | `);
46 |
47 | expect(getTestElement("target1").hidden).toBe(false);
48 | expect(getTestElement("target2").hidden).toBe(true);
49 | });
50 |
51 | test("double quoted string", async () => {
52 | const { getTestElement } = await context.testDOM(`
53 |
57 | `);
58 |
59 | expect(getTestElement("target1").hidden).toBe(false);
60 | expect(getTestElement("target2").hidden).toBe(true);
61 | });
62 | });
63 |
64 | describe("number comparisons", () => {
65 | test("integer", async () => {
66 | const { getTestElement } = await context.testDOM(`
67 |
71 | `);
72 |
73 | expect(getTestElement("target1").hidden).toBe(false);
74 | expect(getTestElement("target2").hidden).toBe(true);
75 | });
76 |
77 | test("float", async () => {
78 | const { getTestElement } = await context.testDOM(`
79 |
83 | `);
84 |
85 | expect(getTestElement("target1").hidden).toBe(false);
86 | expect(getTestElement("target2").hidden).toBe(true);
87 | });
88 | });
89 |
90 | describe("boolean comparisons", () => {
91 | test("true", async () => {
92 | const { getTestElement } = await context.testDOM(`
93 |
98 | `);
99 |
100 | expect(getTestElement("target1").hidden).toBe(false);
101 | expect(getTestElement("target2").hidden).toBe(true);
102 | expect(getTestElement("target3").hidden).toBe(false);
103 | });
104 |
105 | test("false", async () => {
106 | const { getTestElement } = await context.testDOM(`
107 |
112 | `);
113 |
114 | expect(getTestElement("target1").hidden).toBe(true);
115 | expect(getTestElement("target2").hidden).toBe(false);
116 | expect(getTestElement("target3").hidden).toBe(false);
117 | });
118 |
119 | describe("multiple descriptors", () => {
120 | test("true", async () => {
121 | const { getTestElement } = await context.testDOM(`
122 |
123 |
129 |
130 |
131 | `);
132 |
133 | expect(getTestElement("target").hidden).toBe(false);
134 | expect(getTestElement("target").disabled).toBe(false);
135 | });
136 | });
137 | });
138 | });
139 |
--------------------------------------------------------------------------------
/src/mutation.js:
--------------------------------------------------------------------------------
1 | import { dequeueJob } from "./scheduler";
2 | let onAttributeAddeds = [];
3 | let onElRemoveds = [];
4 | let onElAddeds = [];
5 | let onValueAttributeChangeds = [];
6 | let currentlyObserving = false;
7 | let isCollecting = false;
8 | let deferredMutations = [];
9 | let observer = new MutationObserver(onMutate);
10 |
11 | export function onElAdded(callback) {
12 | onElAddeds.push(callback);
13 | }
14 |
15 | export function onElRemoved(el, callback) {
16 | if (typeof callback === "function") {
17 | if (!el.__stimulusX_cleanups) el.__stimulusX_cleanups = [];
18 | el.__stimulusX_cleanups.push(callback);
19 | } else {
20 | callback = el;
21 | onElRemoveds.push(callback);
22 | }
23 | }
24 |
25 | export function onAttributesAdded(callback) {
26 | onAttributeAddeds.push(callback);
27 | }
28 |
29 | export function onAttributeRemoved(el, name, callback) {
30 | if (!el.__stimulusX_attributeCleanups) el.__stimulusX_attributeCleanups = {};
31 | if (!el.__stimulusX_attributeCleanups[name]) el.__stimulusX_attributeCleanups[name] = [];
32 |
33 | el.__stimulusX_attributeCleanups[name].push(callback);
34 | }
35 |
36 | export function onValueAttributeChanged(callback) {
37 | onValueAttributeChangeds.push(callback);
38 | }
39 |
40 | export function cleanupAttributes(el, names) {
41 | if (!el.__stimulusX_attributeCleanups) return;
42 |
43 | Object.entries(el.__stimulusX_attributeCleanups).forEach(([name, value]) => {
44 | if (names === undefined || names.includes(name)) {
45 | value.forEach((i) => i());
46 |
47 | delete el.__stimulusX_attributeCleanups[name];
48 | }
49 | });
50 | }
51 |
52 | export function cleanupElement(el) {
53 | el.__stimulusX_cleanups?.forEach(dequeueJob);
54 |
55 | while (el.__stimulusX_cleanups?.length) el.__stimulusX_cleanups.pop()();
56 | }
57 |
58 | export function startObservingMutations() {
59 | observer.observe(document, {
60 | subtree: true,
61 | childList: true,
62 | attributes: true,
63 | attributeOldValue: true,
64 | });
65 |
66 | currentlyObserving = true;
67 | }
68 |
69 | export function stopObservingMutations() {
70 | flushObserver();
71 |
72 | observer.disconnect();
73 |
74 | currentlyObserving = false;
75 | }
76 |
77 | let queuedMutations = [];
78 |
79 | export function flushObserver() {
80 | let records = observer.takeRecords();
81 |
82 | queuedMutations.push(() => records.length > 0 && onMutate(records));
83 |
84 | let queueLengthWhenTriggered = queuedMutations.length;
85 |
86 | queueMicrotask(() => {
87 | // If these two lengths match, then we KNOW that this is the LAST
88 | // flush in the current event loop. This way, we can process
89 | // all mutations in one batch at the end of everything...
90 | if (queuedMutations.length === queueLengthWhenTriggered) {
91 | // Now Alpine can process all the mutations...
92 | while (queuedMutations.length > 0) queuedMutations.shift()();
93 | }
94 | });
95 | }
96 |
97 | export function mutateDom(callback) {
98 | if (!currentlyObserving) return callback();
99 |
100 | stopObservingMutations();
101 |
102 | let result = callback();
103 |
104 | startObservingMutations();
105 |
106 | return result;
107 | }
108 |
109 | export function deferMutations() {
110 | isCollecting = true;
111 | }
112 |
113 | export function flushAndStopDeferringMutations() {
114 | isCollecting = false;
115 |
116 | onMutate(deferredMutations);
117 |
118 | deferredMutations = [];
119 | }
120 |
121 | function onMutate(mutations) {
122 | if (isCollecting) {
123 | deferredMutations = deferredMutations.concat(mutations);
124 |
125 | return;
126 | }
127 |
128 | let addedNodes = [];
129 | let removedNodes = new Set();
130 | let addedAttributes = new Map();
131 | let removedAttributes = new Map();
132 |
133 | for (let i = 0; i < mutations.length; i++) {
134 | if (mutations[i].target.__stimulusX_ignoreMutationObserver) continue;
135 |
136 | if (mutations[i].type === "childList") {
137 | mutations[i].removedNodes.forEach((node) => {
138 | if (node.nodeType !== 1) return;
139 |
140 | // No need to process removed nodes that haven't been initialized by Alpine...
141 | if (!node.__stimulusX_marker) return;
142 |
143 | removedNodes.add(node);
144 | });
145 |
146 | mutations[i].addedNodes.forEach((node) => {
147 | if (node.nodeType !== 1) return;
148 |
149 | // If the node is a removal as well, that means it's a "move" operation and we'll leave it alone...
150 | if (removedNodes.has(node)) {
151 | removedNodes.delete(node);
152 |
153 | return;
154 | }
155 |
156 | // If the node has already been initialized, we'll leave it alone...
157 | if (node.__stimulusX_marker) return;
158 |
159 | addedNodes.push(node);
160 | });
161 | }
162 |
163 | if (mutations[i].type === "attributes") {
164 | let el = mutations[i].target;
165 | let name = mutations[i].attributeName;
166 | let oldValue = mutations[i].oldValue;
167 |
168 | let add = () => {
169 | if (!addedAttributes.has(el)) addedAttributes.set(el, []);
170 |
171 | addedAttributes.get(el).push({ name, value: el.getAttribute(name) });
172 | };
173 |
174 | let remove = () => {
175 | if (!removedAttributes.has(el)) removedAttributes.set(el, []);
176 |
177 | removedAttributes.get(el).push(name);
178 | };
179 |
180 | // let valueAttributeChanged = () => {
181 |
182 | // };
183 |
184 | // New attribute.
185 | if (el.hasAttribute(name) && oldValue === null) {
186 | add();
187 | // Changed attribute.
188 | } else if (el.hasAttribute(name)) {
189 | remove();
190 | add();
191 | // Removed attribute.
192 | } else {
193 | remove();
194 | }
195 | }
196 | }
197 |
198 | removedAttributes.forEach((attrs, el) => {
199 | cleanupAttributes(el, attrs);
200 | });
201 |
202 | addedAttributes.forEach((attrs, el) => {
203 | onAttributeAddeds.forEach((i) => i(el, attrs));
204 | });
205 |
206 | // There are two special scenarios we need to account for when using the mutation
207 | // observer to init and destroy elements. First, when a node is "moved" on the page,
208 | // it's registered as both an "add" and a "remove", so we want to skip those.
209 | // (This is handled above by the .__stimulusX_marker conditionals...)
210 | // Second, when a node is "wrapped", it gets registered as a "removal" and the wrapper
211 | // as an "addition". We don't want to remove, then re-initialize the node, so we look
212 | // and see if it's inside any added nodes (wrappers) and skip it.
213 | // (This is handled below by the .contains conditional...)
214 |
215 | for (let node of removedNodes) {
216 | if (addedNodes.some((i) => i.contains(node))) continue;
217 |
218 | onElRemoveds.forEach((i) => i(node));
219 | }
220 |
221 | for (let node of addedNodes) {
222 | if (!node.isConnected) continue;
223 |
224 | onElAddeds.forEach((i) => i(node));
225 | }
226 |
227 | addedNodes = null;
228 | removedNodes = null;
229 | addedAttributes = null;
230 | removedAttributes = null;
231 | }
232 |
--------------------------------------------------------------------------------
/test/directives/attr.test.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { createTestContext } from "../support/test-context";
3 |
4 | let context = await createTestContext();
5 |
6 | afterAll(() => context.teardown());
7 |
8 | describe("string attributes", async () => {
9 | beforeAll(() =>
10 | context.subject(
11 | class extends Controller {
12 | static values = {
13 | string: {
14 | type: String,
15 | default: "the string default value",
16 | },
17 | emptyString: {
18 | type: String,
19 | default: "",
20 | },
21 | anotherString: {
22 | type: String,
23 | default: "another string value",
24 | },
25 | };
26 | }
27 | )
28 | );
29 |
30 | test("applies the default value from controller", async () => {
31 | const { getTestElement, subjectController } = await context.testDOM(`
32 |
35 | `);
36 |
37 | expect(getTestElement("target")).toHaveAttribute("data-output", subjectController.stringValue);
38 | });
39 |
40 | test("applies the default value from a value attribute", async () => {
41 | const stringAttrValue = "overridden default value";
42 |
43 | const { getTestElement } = await context.testDOM(`
44 |
47 | `);
48 |
49 | expect(getTestElement("target")).toHaveAttribute("data-output", stringAttrValue);
50 | });
51 |
52 | test("overrides the existing attribute value", async () => {
53 | const { getTestElement, subjectController } = await context.testDOM(`
54 |
57 | `);
58 |
59 | expect(getTestElement("target")).toHaveAttribute("data-output", subjectController.stringValue);
60 | });
61 |
62 | test("doesn't remove attributes when the value is an empty string", async () => {
63 | const { getTestElement } = await context.testDOM(`
64 |
67 | `);
68 |
69 | expect(getTestElement("target")).toHaveAttribute("data-output", "");
70 | });
71 |
72 | test("updates the attribute when the value property changes", async () => {
73 | const { getTestElement, subjectController } = await context.testDOM(`
74 |
77 | `);
78 | const element = getTestElement("target");
79 |
80 | expect(element).toHaveAttribute("data-output", subjectController.stringValue);
81 |
82 | subjectController.stringValue = "a new value";
83 | expect(element).toHaveAttribute("data-output", "a new value");
84 |
85 | subjectController.stringValue = "and another new value";
86 | expect(element).toHaveAttribute("data-output", "and another new value");
87 | });
88 |
89 | test("multiple attribute bindings (single line)", async () => {
90 | const { getTestElement, subjectController } = await context.testDOM(`
91 |
94 | `);
95 | const element = getTestElement("target");
96 |
97 | expect(element).toHaveAttribute("data-string-1", subjectController.stringValue);
98 | expect(element).toHaveAttribute("data-string-2", subjectController.anotherStringValue);
99 |
100 | subjectController.stringValue = "foo";
101 | subjectController.anotherStringValue = "bar";
102 |
103 | expect(element).toHaveAttribute("data-string-1", "foo");
104 | expect(element).toHaveAttribute("data-string-2", "bar");
105 | });
106 |
107 | test("multiple attribute bindings (multi-line)", async () => {
108 | const { getTestElement, subjectController } = await context.testDOM(`
109 |
118 | `);
119 | const element = getTestElement("target");
120 |
121 | expect(element).toHaveAttribute("data-string-1", subjectController.stringValue);
122 | expect(element).toHaveAttribute("data-string-2", subjectController.anotherStringValue);
123 |
124 | subjectController.stringValue = "foo";
125 | subjectController.anotherStringValue = "bar";
126 |
127 | expect(element).toHaveAttribute("data-string-1", "foo");
128 | expect(element).toHaveAttribute("data-string-2", "bar");
129 | });
130 | });
131 |
132 | describe("boolean attributes", async () => {
133 | beforeAll(() =>
134 | context.subject(
135 | class extends Controller {
136 | static values = {
137 | boolean: Boolean,
138 | };
139 | }
140 | )
141 | );
142 |
143 | test("adds or removes the attribute from the element", async () => {
144 | const { getTestElement, subjectController } = await context.testDOM(`
145 |
146 | Summary
147 | Details
148 |
149 | `);
150 | const summary = getTestElement("summary");
151 |
152 | expect(summary).not.toHaveAttribute("open");
153 |
154 | subjectController.booleanValue = true;
155 | expect(summary).toHaveAttribute("open");
156 |
157 | subjectController.booleanValue = false;
158 | expect(summary).not.toHaveAttribute("open");
159 | });
160 | });
161 |
162 | describe("classes", async () => {
163 | beforeAll(() =>
164 | context.subject(
165 | class extends Controller {
166 | static values = {
167 | theme: {
168 | type: String,
169 | default: "light",
170 | },
171 | };
172 |
173 | get themeStylesAsObject() {
174 | return {
175 | "text-gray-900 bg-white": this.themeValue == "light",
176 | "text-white bg-gray-900": this.themeValue == "dark",
177 | };
178 | }
179 |
180 | get themeStylesAsArray() {
181 | switch (this.themeValue) {
182 | case "light":
183 | return ["text-gray-900", "bg-white"];
184 |
185 | case "dark":
186 | return ["text-white", "bg-gray-900"];
187 | }
188 | }
189 |
190 | get themeStylesAsString() {
191 | switch (this.themeValue) {
192 | case "light":
193 | return "text-gray-900 bg-white";
194 |
195 | case "dark":
196 | return "text-white bg-gray-900";
197 | }
198 | }
199 | }
200 | )
201 | );
202 |
203 | test("can resolve class objects", async () => {
204 | const { getTestElement, subjectController } = await context.testDOM(`
205 |
206 |
207 |
208 | `);
209 | const target = getTestElement("target");
210 |
211 | expect(target).toHaveClass("text-gray-900 bg-white", { exact: true });
212 |
213 | subjectController.themeValue = "dark";
214 | expect(target).toHaveClass("text-white bg-gray-900", { exact: true });
215 | });
216 |
217 | test("can resolve class arrays", async () => {
218 | const { getTestElement, subjectController } = await context.testDOM(`
219 |
220 |
221 |
222 | `);
223 | const target = getTestElement("target");
224 |
225 | expect(target).toHaveClass("text-gray-900 bg-white", { exact: true });
226 |
227 | subjectController.themeValue = "dark";
228 | expect(target).toHaveClass("text-white bg-gray-900", { exact: true });
229 | });
230 |
231 | test("can resolve class strings", async () => {
232 | const { getTestElement, subjectController } = await context.testDOM(`
233 |
234 |
235 |
236 | `);
237 | const target = getTestElement("target");
238 |
239 | expect(target).toHaveClass("text-gray-900 bg-white", { exact: true });
240 |
241 | subjectController.themeValue = "dark";
242 | expect(target).toHaveClass("text-white bg-gray-900", { exact: true });
243 | });
244 |
245 | test("doesn't overwrite existing classes", async () => {
246 | const { getTestElement, subjectController } = await context.testDOM(`
247 |
248 |
249 |
250 | `);
251 | const target = getTestElement("target");
252 |
253 | expect(target).toHaveClass("border-hotpink text-gray-900 bg-white", { exact: true });
254 |
255 | subjectController.themeValue = "dark";
256 | expect(target).toHaveClass("border-hotpink text-white bg-gray-900", { exact: true });
257 |
258 | subjectController.themeValue = "unknown";
259 | expect(target).toHaveClass("border-hotpink", { exact: true });
260 | });
261 | });
262 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _Reactivity engine for Stimulus controllers_
6 |
7 |
8 | 
9 | [](https://github.com/allmarkedup/stimulus-x/actions/workflows/ci.yml)
10 |
11 |
12 |
13 | ---
14 |
15 | _StimulusX_ brings modern **reactive programming paradigms** to [Stimulus](https://stimulus.hotwired.dev) controllers.
16 |
17 | **Features include:**
18 |
19 | ❎ Automatic UI updates with reactive DOM bindings
20 | ❎ Declarative binding syntax based on Stimulus' [action descriptors](https://stimulus.hotwired.dev/reference/actions#descriptors)
21 | ❎ Chainable value modifiers
22 | ❎ Property watcher callback
23 | ❎ Extension API
24 |
25 |
26 | **Who is StimulusX for?**
27 |
28 | 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.
29 |
30 | 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.
31 |
32 | [ ↓ Skip examples and jump to the docs ↓](#installation)
33 |
34 | ### Example: A simple counter
35 |
36 | Below is an example of a simple `counter` controller implemented using StimulusX's reactive DOM bindings.
37 |
38 | > [!TIP]
39 | > _You can find a [runnable version of this example on JSfiddle →](https://jsfiddle.net/allmarkedup/q293ay8v/)_
40 |
41 |
42 |
43 | ```html
44 |
45 |
46 | of
47 |
48 |
49 |
50 | ⬆️
51 | ⬇️
55 |
56 | ```
57 |
58 | ```js
59 | // controllers/counter_controller.js
60 | import { Controller } from "@hotwired/stimulus"
61 |
62 | export default class extends Controller {
63 | initialize(){
64 | this.count = 0;
65 | this.max = 5;
66 | }
67 |
68 | increment(){
69 | this.count++;
70 | }
71 |
72 | decrement(){
73 | this.count--;
74 | }
75 |
76 | get displayClasses(){
77 | return {
78 | "text-green": this.count <= this.max,
79 | "text-red font-bold": this.count > this.max,
80 | }
81 | }
82 | }
83 | ```
84 |
85 | ---
86 |
87 | ## Installation
88 |
89 | Add the `stimulus-x` package to your `package.json`:
90 |
91 | #### Using NPM:
92 |
93 | ```
94 | npm i stimulus-x
95 | ```
96 |
97 | #### Using Yarn:
98 |
99 | ```
100 | yarn add stimulus-x
101 | ```
102 |
103 | #### Without a bundler
104 |
105 | You can use StimulusX with native browser `module` imports by loading from it from [Skypack](https://skypack.dev):
106 |
107 | ```html
108 |
109 |
110 |
111 |
112 |
117 |
118 |
119 |
120 |
121 | ```
122 |
123 | ## Usage
124 |
125 | StimulusX hooks into your Stimulus application instance via the `StimulusX.init` method.
126 |
127 | ```js
128 | import { Application, Controller } from "@hotwired/stimulus";
129 | import StimulusX from "stimulus-x";
130 |
131 | window.Stimulus = Application.start();
132 |
133 | // You must call the `StimulusX.init` method _before_ registering any controllers.
134 | StimulusX.init(Stimulus);
135 |
136 | // Register controllers as usual...
137 | Stimulus.register("example", ExampleController);
138 | ```
139 |
140 | 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.
141 |
142 | Explicit controller opt-in
143 |
144 | 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.
145 |
146 | To enable individual controller opt-in set the `optIn` option to `true` when initializing StimulusX:
147 |
148 | ```js
149 | StimulusX.init(Stimulus, { optIn: true });
150 | ```
151 |
152 | To then enable reactive features on a per-controller basis, set the `static reactive` variable to `true` in the controller class:
153 |
154 | ```js
155 | import { Controller } from "@hotwired/stimulus"
156 |
157 | export default class extends Controller {
158 | static reactive = true; // enable StimulusX reactive features for this controller
159 | // ...
160 | }
161 | ```
162 |
163 | Reactive DOM bindings - overview
164 |
165 | [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.
166 |
167 | These bindings are _reactive_ which means the DOM is **automatically updated** when the value of the controller properties change.
168 |
169 | ### Binding descriptors
170 |
171 | Bindings are specified declaratively in your HTML using `data-bind-(attr|text|html)` attributes where the _value_ of the attribute is a **binding descriptor**.
172 |
173 | **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.
174 |
175 | ```html
176 |
177 |
178 | ```
179 |
180 | 📚 ***Read more: [Attribute bindings →](#attribute-binding)***
181 |
182 | **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.
183 |
184 | ```html
185 |
186 |
187 |
188 |
189 |
190 | ```
191 |
192 | 📚 ***Read more: [text bindings](#text-binding)*** _and_ ***[HTML bindings →](#html-binding)***
193 |
194 | > [!NOTE]
195 | > _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._
196 |
197 | ### Value modifiers
198 |
199 | Binding _value modifiers_ are a convenient way to transform or test property values in-situ before updating the DOM.
200 |
201 | ```html
202 |
203 |
204 | ```
205 |
206 | 📚 ***Read more: [Binding value modifiers →](#binding-value-modifiers)***
207 |
208 | ### Negating property values
209 |
210 | Boolean property values can be negated (inverted) by prefixing the `identifier#property` part of the binding descriptor with an exclaimation mark:.
211 |
212 | ```html
213 |
214 | ```
215 |
216 | > [!NOTE]
217 | > _The `!` prefix is really just an more concise alternative syntax for applying [the `:not` modifier](#binding-value-modifiers)._
218 |
219 | ### Shallow vs deep reactivity
220 |
221 | By default StimulusX only tracks changes to **top level** controller properties to figure out when to update the DOM. This is _shallow reactivity_.
222 |
223 | 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:
224 |
225 | ```js
226 | import { Controller } from "@hotwired/stimulus"
227 |
228 | export default class extends Controller {
229 | static reactive = "deep"; // enable deep reactivity mode
230 | // ...
231 | }
232 | ```
233 |
234 | Alternatively you can enable deep reactivity for **all** controllers using the `trackDeep` option when [initializing StimulusX](#usage):
235 |
236 | ```js
237 | StimulusX.init(Stimulus, { trackDeep: true });
238 | ```
239 |
240 | Attribute bindings
241 |
242 | 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.
243 |
244 | They are specified using `data-bind-attr` attributes with [value descriptors](#binding-descriptors) that take the general form `{attribute}~{identifier}#{property}`.
245 |
246 | ```html
247 |
248 |
249 |
250 | ```
251 |
252 | ```js
253 | export default class extends Controller {
254 | initialize(){
255 | this.imageUrl = "https://placeholder.com/kittens.jpg";
256 | }
257 | }
258 | ```
259 |
260 | In the attribute binding descriptor `src~lightbox#imageUrl` above:
261 |
262 | * `src` is the **HTML attribute** to be added/updated/remove
263 | * `lightbox` is the **controller identifier**
264 | * `imageUrl` is the **name of the property** that the attribute value should be bound to
265 |
266 | 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.
267 |
268 | ```js
269 | this.imageUrl = "https://kittens.com/daily-kitten.jpg"
270 | //
271 | ```
272 |
273 |
274 | ### Boolean attributes
275 |
276 | [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`.
277 |
278 | ```html
279 |
280 | submit
281 |
282 | ```
283 |
284 | ```js
285 | export default class extends Controller {
286 | initialize(){
287 | this.incomplete = true;
288 | }
289 | }
290 | ```
291 |
292 | Boolean attribute bindings often pair nicely with **[comparison modifiers](#comparison-modifiers)** such as `:is`:
293 |
294 | ```html
295 |
296 |
297 | submit
298 |
299 | ```
300 |
301 | ```js
302 | export default class extends Controller {
303 | initialize(){
304 | this.status = "incomplete";
305 | }
306 |
307 | // called when the text input value is changed
308 | checkCompleted({ currentTarget }){
309 | if (currentTarget.value?.length > 0) {
310 | this.status === "complete"; // button will be enabled
311 | }
312 | }
313 | }
314 | ```
315 |
316 | ### Binding classes
317 |
318 | `class` attribute bindings let you set specific classes on an element based on controller state.
319 |
320 | ```html
321 |
322 |
323 | ...
324 |
325 |
326 | ```
327 |
328 | ```js
329 | // controllers/counter_controller.js
330 | import { Controller } from "@hotwired/stimulus"
331 |
332 | export default class extends Controller {
333 | initialize(){
334 | this.count = 0;
335 | }
336 |
337 | get validityClasses(){
338 | if (this.count > 10) {
339 | return "text-red font-bold";
340 | } else {
341 | return "text-green";
342 | }
343 | }
344 | }
345 | ```
346 |
347 | 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`:
348 |
349 | * If `this.count > 10` then the element `class` attribute will be set to `"text-red font-bold"`.
350 | * If `this.count < 10` then the element `class` attribute will be set to `"text-green"`.
351 |
352 | The list of classes can be returned as a **string** or as an **array** - or as a special [class object](#class-objects).
353 |
354 | #### Class objects
355 |
356 | 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.
357 |
358 | The example above could be rewritten to use a class object as follows:
359 |
360 | ```js
361 | export default class extends Controller {
362 | // ...
363 | get validityClasses(){
364 | return {
365 | "text-red font-bold": this.count > 10,
366 | "text-green": this.count <= 10,
367 | }
368 | }
369 | }
370 | ```
371 |
372 | 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.
373 |
374 | Text content bindings
375 |
376 | 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.
377 |
378 | Text content bindings are specified using `data-bind-text` attributes where the value is a binding descriptor in the form `{identifier}#{property}`.
379 |
380 | ```html
381 |
382 | Status:
383 |
384 | ```
385 |
386 | ```js
387 | export default class extends Controller {
388 | static values = {
389 | status: {
390 | type: String,
391 | default: "in progress"
392 | }
393 | }
394 | }
395 | ```
396 |
397 | HTML bindings
398 |
399 | HTML bindings are very similar to [text content bindings](#️text-bindings) except they update the element's `innerHTML` instead of `textContent`.
400 |
401 | HTML bindings are specified using `data-bind-html` attributes where the value is a binding descriptor in the form `{identifier}#{property}`.
402 |
403 | ```html
404 |
407 | ```
408 |
409 | ```js
410 | export default class extends Controller {
411 | initialize(){
412 | this.status = "in progress";
413 | }
414 |
415 | get statusIcon(){
416 | if (this.status === "complete"){
417 | return ` `;
418 | } else {
419 | return ` `;
420 | }
421 | }
422 | }
423 | ```
424 |
425 | Binding value modifiers
426 |
427 | Inline _value modifiers_ are a convenient way to transform or test property values before updating the DOM.
428 |
429 | Modifiers are appended to the end of [binding descriptors](#binding-descriptors) and are separated from the descriptor (or from each other) by a `:` colon.
430 |
431 | The example below uses the `upcase` modifier to transform the title to upper case before displaying it on the page:
432 |
433 | ```html
434 |
435 | ```
436 |
437 | > [!TIP]
438 | > _Multiple modifiers can be piped together one after each other, separated by colons, e.g. `article#title:upcase:trim`_
439 |
440 |
441 |
442 | String transform modifiers provide stackable output transformations for string values.
443 |
444 | #### Available string modifiers:
445 |
446 | * `:upcase` - transform text to uppercase
447 | * `:downcase` - transform text to lowercase
448 | * `:strip` - strip leading and trailing whitespace
449 |
450 | :upcase
451 |
452 | Converts the string to uppercase.
453 |
454 | ```html
455 |
456 | ```
457 |
458 | :downcase
459 |
460 | Converts the string to lowercase.
461 |
462 | ```html
463 |
464 | ```
465 |
466 | :strip
467 |
468 | Strips leading and trailing whitespace from the string value.
469 |
470 | ```html
471 |
472 | ```
473 |
474 | Comparison modifiers
475 |
476 | _Comparison modifiers_ compare the resolved **controller property value** against a **provided test value**.
477 |
478 | ```html
479 |
480 | ```
481 |
482 | 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.
483 |
484 | > [!TIP]
485 | > _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_:
486 | > ```html
487 | > `
488 | > ```
489 |
490 | #### Available comparison modifiers:
491 |
492 | * `:is()` - equality test ([read more](#is-modifier))
493 | * `:isNot()` - negated equality test ([read more](#is-not-modifier))
494 | * `:gt()` - 'greater than' test ([read more](#gt-modifier))
495 | * `:gte()` - 'greater than or equal to' test ([read more](#gte-modifier))
496 | * `:lt()` - 'less than' test ([read more](#lt-modifier))
497 | * `:lte()` - 'less than or equal to' test ([read more](#lte-modifier))
498 |
499 | :is(<value>)
500 |
501 | The `:is` modifier compares the resolved property value with the `` provided as an argument, returning `true` if they match and `false` if not.
502 |
503 | ```html
504 |
505 |
506 | ```
507 |
508 | * **String** comparison: `:is('single quoted string')`, `:is("double quoted string")`
509 | * **Integer** comparison: `:is(123)`
510 | * **Float** comparison: `:is(1.23)`
511 | * **Boolean** comparison: `:is(true)`, `:is(false)`
512 |
513 | :isNot(<value>)
514 |
515 | 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.
516 |
517 | > [!IMPORTANT]
518 | > _The `:is` and `:isNot` modifiers only accept simple `String`, `Number` or `Boolean` values. `Object` and `Array` values are not supported._
519 |
520 | :gt(<value>)
521 |
522 | The `:gt` modifier returns `true` if the resolved property value is **greater than** the numeric `` provided as an argument.
523 |
524 | ```html
525 |
526 | +
527 | ```
528 |
529 | :gte(<value>)
530 |
531 | The `:gte` modifier returns `true` if the resolved property value is **greater than or equal to** the numeric `` provided as an argument.
532 |
533 | ```html
534 |
535 | +
536 | ```
537 |
538 | :lt(<value>)
539 |
540 | The `:lt` modifier returns `true` if the resolved property value is **less than** the numeric `` provided as an argument.
541 |
542 | ```html
543 |
544 | -
545 | ```
546 |
547 | :lte(<value>)
548 |
549 | The `:lte` modifier returns `true` if the resolved property value is **less than or equal to** the numeric `` provided as an argument.
550 |
551 | ```html
552 |
553 | -
554 | ```
555 |
556 | Other modifiers
557 |
558 | * `:not` - negate (invert) a boolean value
559 |
560 | > [!TIP]
561 | > _You can add your own **custom modifiers** if required. See [Extending StimulusX](#extending) for more info._
562 |
563 | Watching properties for changes
564 |
565 | ```js
566 | import { Controller } from "@hotwired/stimulus"
567 |
568 | export default class extends Controller {
569 | static watch = ["enabled", "userInput"];
570 |
571 | connect(){
572 | this.enabled = false;
573 | this.userInput = "";
574 | }
575 |
576 | enabledPropertyChanged(currentValue, previousValue){
577 | if (currentValue) {
578 | console.log("Controller is enabled");
579 | } else {
580 | console.log("Controller has been disabled");
581 | }
582 | }
583 |
584 | userInputPropertyChanged(currentValue, previousValue){
585 | console.log(`User input has changed from "${previousValue}" to "${currentValue}"`);
586 | }
587 |
588 | // ...
589 | }
590 | ```
591 |
592 | 🚧 _More docs coming soon..._
593 |
594 |
595 | Extending StimulusX
596 |
597 | ### Custom modifiers
598 |
599 | You can add your own modifiers using the `StimulusX.modifier` method:
600 |
601 | ```js
602 | StimulusX.modifier("modifierName", (value) => {
603 | // Do something to `value` and return the result of the transformation.
604 | const transformedValue = doSomethingTo(value);
605 | return transformedValue;
606 | });
607 | ```
608 |
609 | ### Custom directives
610 |
611 | 🚧 _Documentation coming soon..._
612 |
613 | ## Known issues, caveats and workarounds
614 |
615 | ### ❌ Private properties and methods
616 |
617 | 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/)
618 |
619 | 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.
620 |
621 | ## Credits and inspiration
622 |
623 | 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.
624 |
625 | ## License
626 |
627 | StimulusX is available as open source under the terms of the MIT License.
--------------------------------------------------------------------------------
/dist/stimulus-x.js:
--------------------------------------------------------------------------------
1 | import {getProperty as $5OpyM$getProperty} from "dot-prop";
2 | import {isReactive as $5OpyM$isReactive, reactive as $5OpyM$reactive, shallowReactive as $5OpyM$shallowReactive, effect as $5OpyM$effect, stop as $5OpyM$stop} from "@vue/reactivity/dist/reactivity.esm-browser.prod.js";
3 |
4 | let $eae25d6e66596517$var$flushPending = false;
5 | let $eae25d6e66596517$var$flushing = false;
6 | let $eae25d6e66596517$var$queue = [];
7 | let $eae25d6e66596517$var$lastFlushedIndex = -1;
8 | let $eae25d6e66596517$var$tickStack = [];
9 | let $eae25d6e66596517$var$isHolding = false;
10 | function $eae25d6e66596517$export$d30788f2c20241cd(callback) {
11 | $eae25d6e66596517$export$fba1a0a20887772f(callback);
12 | }
13 | function $eae25d6e66596517$export$fba1a0a20887772f(job) {
14 | if (!$eae25d6e66596517$var$queue.includes(job)) $eae25d6e66596517$var$queue.push(job);
15 | $eae25d6e66596517$var$queueFlush();
16 | }
17 | function $eae25d6e66596517$export$edbe2d8b64bcb07c(job) {
18 | let index = $eae25d6e66596517$var$queue.indexOf(job);
19 | if (index !== -1 && index > $eae25d6e66596517$var$lastFlushedIndex) $eae25d6e66596517$var$queue.splice(index, 1);
20 | }
21 | function $eae25d6e66596517$var$queueFlush() {
22 | if (!$eae25d6e66596517$var$flushing && !$eae25d6e66596517$var$flushPending) {
23 | $eae25d6e66596517$var$flushPending = true;
24 | queueMicrotask($eae25d6e66596517$export$8ca066e62735a16c);
25 | }
26 | }
27 | function $eae25d6e66596517$export$8ca066e62735a16c() {
28 | $eae25d6e66596517$var$flushPending = false;
29 | $eae25d6e66596517$var$flushing = true;
30 | for(let i = 0; i < $eae25d6e66596517$var$queue.length; i++){
31 | $eae25d6e66596517$var$queue[i]();
32 | $eae25d6e66596517$var$lastFlushedIndex = i;
33 | }
34 | $eae25d6e66596517$var$queue.length = 0;
35 | $eae25d6e66596517$var$lastFlushedIndex = -1;
36 | $eae25d6e66596517$var$flushing = false;
37 | }
38 | function $eae25d6e66596517$export$bdd553fddd433dcb(callback = ()=>{}) {
39 | queueMicrotask(()=>{
40 | $eae25d6e66596517$var$isHolding || setTimeout(()=>{
41 | $eae25d6e66596517$export$d80ec80fb4bee1e6();
42 | });
43 | });
44 | return new Promise((res)=>{
45 | $eae25d6e66596517$var$tickStack.push(()=>{
46 | callback();
47 | res();
48 | });
49 | });
50 | }
51 | function $eae25d6e66596517$export$d80ec80fb4bee1e6() {
52 | $eae25d6e66596517$var$isHolding = false;
53 | while($eae25d6e66596517$var$tickStack.length)$eae25d6e66596517$var$tickStack.shift()();
54 | }
55 | function $eae25d6e66596517$export$e9a53d8785d6cfc9() {
56 | $eae25d6e66596517$var$isHolding = true;
57 | }
58 |
59 |
60 |
61 |
62 |
63 |
64 | const $3ee5a2b2e05cc741$export$352205f445242f02 = (0, $5OpyM$isReactive);
65 | const $3ee5a2b2e05cc741$export$90a44edba14e47be = (0, $5OpyM$reactive);
66 | const $3ee5a2b2e05cc741$export$8d81cefd22d22260 = (0, $5OpyM$shallowReactive);
67 | const $3ee5a2b2e05cc741$export$dc573d8a6576cdb3 = (callback)=>(0, $5OpyM$effect)(callback, {
68 | scheduler: (0, $eae25d6e66596517$export$d30788f2c20241cd)((task)=>task)
69 | });
70 | function $3ee5a2b2e05cc741$export$1ecd3170301acce1(el) {
71 | let cleanup = ()=>{};
72 | let wrappedEffect = (callback)=>{
73 | let effectReference = $3ee5a2b2e05cc741$export$dc573d8a6576cdb3(callback);
74 | if (!el.__stimulusX_effects) el.__stimulusX_effects = new Set();
75 | el.__stimulusX_effects.add(effectReference);
76 | cleanup = ()=>{
77 | if (effectReference === undefined) return;
78 | el.__stimulusX_effects.delete(effectReference);
79 | (0, $5OpyM$stop)(effectReference);
80 | };
81 | return effectReference;
82 | };
83 | return [
84 | wrappedEffect,
85 | ()=>{
86 | cleanup();
87 | }
88 | ];
89 | }
90 | function $3ee5a2b2e05cc741$export$3db5d71bdb2d5499(getter, callback) {
91 | let firstTime = true;
92 | let oldValue;
93 | let effectReference = $3ee5a2b2e05cc741$export$dc573d8a6576cdb3(()=>{
94 | let value = getter();
95 | // JSON.stringify touches every single property at any level enabling deep watching
96 | JSON.stringify(value);
97 | if (!firstTime) // We have to queue this watcher as a microtask so that
98 | // the watcher doesn't pick up its own dependencies.
99 | queueMicrotask(()=>{
100 | callback(value, oldValue);
101 | oldValue = value;
102 | });
103 | else oldValue = value;
104 | firstTime = false;
105 | });
106 | return ()=>(0, $5OpyM$stop)(effectReference);
107 | }
108 |
109 |
110 |
111 | let $c6f8b3abaeac122e$var$onAttributeAddeds = [];
112 | let $c6f8b3abaeac122e$var$onElRemoveds = [];
113 | let $c6f8b3abaeac122e$var$onElAddeds = [];
114 | let $c6f8b3abaeac122e$var$onValueAttributeChangeds = [];
115 | let $c6f8b3abaeac122e$var$currentlyObserving = false;
116 | let $c6f8b3abaeac122e$var$isCollecting = false;
117 | let $c6f8b3abaeac122e$var$deferredMutations = [];
118 | let $c6f8b3abaeac122e$var$observer = new MutationObserver($c6f8b3abaeac122e$var$onMutate);
119 | function $c6f8b3abaeac122e$export$c395e4fde41c37ff(callback) {
120 | $c6f8b3abaeac122e$var$onElAddeds.push(callback);
121 | }
122 | function $c6f8b3abaeac122e$export$bb8862ef847f5ec0(el, callback) {
123 | if (typeof callback === "function") {
124 | if (!el.__stimulusX_cleanups) el.__stimulusX_cleanups = [];
125 | el.__stimulusX_cleanups.push(callback);
126 | } else {
127 | callback = el;
128 | $c6f8b3abaeac122e$var$onElRemoveds.push(callback);
129 | }
130 | }
131 | function $c6f8b3abaeac122e$export$545f7104b1510552(callback) {
132 | $c6f8b3abaeac122e$var$onAttributeAddeds.push(callback);
133 | }
134 | function $c6f8b3abaeac122e$export$5d89a587b01747c6(el, name, callback) {
135 | if (!el.__stimulusX_attributeCleanups) el.__stimulusX_attributeCleanups = {};
136 | if (!el.__stimulusX_attributeCleanups[name]) el.__stimulusX_attributeCleanups[name] = [];
137 | el.__stimulusX_attributeCleanups[name].push(callback);
138 | }
139 | function $c6f8b3abaeac122e$export$309d6f15c1c4d36e(callback) {
140 | $c6f8b3abaeac122e$var$onValueAttributeChangeds.push(callback);
141 | }
142 | function $c6f8b3abaeac122e$export$2c8bfe603cc113da(el, names) {
143 | if (!el.__stimulusX_attributeCleanups) return;
144 | Object.entries(el.__stimulusX_attributeCleanups).forEach(([name, value])=>{
145 | if (names === undefined || names.includes(name)) {
146 | value.forEach((i)=>i());
147 | delete el.__stimulusX_attributeCleanups[name];
148 | }
149 | });
150 | }
151 | function $c6f8b3abaeac122e$export$21fc366069a4f56f(el) {
152 | el.__stimulusX_cleanups?.forEach((0, $eae25d6e66596517$export$edbe2d8b64bcb07c));
153 | while(el.__stimulusX_cleanups?.length)el.__stimulusX_cleanups.pop()();
154 | }
155 | function $c6f8b3abaeac122e$export$1a5ae5db40475a2d() {
156 | $c6f8b3abaeac122e$var$observer.observe(document, {
157 | subtree: true,
158 | childList: true,
159 | attributes: true,
160 | attributeOldValue: true
161 | });
162 | $c6f8b3abaeac122e$var$currentlyObserving = true;
163 | }
164 | function $c6f8b3abaeac122e$export$d4f6b05796af6998() {
165 | $c6f8b3abaeac122e$export$2f1f1886cd00d96e();
166 | $c6f8b3abaeac122e$var$observer.disconnect();
167 | $c6f8b3abaeac122e$var$currentlyObserving = false;
168 | }
169 | let $c6f8b3abaeac122e$var$queuedMutations = [];
170 | function $c6f8b3abaeac122e$export$2f1f1886cd00d96e() {
171 | let records = $c6f8b3abaeac122e$var$observer.takeRecords();
172 | $c6f8b3abaeac122e$var$queuedMutations.push(()=>records.length > 0 && $c6f8b3abaeac122e$var$onMutate(records));
173 | let queueLengthWhenTriggered = $c6f8b3abaeac122e$var$queuedMutations.length;
174 | queueMicrotask(()=>{
175 | // If these two lengths match, then we KNOW that this is the LAST
176 | // flush in the current event loop. This way, we can process
177 | // all mutations in one batch at the end of everything...
178 | if ($c6f8b3abaeac122e$var$queuedMutations.length === queueLengthWhenTriggered) // Now Alpine can process all the mutations...
179 | while($c6f8b3abaeac122e$var$queuedMutations.length > 0)$c6f8b3abaeac122e$var$queuedMutations.shift()();
180 | });
181 | }
182 | function $c6f8b3abaeac122e$export$c98382a3d82f9519(callback) {
183 | if (!$c6f8b3abaeac122e$var$currentlyObserving) return callback();
184 | $c6f8b3abaeac122e$export$d4f6b05796af6998();
185 | let result = callback();
186 | $c6f8b3abaeac122e$export$1a5ae5db40475a2d();
187 | return result;
188 | }
189 | function $c6f8b3abaeac122e$export$9a7d8d7577dd8469() {
190 | $c6f8b3abaeac122e$var$isCollecting = true;
191 | }
192 | function $c6f8b3abaeac122e$export$47d46026c1b12c48() {
193 | $c6f8b3abaeac122e$var$isCollecting = false;
194 | $c6f8b3abaeac122e$var$onMutate($c6f8b3abaeac122e$var$deferredMutations);
195 | $c6f8b3abaeac122e$var$deferredMutations = [];
196 | }
197 | function $c6f8b3abaeac122e$var$onMutate(mutations) {
198 | if ($c6f8b3abaeac122e$var$isCollecting) {
199 | $c6f8b3abaeac122e$var$deferredMutations = $c6f8b3abaeac122e$var$deferredMutations.concat(mutations);
200 | return;
201 | }
202 | let addedNodes = [];
203 | let removedNodes = new Set();
204 | let addedAttributes = new Map();
205 | let removedAttributes = new Map();
206 | for(let i = 0; i < mutations.length; i++){
207 | if (mutations[i].target.__stimulusX_ignoreMutationObserver) continue;
208 | if (mutations[i].type === "childList") {
209 | mutations[i].removedNodes.forEach((node)=>{
210 | if (node.nodeType !== 1) return;
211 | // No need to process removed nodes that haven't been initialized by Alpine...
212 | if (!node.__stimulusX_marker) return;
213 | removedNodes.add(node);
214 | });
215 | mutations[i].addedNodes.forEach((node)=>{
216 | if (node.nodeType !== 1) return;
217 | // If the node is a removal as well, that means it's a "move" operation and we'll leave it alone...
218 | if (removedNodes.has(node)) {
219 | removedNodes.delete(node);
220 | return;
221 | }
222 | // If the node has already been initialized, we'll leave it alone...
223 | if (node.__stimulusX_marker) return;
224 | addedNodes.push(node);
225 | });
226 | }
227 | if (mutations[i].type === "attributes") {
228 | let el = mutations[i].target;
229 | let name = mutations[i].attributeName;
230 | let oldValue = mutations[i].oldValue;
231 | let add = ()=>{
232 | if (!addedAttributes.has(el)) addedAttributes.set(el, []);
233 | addedAttributes.get(el).push({
234 | name: name,
235 | value: el.getAttribute(name)
236 | });
237 | };
238 | let remove = ()=>{
239 | if (!removedAttributes.has(el)) removedAttributes.set(el, []);
240 | removedAttributes.get(el).push(name);
241 | };
242 | // let valueAttributeChanged = () => {
243 | // };
244 | // New attribute.
245 | if (el.hasAttribute(name) && oldValue === null) add();
246 | else if (el.hasAttribute(name)) {
247 | remove();
248 | add();
249 | // Removed attribute.
250 | } else remove();
251 | }
252 | }
253 | removedAttributes.forEach((attrs, el)=>{
254 | $c6f8b3abaeac122e$export$2c8bfe603cc113da(el, attrs);
255 | });
256 | addedAttributes.forEach((attrs, el)=>{
257 | $c6f8b3abaeac122e$var$onAttributeAddeds.forEach((i)=>i(el, attrs));
258 | });
259 | // There are two special scenarios we need to account for when using the mutation
260 | // observer to init and destroy elements. First, when a node is "moved" on the page,
261 | // it's registered as both an "add" and a "remove", so we want to skip those.
262 | // (This is handled above by the .__stimulusX_marker conditionals...)
263 | // Second, when a node is "wrapped", it gets registered as a "removal" and the wrapper
264 | // as an "addition". We don't want to remove, then re-initialize the node, so we look
265 | // and see if it's inside any added nodes (wrappers) and skip it.
266 | // (This is handled below by the .contains conditional...)
267 | for (let node of removedNodes){
268 | if (addedNodes.some((i)=>i.contains(node))) continue;
269 | $c6f8b3abaeac122e$var$onElRemoveds.forEach((i)=>i(node));
270 | }
271 | for (let node of addedNodes){
272 | if (!node.isConnected) continue;
273 | $c6f8b3abaeac122e$var$onElAddeds.forEach((i)=>i(node));
274 | }
275 | addedNodes = null;
276 | removedNodes = null;
277 | addedAttributes = null;
278 | removedAttributes = null;
279 | }
280 |
281 |
282 |
283 | const $50e97065b94a2e88$var$defaultOptions = {
284 | optIn: false,
285 | compileDirectives: true,
286 | trackDeep: false
287 | };
288 | let $50e97065b94a2e88$var$options = $50e97065b94a2e88$var$defaultOptions;
289 | function $50e97065b94a2e88$export$6df0712d20d2cc08(key) {
290 | return $50e97065b94a2e88$var$options[key];
291 | }
292 | function $50e97065b94a2e88$export$d2312e68e1f5ad00() {
293 | return $50e97065b94a2e88$var$options;
294 | }
295 | function $50e97065b94a2e88$export$add91eafeaeab287(opts) {
296 | $50e97065b94a2e88$var$options = Object.assign({}, $50e97065b94a2e88$var$defaultOptions, opts);
297 | return $50e97065b94a2e88$var$options;
298 | }
299 |
300 |
301 | function $61c34dda51f70fa1$export$d56142fa17014959(ControllerClass) {
302 | return class extends ControllerClass {
303 | constructor(context){
304 | super(context);
305 | // Override the attribute setter so that our mutation observer doesn't pick up on changes
306 | // that are also already being handled directly by Stimulus.
307 | const setData = this.data.set;
308 | this.data.set = (key, value)=>{
309 | (0, $c6f8b3abaeac122e$export$c98382a3d82f9519)(()=>setData.call(this.data, key, value));
310 | };
311 | // Create a reactive controller object
312 | const trackDeep = (0, $50e97065b94a2e88$export$6df0712d20d2cc08)("trackDeep") || this.constructor.reactive === "deep";
313 | const reactiveSelf = trackDeep ? (0, $3ee5a2b2e05cc741$export$90a44edba14e47be)(this) : (0, $3ee5a2b2e05cc741$export$8d81cefd22d22260)(this);
314 | // Initialize watched property callbacks
315 | const watchedProps = this.constructor.watch || [];
316 | watchedProps.forEach((prop)=>$61c34dda51f70fa1$export$dcc3676fc96ef4c(reactiveSelf, prop));
317 | // Return the reactive controller instance
318 | return reactiveSelf;
319 | }
320 | connect() {
321 | // Initialize the DOM tree and run directives when connected
322 | super.connect();
323 | (0, $eae25d6e66596517$export$bdd553fddd433dcb)(()=>(0, $85d582547429ac89$export$b1432670dab450da)(this.element));
324 | }
325 | };
326 | }
327 | function $61c34dda51f70fa1$export$6d5f0ef1727b562e(el, identifier) {
328 | const controllerElement = el.closest(`[data-controller~="${identifier}"]`);
329 | if (controllerElement) return (0, $85d582547429ac89$export$8d516e055d924071).getControllerForElementAndIdentifier(controllerElement, identifier);
330 | }
331 | function $61c34dda51f70fa1$export$121af9acc174ac93(controller, property) {
332 | let value = (0, $5OpyM$getProperty)(controller, property);
333 | if (typeof value === "function") value = value.apply(controller);
334 | return value;
335 | }
336 | function $61c34dda51f70fa1$export$dcc3676fc96ef4c(controller, propertyRef) {
337 | const getter = ()=>$61c34dda51f70fa1$export$121af9acc174ac93(controller, propertyRef);
338 | const cleanup = (0, $3ee5a2b2e05cc741$export$3db5d71bdb2d5499)(getter, (value, oldValue)=>{
339 | $61c34dda51f70fa1$var$callCallbacks(controller, propertyRef, value, oldValue, false);
340 | });
341 | // Run once on creation
342 | $61c34dda51f70fa1$var$callCallbacks(controller, propertyRef, getter(), undefined, true);
343 | const rootElement = controller.element;
344 | if (!rootElement.__stimulusX_cleanups) rootElement.__stimulusX_cleanups = [];
345 | rootElement.__stimulusX_cleanups.push(cleanup);
346 | }
347 | function $61c34dda51f70fa1$var$callCallbacks(controller, propertyRef, value, oldValue, initial) {
348 | // Generic callback, called when _any_ watched property changes
349 | if (typeof controller.watchedPropertyChanged === "function") controller.watchedPropertyChanged(propertyRef, value, oldValue, {
350 | initial: initial
351 | });
352 | // Property-specific change callback
353 | const propertyWatcherCallback = controller[`${$61c34dda51f70fa1$var$getCamelizedPropertyRef(propertyRef)}PropertyChanged`];
354 | if (typeof propertyWatcherCallback === "function") propertyWatcherCallback.call(controller, value, oldValue, {
355 | initial: initial
356 | });
357 | }
358 | function $61c34dda51f70fa1$var$getCamelizedPropertyRef(propertyRef) {
359 | return $61c34dda51f70fa1$var$camelCase(propertyRef.replace(".", " "));
360 | }
361 | function $61c34dda51f70fa1$var$camelCase(subject) {
362 | return subject.toLowerCase().replace(/-(\w)/g, (match, char)=>char.toUpperCase());
363 | }
364 |
365 |
366 | function $f3ad94c9f84f4d57$export$8a7688a96d852767(subject) {
367 | return subject.replace(/:/g, "_").split("_").map((word, index)=>index === 0 ? word : word[0].toUpperCase() + word.slice(1)).join("");
368 | }
369 | function $f3ad94c9f84f4d57$export$588732934346abbf(el, callback) {
370 | let skip = false;
371 | callback(el, ()=>skip = true);
372 | if (skip) return;
373 | let node = el.firstElementChild;
374 | while(node){
375 | $f3ad94c9f84f4d57$export$588732934346abbf(node, callback, false);
376 | node = node.nextElementSibling;
377 | }
378 | }
379 | function $f3ad94c9f84f4d57$export$248d38f6296296c5(x, y) {
380 | const ok = Object.keys, tx = typeof x, ty = typeof y;
381 | return x && y && tx === "object" && tx === ty ? ok(x).length === ok(y).length && ok(x).every((key)=>$f3ad94c9f84f4d57$export$248d38f6296296c5(x[key], y[key])) : x === y;
382 | }
383 |
384 |
385 |
386 |
387 |
388 | const $e46f4b33a7e1fc07$var$modifierHandlers = [];
389 | function $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3(name, handler) {
390 | $e46f4b33a7e1fc07$var$modifierHandlers.push({
391 | name: name,
392 | handler: handler
393 | });
394 | }
395 | function $e46f4b33a7e1fc07$export$f1696300e8775372(value, modifiers = []) {
396 | return modifiers.reduce((value, modifier)=>{
397 | const { name: name, args: args } = modifier;
398 | if ($e46f4b33a7e1fc07$var$modifierExists(name)) return $e46f4b33a7e1fc07$var$applyModifier(value, name, args);
399 | else {
400 | console.error(`Unknown modifier '${modifier}'`);
401 | return value;
402 | }
403 | }, value);
404 | }
405 | function $e46f4b33a7e1fc07$var$applyModifier(value, name, args = []) {
406 | return $e46f4b33a7e1fc07$var$getModifier(name).handler(value, args);
407 | }
408 | function $e46f4b33a7e1fc07$var$modifierExists(name) {
409 | return !!$e46f4b33a7e1fc07$var$getModifier(name);
410 | }
411 | function $e46f4b33a7e1fc07$var$getModifier(name) {
412 | return $e46f4b33a7e1fc07$var$modifierHandlers.find((modifier)=>modifier.name === name);
413 | }
414 | function $e46f4b33a7e1fc07$export$b03ed9b4aed11ed(modifier) {
415 | const matches = modifier.match(/^([^\(]+)(?=\((?=(.*)\)$)|$)/);
416 | if (matches && typeof matches[2] !== "undefined") {
417 | const argStr = matches[2].trim();
418 | const firstChar = argStr[0];
419 | const lastChar = argStr[argStr.length - 1];
420 | let argValue = null;
421 | if (firstChar === "'" && lastChar === "'" || firstChar === "`" && lastChar === "`" || firstChar === `"` && lastChar === `"`) argValue = argStr.slice(1, argStr.length - 1);
422 | else argValue = JSON.parse(argStr);
423 | return {
424 | name: matches[1],
425 | args: [
426 | argValue
427 | ]
428 | };
429 | } else return {
430 | name: modifier,
431 | args: []
432 | };
433 | }
434 |
435 |
436 |
437 |
438 | let $695a1f9e83b71f7c$var$directiveHandlers = {};
439 | let $695a1f9e83b71f7c$var$isDeferringHandlers = false;
440 | let $695a1f9e83b71f7c$var$directiveHandlerStacks = new Map();
441 | let $695a1f9e83b71f7c$var$currentHandlerStackKey = Symbol();
442 | let $695a1f9e83b71f7c$var$attributePrefix = "data-bind-";
443 | function $695a1f9e83b71f7c$export$99b43ad1ed32e735(name, callback) {
444 | $695a1f9e83b71f7c$var$directiveHandlers[name] = callback;
445 | }
446 | function $695a1f9e83b71f7c$export$19b57a1ea2e090cb(name) {
447 | return Object.keys($695a1f9e83b71f7c$var$directiveHandlers).includes(name);
448 | }
449 | function $695a1f9e83b71f7c$export$90a684c00f3df6ed(el, attributes) {
450 | let directives = [];
451 | if (el.__stimulusX_directives) directives = el.__stimulusX_directives;
452 | else {
453 | directives = Array.from(attributes).filter($695a1f9e83b71f7c$var$isDirectiveAttribute).map($695a1f9e83b71f7c$var$toParsedDirectives);
454 | if ((0, $50e97065b94a2e88$export$6df0712d20d2cc08)("compileDirectives") === true) el.__stimulusX_directives = directives;
455 | }
456 | return directives.flat().filter((d)=>d).map((directive)=>$695a1f9e83b71f7c$export$1dd40105af141b08(el, directive));
457 | }
458 | function $695a1f9e83b71f7c$export$3d81bdeca067fd2d(callback) {
459 | $695a1f9e83b71f7c$var$isDeferringHandlers = true;
460 | let key = Symbol();
461 | $695a1f9e83b71f7c$var$currentHandlerStackKey = key;
462 | $695a1f9e83b71f7c$var$directiveHandlerStacks.set(key, []);
463 | let flushHandlers = ()=>{
464 | while($695a1f9e83b71f7c$var$directiveHandlerStacks.get(key).length)$695a1f9e83b71f7c$var$directiveHandlerStacks.get(key).shift()();
465 | $695a1f9e83b71f7c$var$directiveHandlerStacks.delete(key);
466 | };
467 | let stopDeferring = ()=>{
468 | $695a1f9e83b71f7c$var$isDeferringHandlers = false;
469 | flushHandlers();
470 | };
471 | callback(flushHandlers);
472 | stopDeferring();
473 | }
474 | function $695a1f9e83b71f7c$export$a51f92c9c1609d03(el) {
475 | let cleanups = [];
476 | let cleanup = (callback)=>cleanups.push(callback);
477 | let [effect, cleanupEffect] = (0, $3ee5a2b2e05cc741$export$1ecd3170301acce1)(el);
478 | cleanups.push(cleanupEffect);
479 | let utilities = {
480 | effect: effect,
481 | cleanup: cleanup
482 | };
483 | let doCleanup = ()=>{
484 | cleanups.forEach((i)=>i());
485 | };
486 | return [
487 | utilities,
488 | doCleanup
489 | ];
490 | }
491 | function $695a1f9e83b71f7c$export$1dd40105af141b08(el, directive) {
492 | let handler = $695a1f9e83b71f7c$var$directiveHandlers[directive.type] || (()=>{});
493 | let [utilities, cleanup] = $695a1f9e83b71f7c$export$a51f92c9c1609d03(el);
494 | (0, $c6f8b3abaeac122e$export$5d89a587b01747c6)(el, directive.attr, cleanup);
495 | let wrapperHandler = ()=>{
496 | let controller = (0, $61c34dda51f70fa1$export$6d5f0ef1727b562e)(el, directive.identifier);
497 | if (controller) {
498 | if (!(0, $3ee5a2b2e05cc741$export$352205f445242f02)(controller)) {
499 | console.warn(`StimulusX: Directive attached to non-reactive controller '${directive.identifier}'`, el);
500 | return;
501 | }
502 | handler = handler.bind(handler, el, directive, {
503 | ...utilities,
504 | evaluate: $695a1f9e83b71f7c$var$evaluator(controller),
505 | modify: (0, $e46f4b33a7e1fc07$export$f1696300e8775372)
506 | });
507 | $695a1f9e83b71f7c$var$isDeferringHandlers ? $695a1f9e83b71f7c$var$directiveHandlerStacks.get($695a1f9e83b71f7c$var$currentHandlerStackKey).push(handler) : handler();
508 | } else console.error(`Controller '${directive.identifier}' not found`);
509 | };
510 | return wrapperHandler;
511 | }
512 | function $695a1f9e83b71f7c$var$evaluator(controller) {
513 | return (property)=>(0, $61c34dda51f70fa1$export$121af9acc174ac93)(controller, property);
514 | }
515 | function $695a1f9e83b71f7c$var$matchedAttributeRegex() {
516 | return new RegExp(`${$695a1f9e83b71f7c$var$attributePrefix}(${Object.keys($695a1f9e83b71f7c$var$directiveHandlers).join("|")})$`);
517 | }
518 | function $695a1f9e83b71f7c$var$isDirectiveAttribute({ name: name }) {
519 | return $695a1f9e83b71f7c$var$matchedAttributeRegex().test(name);
520 | }
521 | function $695a1f9e83b71f7c$var$toParsedDirectives({ name: name, value: value }) {
522 | const type = name.match($695a1f9e83b71f7c$var$matchedAttributeRegex())[1];
523 | const bindingExpressions = value.trim().split(/\s+(?![^\(]*\))/) // split string on all spaces not contained in parentheses
524 | .filter((e)=>e);
525 | return bindingExpressions.map((bindingExpression)=>{
526 | const subjectMatch = bindingExpression.match(/^([a-zA-Z0-9\-_]+)~/);
527 | const subject = subjectMatch ? subjectMatch[1] : null;
528 | let valueExpression = subject ? bindingExpression.replace(`${subject}~`, "") : bindingExpression;
529 | let modifiers = valueExpression.match(/\:[^:\]]+(?=[^\]]*$)/g) || [];
530 | modifiers = modifiers.map((i)=>i.replace(":", ""));
531 | valueExpression = valueExpression.split(":")[0];
532 | if (valueExpression[0] === "!") {
533 | valueExpression = valueExpression.slice(1);
534 | modifiers.push("not");
535 | }
536 | modifiers = modifiers.map((m)=>(0, $e46f4b33a7e1fc07$export$b03ed9b4aed11ed)(m));
537 | const identifierMatch = valueExpression.match(/^([a-zA-Z0-9\-_]+)#/);
538 | if (!identifierMatch) {
539 | console.warn(`Invalid binding descriptor ${bindingExpression}`);
540 | return null;
541 | }
542 | const identifier = identifierMatch[1];
543 | let property = identifier ? valueExpression.replace(`${identifier}#`, "") : valueExpression;
544 | return {
545 | type: type,
546 | subject: subject,
547 | modifiers: modifiers,
548 | identifier: identifier,
549 | property: property,
550 | attr: name
551 | };
552 | });
553 | }
554 |
555 |
556 |
557 | let $85d582547429ac89$var$markerCount = 1;
558 | let $85d582547429ac89$export$8d516e055d924071 = null;
559 | function $85d582547429ac89$export$2cd8252107eb640b(app, opts = {}) {
560 | const { optIn: optIn } = (0, $50e97065b94a2e88$export$add91eafeaeab287)(opts);
561 | $85d582547429ac89$export$8d516e055d924071 = app;
562 | // Override controller registration to insert a reactive subclass instead of the original
563 | $85d582547429ac89$export$8d516e055d924071.register = function(identifier, ControllerClass) {
564 | let controllerConstructor;
565 | if (optIn === false || ControllerClass.reactive === true) controllerConstructor = (0, $61c34dda51f70fa1$export$d56142fa17014959)(ControllerClass, $85d582547429ac89$export$8d516e055d924071);
566 | else controllerConstructor = ControllerClass;
567 | $85d582547429ac89$export$8d516e055d924071.load({
568 | identifier: identifier,
569 | controllerConstructor: controllerConstructor
570 | });
571 | };
572 | // Handle re-initializing reactive effects after Turbo morphing
573 | document.addEventListener("turbo:before-morph-element", $85d582547429ac89$export$24aca0785fb6fc3);
574 | document.addEventListener("turbo:morph-element", $85d582547429ac89$export$e08553505fddfb4d);
575 | // start watching the dom for changes
576 | (0, $c6f8b3abaeac122e$export$1a5ae5db40475a2d)();
577 | (0, $c6f8b3abaeac122e$export$c395e4fde41c37ff)((el)=>{
578 | // Controller root elements init their own tree when connected so we can skip them.
579 | // if (el.hasAttribute("data-controller")) return;
580 | (0, $eae25d6e66596517$export$bdd553fddd433dcb)(()=>$85d582547429ac89$export$b1432670dab450da(el));
581 | });
582 | (0, $c6f8b3abaeac122e$export$bb8862ef847f5ec0)((el)=>(0, $eae25d6e66596517$export$bdd553fddd433dcb)(()=>$85d582547429ac89$export$b68acd4f72f4a123(el)));
583 | (0, $c6f8b3abaeac122e$export$545f7104b1510552)((el, attrs)=>{
584 | $85d582547429ac89$var$handleValueAttributes(el, attrs);
585 | (0, $695a1f9e83b71f7c$export$90a684c00f3df6ed)(el, attrs).forEach((handle)=>handle());
586 | });
587 | }
588 | function $85d582547429ac89$export$b1432670dab450da(el) {
589 | (0, $695a1f9e83b71f7c$export$3d81bdeca067fd2d)(()=>{
590 | (0, $f3ad94c9f84f4d57$export$588732934346abbf)(el, (el)=>{
591 | if (el.__stimulusX_marker) return;
592 | (0, $695a1f9e83b71f7c$export$90a684c00f3df6ed)(el, el.attributes).forEach((handle)=>handle());
593 | el.__stimulusX_marker = $85d582547429ac89$var$markerCount++;
594 | });
595 | });
596 | }
597 | function $85d582547429ac89$export$b68acd4f72f4a123(root) {
598 | (0, $f3ad94c9f84f4d57$export$588732934346abbf)(root, (el)=>{
599 | (0, $c6f8b3abaeac122e$export$21fc366069a4f56f)(el);
600 | (0, $c6f8b3abaeac122e$export$2c8bfe603cc113da)(el);
601 | delete el.__stimulusX_directives;
602 | delete el.__stimulusX_marker;
603 | });
604 | }
605 | function $85d582547429ac89$export$24aca0785fb6fc3({ target: target, detail: { newElement: newElement } }) {
606 | if (!newElement && target.__stimulusX_marker) return $85d582547429ac89$export$b68acd4f72f4a123(target);
607 | delete target.__stimulusX_marker;
608 | }
609 | function $85d582547429ac89$export$e08553505fddfb4d({ target: target, detail: { newElement: newElement } }) {
610 | if (newElement) $85d582547429ac89$export$b1432670dab450da(target);
611 | }
612 | // Changes to controller value attributes in the DOM do not call
613 | // any properties on the controller so changes are not detected.
614 | // To fix this any value attribute changes are registered by calling
615 | // the value setter on the proxy with the current value - the value is
616 | // unchanged but calling the getter triggers any related effects.
617 | function $85d582547429ac89$var$handleValueAttributes(el, attrs) {
618 | if (!el.hasAttribute("data-controller")) return;
619 | const controllerNames = el.getAttribute("data-controller").trim().split(" ").filter((e)=>e);
620 | const valueAttributeMatcher = new RegExp(`^data-(${controllerNames.join("|")})-([a-zA-Z0-9\-_]+)-value$`);
621 | for(let i = 0; i < attrs.length; i++){
622 | const attr = attrs[i];
623 | const matches = attr.name.match(valueAttributeMatcher);
624 | if (matches && matches.length) {
625 | const identifier = matches[1];
626 | const valueName = matches[2];
627 | const controller = $85d582547429ac89$export$8d516e055d924071.getControllerForElementAndIdentifier(el, identifier);
628 | (0, $c6f8b3abaeac122e$export$c98382a3d82f9519)(()=>{
629 | controller[`${valueName}Value`] = controller[`${valueName}Value`];
630 | });
631 | }
632 | }
633 | }
634 |
635 |
636 |
637 |
638 |
639 |
640 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("downcase", (value)=>value.toString().toLowerCase());
641 |
642 |
643 |
644 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("gt", (value, args = [])=>{
645 | if (args.length === 0) {
646 | console.warn("Missing argument for `:gt` modifier");
647 | return false;
648 | }
649 | return value > args[0];
650 | });
651 |
652 |
653 |
654 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("gte", (value, args = [])=>{
655 | if (args.length === 0) {
656 | console.warn("Missing argument for `:gte` modifier");
657 | return false;
658 | }
659 | return value >= args[0];
660 | });
661 |
662 |
663 |
664 |
665 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("is", (value, args = [])=>{
666 | if (args.length === 0) {
667 | console.warn("Missing argument for `:is` modifier");
668 | return false;
669 | } else return (0, $f3ad94c9f84f4d57$export$248d38f6296296c5)(value, args[0]);
670 | });
671 |
672 |
673 |
674 |
675 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("isNot", (value, args = [])=>{
676 | if (args.length === 0) {
677 | console.warn("Missing argument for `:isNot` modifier");
678 | return false;
679 | } else return !(0, $f3ad94c9f84f4d57$export$248d38f6296296c5)(value, args[0]);
680 | });
681 |
682 |
683 |
684 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("lt", (value, args = [])=>{
685 | if (args.length === 0) {
686 | console.warn("Missing argument for `:lt` modifier");
687 | return false;
688 | }
689 | return value < args[0];
690 | });
691 |
692 |
693 |
694 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("lte", (value, args = [])=>{
695 | if (args.length === 0) {
696 | console.warn("Missing argument for `:lte` modifier");
697 | return false;
698 | }
699 | return value <= args[0];
700 | });
701 |
702 |
703 |
704 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("not", (value)=>!value);
705 |
706 |
707 |
708 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("strip", (value)=>value.toString().trim());
709 |
710 |
711 |
712 | (0, $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3)("upcase", (value)=>value.toString().toUpperCase());
713 |
714 |
715 |
716 |
717 | function $bf07eb3c6349a827$export$2706f8d45625eda6(el, value) {
718 | if (Array.isArray(value)) return $bf07eb3c6349a827$var$setClassesFromString(el, value.join(" "));
719 | else if (typeof value === "object" && value !== null) return $bf07eb3c6349a827$var$setClassesFromObject(el, value);
720 | return $bf07eb3c6349a827$var$setClassesFromString(el, value);
721 | }
722 | function $bf07eb3c6349a827$var$setClassesFromString(el, classString) {
723 | classString = classString || "";
724 | let missingClasses = (classString)=>classString.split(" ").filter((i)=>!el.classList.contains(i)).filter(Boolean);
725 | let classes = missingClasses(classString);
726 | el.classList.add(...classes);
727 | return ()=>el.classList.remove(...classes);
728 | }
729 | function $bf07eb3c6349a827$var$setClassesFromObject(el, classObject) {
730 | let split = (classString)=>classString.split(" ").filter(Boolean);
731 | let forAdd = Object.entries(classObject).flatMap(([classString, bool])=>bool ? split(classString) : false).filter(Boolean);
732 | let forRemove = Object.entries(classObject).flatMap(([classString, bool])=>!bool ? split(classString) : false).filter(Boolean);
733 | let added = [];
734 | let removed = [];
735 | forRemove.forEach((i)=>{
736 | if (el.classList.contains(i)) {
737 | el.classList.remove(i);
738 | removed.push(i);
739 | }
740 | });
741 | forAdd.forEach((i)=>{
742 | if (!el.classList.contains(i)) {
743 | el.classList.add(i);
744 | added.push(i);
745 | }
746 | });
747 | return ()=>{
748 | removed.forEach((i)=>el.classList.add(i));
749 | added.forEach((i)=>el.classList.remove(i));
750 | };
751 | }
752 |
753 |
754 | // As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute
755 | const $14a833556dde2961$var$booleanAttributes = new Set([
756 | "allowfullscreen",
757 | "async",
758 | "autofocus",
759 | "autoplay",
760 | "checked",
761 | "controls",
762 | "default",
763 | "defer",
764 | "disabled",
765 | "formnovalidate",
766 | "inert",
767 | "ismap",
768 | "itemscope",
769 | "loop",
770 | "multiple",
771 | "muted",
772 | "nomodule",
773 | "novalidate",
774 | "open",
775 | "playsinline",
776 | "readonly",
777 | "required",
778 | "reversed",
779 | "selected"
780 | ]);
781 | const $14a833556dde2961$var$preserveIfFalsey = [
782 | "aria-pressed",
783 | "aria-checked",
784 | "aria-expanded",
785 | "aria-selected"
786 | ];
787 | function $14a833556dde2961$export$2385a24977818dd0(element, name, value) {
788 | switch(name){
789 | case "class":
790 | $14a833556dde2961$var$bindClasses(element, value);
791 | break;
792 | case "checked":
793 | case "selected":
794 | $14a833556dde2961$var$bindAttributeAndProperty(element, name, value);
795 | break;
796 | default:
797 | $14a833556dde2961$var$bindAttribute(element, name, value);
798 | break;
799 | }
800 | }
801 | function $14a833556dde2961$var$bindClasses(element, value) {
802 | if (element.__stimulusX_undoClasses) element.__stimulusX_undoClasses();
803 | element.__stimulusX_undoClasses = (0, $bf07eb3c6349a827$export$2706f8d45625eda6)(element, value);
804 | }
805 | function $14a833556dde2961$var$bindAttribute(el, name, value) {
806 | if ([
807 | null,
808 | undefined,
809 | false
810 | ].includes(value) && $14a833556dde2961$var$attributeShouldntBePreservedIfFalsy(name)) el.removeAttribute(name);
811 | else {
812 | if ($14a833556dde2961$var$isBooleanAttr(name)) value = name;
813 | $14a833556dde2961$var$setIfChanged(el, name, value);
814 | }
815 | }
816 | function $14a833556dde2961$var$bindAttributeAndProperty(el, name, value) {
817 | $14a833556dde2961$var$bindAttribute(el, name, value);
818 | $14a833556dde2961$var$setPropertyIfChanged(el, name, value);
819 | }
820 | function $14a833556dde2961$var$setIfChanged(el, attrName, value) {
821 | if (el.getAttribute(attrName) != value) el.setAttribute(attrName, value);
822 | }
823 | function $14a833556dde2961$var$setPropertyIfChanged(el, propName, value) {
824 | if (el[propName] !== value) el[propName] = value;
825 | }
826 | function $14a833556dde2961$var$isBooleanAttr(attrName) {
827 | return $14a833556dde2961$var$booleanAttributes.has(attrName);
828 | }
829 | function $14a833556dde2961$var$attributeShouldntBePreservedIfFalsy(name) {
830 | return !$14a833556dde2961$var$preserveIfFalsey.includes(name);
831 | }
832 |
833 |
834 | (0, $695a1f9e83b71f7c$export$99b43ad1ed32e735)("attr", (el, { property: property, subject: subject, modifiers: modifiers }, { effect: effect, evaluate: evaluate, modify: modify })=>{
835 | effect(()=>{
836 | (0, $c6f8b3abaeac122e$export$c98382a3d82f9519)(()=>{
837 | const value = modify(evaluate(property), modifiers);
838 | (0, $14a833556dde2961$export$2385a24977818dd0)(el, subject, value);
839 | });
840 | });
841 | });
842 |
843 |
844 |
845 |
846 | (0, $695a1f9e83b71f7c$export$99b43ad1ed32e735)("text", (el, { property: property, modifiers: modifiers }, { effect: effect, evaluate: evaluate, modify: modify })=>{
847 | effect(()=>(0, $c6f8b3abaeac122e$export$c98382a3d82f9519)(()=>{
848 | const value = modify(evaluate(property), modifiers);
849 | el.textContent = value?.toString();
850 | }));
851 | });
852 |
853 |
854 | const $cf838c15c8b009ba$var$StimulusX = {
855 | init: $85d582547429ac89$export$2cd8252107eb640b,
856 | modifier: $e46f4b33a7e1fc07$export$cd4b50bb4e5c05a3,
857 | directive: $695a1f9e83b71f7c$export$99b43ad1ed32e735,
858 | nextTick: $eae25d6e66596517$export$bdd553fddd433dcb
859 | };
860 | var $cf838c15c8b009ba$export$2e2bcd8739ae039 = $cf838c15c8b009ba$var$StimulusX;
861 |
862 |
863 | export {$cf838c15c8b009ba$export$2e2bcd8739ae039 as default, $eae25d6e66596517$export$bdd553fddd433dcb as nextTick};
864 | //# sourceMappingURL=stimulus-x.js.map
865 |
--------------------------------------------------------------------------------
/dist/stimulus-x.js.map:
--------------------------------------------------------------------------------
1 | {"mappings":"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","sources":["src/index.js","src/lifecycle.js","src/scheduler.js","src/controller.js","src/reactivity.js","src/mutation.js","src/options.js","src/utils.js","src/directives.js","src/modifiers.js","src/modifiers/downcase.js","src/modifiers/gt.js","src/modifiers/gte.js","src/modifiers/is.js","src/modifiers/is-not.js","src/modifiers/lt.js","src/modifiers/lte.js","src/modifiers/not.js","src/modifiers/strip.js","src/modifiers/upcase.js","src/directives/attr.js","src/attributes.js","src/classes.js","src/directives/text.js"],"sourcesContent":["import { init } from \"./lifecycle\";\nimport { modifier } from \"./modifiers\";\nimport { directive } from \"./directives\";\nimport { nextTick } from \"./scheduler\";\n\nimport \"./modifiers/downcase\";\nimport \"./modifiers/gt\";\nimport \"./modifiers/gte\";\nimport \"./modifiers/is\";\nimport \"./modifiers/is-not\";\nimport \"./modifiers/lt\";\nimport \"./modifiers/lte\";\nimport \"./modifiers/not\";\nimport \"./modifiers/strip\";\nimport \"./modifiers/upcase\";\n\nimport \"./directives/attr\";\nimport \"./directives/text\";\n\nconst StimulusX = {\n init,\n modifier,\n directive,\n nextTick,\n};\n\nexport default StimulusX;\n\nexport { nextTick };\n","import { nextTick } from \"./scheduler\";\nimport { createReactiveControllerClass } from \"./controller\";\nimport { walk } from \"./utils\";\nimport {\n startObservingMutations,\n onAttributesAdded,\n onElAdded,\n onElRemoved,\n cleanupAttributes,\n cleanupElement,\n mutateDom,\n} from \"./mutation\";\nimport { deferHandlingDirectives, directives } from \"./directives\";\nimport { setOptions } from \"./options\";\n\nlet markerCount = 1;\nlet application = null;\n\nexport function init(app, opts = {}) {\n const { optIn } = setOptions(opts);\n application = app;\n\n // Override controller registration to insert a reactive subclass instead of the original\n application.register = function (identifier, ControllerClass) {\n let controllerConstructor;\n if (optIn === false || ControllerClass.reactive === true) {\n controllerConstructor = createReactiveControllerClass(ControllerClass, application);\n } else {\n controllerConstructor = ControllerClass;\n }\n\n application.load({\n identifier,\n controllerConstructor,\n });\n };\n\n // Handle re-initializing reactive effects after Turbo morphing\n document.addEventListener(\"turbo:before-morph-element\", beforeMorphElementCallback);\n document.addEventListener(\"turbo:morph-element\", morphElementCallback);\n\n // start watching the dom for changes\n startObservingMutations();\n\n onElAdded((el) => {\n // Controller root elements init their own tree when connected so we can skip them.\n // if (el.hasAttribute(\"data-controller\")) return;\n nextTick(() => initTree(el));\n });\n\n onElRemoved((el) => nextTick(() => destroyTree(el)));\n\n onAttributesAdded((el, attrs) => {\n handleValueAttributes(el, attrs);\n directives(el, attrs).forEach((handle) => handle());\n });\n}\n\nexport function initTree(el) {\n deferHandlingDirectives(() => {\n walk(el, (el) => {\n if (el.__stimulusX_marker) return;\n\n directives(el, el.attributes).forEach((handle) => handle());\n\n el.__stimulusX_marker = markerCount++;\n });\n });\n}\n\nexport function destroyTree(root) {\n walk(root, (el) => {\n cleanupElement(el);\n cleanupAttributes(el);\n delete el.__stimulusX_directives;\n delete el.__stimulusX_marker;\n });\n}\n\nexport function beforeMorphElementCallback({ target, detail: { newElement } }) {\n if (!newElement && target.__stimulusX_marker) {\n return destroyTree(target);\n }\n delete target.__stimulusX_marker;\n}\n\nexport function morphElementCallback({ target, detail: { newElement } }) {\n if (newElement) initTree(target);\n}\n\n// Changes to controller value attributes in the DOM do not call\n// any properties on the controller so changes are not detected.\n// To fix this any value attribute changes are registered by calling\n// the value setter on the proxy with the current value - the value is\n// unchanged but calling the getter triggers any related effects.\nfunction handleValueAttributes(el, attrs) {\n if (!el.hasAttribute(\"data-controller\")) return;\n\n const controllerNames = el\n .getAttribute(\"data-controller\")\n .trim()\n .split(\" \")\n .filter((e) => e);\n\n const valueAttributeMatcher = new RegExp(\n `^data-(${controllerNames.join(\"|\")})-([a-zA-Z0-9\\-_]+)-value$`\n );\n\n for (let i = 0; i < attrs.length; i++) {\n const attr = attrs[i];\n const matches = attr.name.match(valueAttributeMatcher);\n if (matches && matches.length) {\n const identifier = matches[1];\n const valueName = matches[2];\n const controller = application.getControllerForElementAndIdentifier(el, identifier);\n\n mutateDom(() => {\n controller[`${valueName}Value`] = controller[`${valueName}Value`];\n });\n }\n }\n}\n\nexport { application };\n","let flushPending = false;\nlet flushing = false;\nlet queue = [];\nlet lastFlushedIndex = -1;\nlet tickStack = [];\nlet isHolding = false;\n\nexport function scheduler(callback) {\n queueJob(callback);\n}\n\nexport function queueJob(job) {\n if (!queue.includes(job)) queue.push(job);\n\n queueFlush();\n}\n\nexport function dequeueJob(job) {\n let index = queue.indexOf(job);\n\n if (index !== -1 && index > lastFlushedIndex) queue.splice(index, 1);\n}\n\nfunction queueFlush() {\n if (!flushing && !flushPending) {\n flushPending = true;\n\n queueMicrotask(flushJobs);\n }\n}\n\nexport function flushJobs() {\n flushPending = false;\n flushing = true;\n\n for (let i = 0; i < queue.length; i++) {\n queue[i]();\n lastFlushedIndex = i;\n }\n\n queue.length = 0;\n lastFlushedIndex = -1;\n\n flushing = false;\n}\n\nexport function nextTick(callback = () => {}) {\n queueMicrotask(() => {\n isHolding ||\n setTimeout(() => {\n releaseNextTicks();\n });\n });\n\n return new Promise((res) => {\n tickStack.push(() => {\n callback();\n res();\n });\n });\n}\n\nexport function releaseNextTicks() {\n isHolding = false;\n\n while (tickStack.length) tickStack.shift()();\n}\n\nexport function holdNextTicks() {\n isHolding = true;\n}\n","import { getProperty } from \"dot-prop\";\nimport { application } from \"./lifecycle\";\nimport { reactive, shallowReactive, watch } from \"./reactivity\";\nimport { mutateDom } from \"./mutation\";\nimport { nextTick } from \"./scheduler\";\nimport { initTree } from \"./lifecycle\";\nimport { getOption } from \"./options\";\n\nexport function createReactiveControllerClass(ControllerClass) {\n return class extends ControllerClass {\n constructor(context) {\n super(context);\n\n // Override the attribute setter so that our mutation observer doesn't pick up on changes\n // that are also already being handled directly by Stimulus.\n const setData = this.data.set;\n this.data.set = (key, value) => {\n mutateDom(() => setData.call(this.data, key, value));\n };\n\n // Create a reactive controller object\n const trackDeep = getOption(\"trackDeep\") || this.constructor.reactive === \"deep\";\n const reactiveSelf = trackDeep ? reactive(this) : shallowReactive(this);\n\n // Initialize watched property callbacks\n const watchedProps = this.constructor.watch || [];\n watchedProps.forEach((prop) => watchControllerProperty(reactiveSelf, prop));\n\n // Return the reactive controller instance\n return reactiveSelf;\n }\n\n connect() {\n // Initialize the DOM tree and run directives when connected\n super.connect();\n nextTick(() => initTree(this.element));\n }\n };\n}\n\nexport function getClosestController(el, identifier) {\n const controllerElement = el.closest(`[data-controller~=\"${identifier}\"]`);\n if (controllerElement) {\n return application.getControllerForElementAndIdentifier(controllerElement, identifier);\n }\n}\n\nexport function evaluateControllerProperty(controller, property) {\n let value = getProperty(controller, property);\n if (typeof value === \"function\") {\n value = value.apply(controller);\n }\n return value;\n}\n\nexport function watchControllerProperty(controller, propertyRef) {\n const getter = () => evaluateControllerProperty(controller, propertyRef);\n const cleanup = watch(getter, (value, oldValue) => {\n callCallbacks(controller, propertyRef, value, oldValue, false);\n });\n\n // Run once on creation\n callCallbacks(controller, propertyRef, getter(), undefined, true);\n\n const rootElement = controller.element;\n if (!rootElement.__stimulusX_cleanups) rootElement.__stimulusX_cleanups = [];\n rootElement.__stimulusX_cleanups.push(cleanup);\n}\n\nfunction callCallbacks(controller, propertyRef, value, oldValue, initial) {\n // Generic callback, called when _any_ watched property changes\n if (typeof controller.watchedPropertyChanged === \"function\") {\n controller.watchedPropertyChanged(propertyRef, value, oldValue, { initial });\n }\n\n // Property-specific change callback\n const propertyWatcherCallback =\n controller[`${getCamelizedPropertyRef(propertyRef)}PropertyChanged`];\n if (typeof propertyWatcherCallback === \"function\") {\n propertyWatcherCallback.call(controller, value, oldValue, { initial });\n }\n}\n\nfunction getCamelizedPropertyRef(propertyRef) {\n return camelCase(propertyRef.replace(\".\", \" \"));\n}\n\nfunction camelCase(subject) {\n return subject.toLowerCase().replace(/-(\\w)/g, (match, char) => char.toUpperCase());\n}\n","import {\n effect as vueEffect,\n stop as release,\n reactive as vueReactive,\n shallowReactive as vueShallowReactive,\n isReactive as vueIsReactive,\n} from \"@vue/reactivity/dist/reactivity.esm-browser.prod.js\";\nimport { scheduler } from \"./scheduler\";\n\nconst isReactive = vueIsReactive;\nconst reactive = vueReactive;\nconst shallowReactive = vueShallowReactive;\n\nconst effect = (callback) =>\n vueEffect(callback, {\n scheduler: scheduler((task) => task),\n });\n\nexport function elementBoundEffect(el) {\n let cleanup = () => {};\n\n let wrappedEffect = (callback) => {\n let effectReference = effect(callback);\n\n if (!el.__stimulusX_effects) {\n el.__stimulusX_effects = new Set();\n }\n\n el.__stimulusX_effects.add(effectReference);\n\n cleanup = () => {\n if (effectReference === undefined) return;\n\n el.__stimulusX_effects.delete(effectReference);\n\n release(effectReference);\n };\n\n return effectReference;\n };\n\n return [\n wrappedEffect,\n () => {\n cleanup();\n },\n ];\n}\n\nexport function watch(getter, callback) {\n let firstTime = true;\n let oldValue;\n\n let effectReference = effect(() => {\n let value = getter();\n\n // JSON.stringify touches every single property at any level enabling deep watching\n JSON.stringify(value);\n\n if (!firstTime) {\n // We have to queue this watcher as a microtask so that\n // the watcher doesn't pick up its own dependencies.\n queueMicrotask(() => {\n callback(value, oldValue);\n\n oldValue = value;\n });\n } else {\n oldValue = value;\n }\n\n firstTime = false;\n });\n\n return () => release(effectReference);\n}\n\nexport { effect, release, reactive, shallowReactive, isReactive };\n","import { dequeueJob } from \"./scheduler\";\nlet onAttributeAddeds = [];\nlet onElRemoveds = [];\nlet onElAddeds = [];\nlet onValueAttributeChangeds = [];\nlet currentlyObserving = false;\nlet isCollecting = false;\nlet deferredMutations = [];\nlet observer = new MutationObserver(onMutate);\n\nexport function onElAdded(callback) {\n onElAddeds.push(callback);\n}\n\nexport function onElRemoved(el, callback) {\n if (typeof callback === \"function\") {\n if (!el.__stimulusX_cleanups) el.__stimulusX_cleanups = [];\n el.__stimulusX_cleanups.push(callback);\n } else {\n callback = el;\n onElRemoveds.push(callback);\n }\n}\n\nexport function onAttributesAdded(callback) {\n onAttributeAddeds.push(callback);\n}\n\nexport function onAttributeRemoved(el, name, callback) {\n if (!el.__stimulusX_attributeCleanups) el.__stimulusX_attributeCleanups = {};\n if (!el.__stimulusX_attributeCleanups[name]) el.__stimulusX_attributeCleanups[name] = [];\n\n el.__stimulusX_attributeCleanups[name].push(callback);\n}\n\nexport function onValueAttributeChanged(callback) {\n onValueAttributeChangeds.push(callback);\n}\n\nexport function cleanupAttributes(el, names) {\n if (!el.__stimulusX_attributeCleanups) return;\n\n Object.entries(el.__stimulusX_attributeCleanups).forEach(([name, value]) => {\n if (names === undefined || names.includes(name)) {\n value.forEach((i) => i());\n\n delete el.__stimulusX_attributeCleanups[name];\n }\n });\n}\n\nexport function cleanupElement(el) {\n el.__stimulusX_cleanups?.forEach(dequeueJob);\n\n while (el.__stimulusX_cleanups?.length) el.__stimulusX_cleanups.pop()();\n}\n\nexport function startObservingMutations() {\n observer.observe(document, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeOldValue: true,\n });\n\n currentlyObserving = true;\n}\n\nexport function stopObservingMutations() {\n flushObserver();\n\n observer.disconnect();\n\n currentlyObserving = false;\n}\n\nlet queuedMutations = [];\n\nexport function flushObserver() {\n let records = observer.takeRecords();\n\n queuedMutations.push(() => records.length > 0 && onMutate(records));\n\n let queueLengthWhenTriggered = queuedMutations.length;\n\n queueMicrotask(() => {\n // If these two lengths match, then we KNOW that this is the LAST\n // flush in the current event loop. This way, we can process\n // all mutations in one batch at the end of everything...\n if (queuedMutations.length === queueLengthWhenTriggered) {\n // Now Alpine can process all the mutations...\n while (queuedMutations.length > 0) queuedMutations.shift()();\n }\n });\n}\n\nexport function mutateDom(callback) {\n if (!currentlyObserving) return callback();\n\n stopObservingMutations();\n\n let result = callback();\n\n startObservingMutations();\n\n return result;\n}\n\nexport function deferMutations() {\n isCollecting = true;\n}\n\nexport function flushAndStopDeferringMutations() {\n isCollecting = false;\n\n onMutate(deferredMutations);\n\n deferredMutations = [];\n}\n\nfunction onMutate(mutations) {\n if (isCollecting) {\n deferredMutations = deferredMutations.concat(mutations);\n\n return;\n }\n\n let addedNodes = [];\n let removedNodes = new Set();\n let addedAttributes = new Map();\n let removedAttributes = new Map();\n\n for (let i = 0; i < mutations.length; i++) {\n if (mutations[i].target.__stimulusX_ignoreMutationObserver) continue;\n\n if (mutations[i].type === \"childList\") {\n mutations[i].removedNodes.forEach((node) => {\n if (node.nodeType !== 1) return;\n\n // No need to process removed nodes that haven't been initialized by Alpine...\n if (!node.__stimulusX_marker) return;\n\n removedNodes.add(node);\n });\n\n mutations[i].addedNodes.forEach((node) => {\n if (node.nodeType !== 1) return;\n\n // If the node is a removal as well, that means it's a \"move\" operation and we'll leave it alone...\n if (removedNodes.has(node)) {\n removedNodes.delete(node);\n\n return;\n }\n\n // If the node has already been initialized, we'll leave it alone...\n if (node.__stimulusX_marker) return;\n\n addedNodes.push(node);\n });\n }\n\n if (mutations[i].type === \"attributes\") {\n let el = mutations[i].target;\n let name = mutations[i].attributeName;\n let oldValue = mutations[i].oldValue;\n\n let add = () => {\n if (!addedAttributes.has(el)) addedAttributes.set(el, []);\n\n addedAttributes.get(el).push({ name, value: el.getAttribute(name) });\n };\n\n let remove = () => {\n if (!removedAttributes.has(el)) removedAttributes.set(el, []);\n\n removedAttributes.get(el).push(name);\n };\n\n // let valueAttributeChanged = () => {\n\n // };\n\n // New attribute.\n if (el.hasAttribute(name) && oldValue === null) {\n add();\n // Changed attribute.\n } else if (el.hasAttribute(name)) {\n remove();\n add();\n // Removed attribute.\n } else {\n remove();\n }\n }\n }\n\n removedAttributes.forEach((attrs, el) => {\n cleanupAttributes(el, attrs);\n });\n\n addedAttributes.forEach((attrs, el) => {\n onAttributeAddeds.forEach((i) => i(el, attrs));\n });\n\n // There are two special scenarios we need to account for when using the mutation\n // observer to init and destroy elements. First, when a node is \"moved\" on the page,\n // it's registered as both an \"add\" and a \"remove\", so we want to skip those.\n // (This is handled above by the .__stimulusX_marker conditionals...)\n // Second, when a node is \"wrapped\", it gets registered as a \"removal\" and the wrapper\n // as an \"addition\". We don't want to remove, then re-initialize the node, so we look\n // and see if it's inside any added nodes (wrappers) and skip it.\n // (This is handled below by the .contains conditional...)\n\n for (let node of removedNodes) {\n if (addedNodes.some((i) => i.contains(node))) continue;\n\n onElRemoveds.forEach((i) => i(node));\n }\n\n for (let node of addedNodes) {\n if (!node.isConnected) continue;\n\n onElAddeds.forEach((i) => i(node));\n }\n\n addedNodes = null;\n removedNodes = null;\n addedAttributes = null;\n removedAttributes = null;\n}\n","const defaultOptions = {\n optIn: false,\n compileDirectives: true,\n trackDeep: false,\n};\n\nlet options = defaultOptions;\n\nexport function getOption(key) {\n return options[key];\n}\n\nexport function getOptions() {\n return options;\n}\n\nexport function setOptions(opts) {\n options = Object.assign({}, defaultOptions, opts);\n return options;\n}\n","export function camelCase(subject) {\n return subject\n .replace(/:/g, \"_\")\n .split(\"_\")\n .map((word, index) => (index === 0 ? word : word[0].toUpperCase() + word.slice(1)))\n .join(\"\");\n}\n\nexport function walk(el, callback) {\n let skip = false;\n callback(el, () => (skip = true));\n if (skip) return;\n\n let node = el.firstElementChild;\n while (node) {\n walk(node, callback, false);\n node = node.nextElementSibling;\n }\n}\n\nexport function isEqual(x, y) {\n const ok = Object.keys,\n tx = typeof x,\n ty = typeof y;\n return x && y && tx === \"object\" && tx === ty\n ? ok(x).length === ok(y).length && ok(x).every((key) => isEqual(x[key], y[key]))\n : x === y;\n}\n","import { onAttributeRemoved } from \"./mutation\";\nimport { elementBoundEffect, isReactive } from \"./reactivity\";\nimport { applyModifiers, parseModifier } from \"./modifiers\";\nimport { getClosestController, evaluateControllerProperty } from \"./controller\";\nimport { getOption } from \"./options\";\n\nlet directiveHandlers = {};\nlet isDeferringHandlers = false;\nlet directiveHandlerStacks = new Map();\nlet currentHandlerStackKey = Symbol();\n\nlet attributePrefix = \"data-bind-\";\n\nexport function directive(name, callback) {\n directiveHandlers[name] = callback;\n}\n\nexport function directiveExists(name) {\n return Object.keys(directiveHandlers).includes(name);\n}\n\nexport function directives(el, attributes) {\n let directives = [];\n\n if (el.__stimulusX_directives) {\n directives = el.__stimulusX_directives;\n } else {\n directives = Array.from(attributes).filter(isDirectiveAttribute).map(toParsedDirectives);\n if (getOption(\"compileDirectives\") === true) el.__stimulusX_directives = directives;\n }\n\n return directives\n .flat()\n .filter((d) => d)\n .map((directive) => getDirectiveHandler(el, directive));\n}\n\nexport function deferHandlingDirectives(callback) {\n isDeferringHandlers = true;\n\n let key = Symbol();\n\n currentHandlerStackKey = key;\n directiveHandlerStacks.set(key, []);\n\n let flushHandlers = () => {\n while (directiveHandlerStacks.get(key).length) directiveHandlerStacks.get(key).shift()();\n directiveHandlerStacks.delete(key);\n };\n\n let stopDeferring = () => {\n isDeferringHandlers = false;\n flushHandlers();\n };\n\n callback(flushHandlers);\n stopDeferring();\n}\n\nexport function getElementBoundUtilities(el) {\n let cleanups = [];\n let cleanup = (callback) => cleanups.push(callback);\n let [effect, cleanupEffect] = elementBoundEffect(el);\n\n cleanups.push(cleanupEffect);\n\n let utilities = {\n effect,\n cleanup,\n };\n\n let doCleanup = () => {\n cleanups.forEach((i) => i());\n };\n\n return [utilities, doCleanup];\n}\n\nexport function getDirectiveHandler(el, directive) {\n let handler = directiveHandlers[directive.type] || (() => {});\n let [utilities, cleanup] = getElementBoundUtilities(el);\n\n onAttributeRemoved(el, directive.attr, cleanup);\n\n let wrapperHandler = () => {\n let controller = getClosestController(el, directive.identifier);\n if (controller) {\n if (!isReactive(controller)) {\n console.warn(\n `StimulusX: Directive attached to non-reactive controller '${directive.identifier}'`,\n el\n );\n return;\n }\n handler = handler.bind(handler, el, directive, {\n ...utilities,\n evaluate: evaluator(controller),\n modify: applyModifiers,\n });\n isDeferringHandlers\n ? directiveHandlerStacks.get(currentHandlerStackKey).push(handler)\n : handler();\n } else {\n console.error(`Controller '${directive.identifier}' not found`);\n }\n };\n\n return wrapperHandler;\n}\n\nfunction evaluator(controller) {\n return (property) => evaluateControllerProperty(controller, property);\n}\n\nfunction matchedAttributeRegex() {\n return new RegExp(`${attributePrefix}(${Object.keys(directiveHandlers).join(\"|\")})$`);\n}\n\nfunction isDirectiveAttribute({ name }) {\n return matchedAttributeRegex().test(name);\n}\n\nfunction toParsedDirectives({ name, value }) {\n const type = name.match(matchedAttributeRegex())[1];\n const bindingExpressions = value\n .trim()\n .split(/\\s+(?![^\\(]*\\))/) // split string on all spaces not contained in parentheses\n .filter((e) => e);\n\n return bindingExpressions.map((bindingExpression) => {\n const subjectMatch = bindingExpression.match(/^([a-zA-Z0-9\\-_]+)~/);\n const subject = subjectMatch ? subjectMatch[1] : null;\n let valueExpression = subject\n ? bindingExpression.replace(`${subject}~`, \"\")\n : bindingExpression;\n\n let modifiers = valueExpression.match(/\\:[^:\\]]+(?=[^\\]]*$)/g) || [];\n modifiers = modifiers.map((i) => i.replace(\":\", \"\"));\n\n valueExpression = valueExpression.split(\":\")[0];\n\n if (valueExpression[0] === \"!\") {\n valueExpression = valueExpression.slice(1);\n modifiers.push(\"not\");\n }\n\n modifiers = modifiers.map((m) => parseModifier(m));\n\n const identifierMatch = valueExpression.match(/^([a-zA-Z0-9\\-_]+)#/);\n if (!identifierMatch) {\n console.warn(`Invalid binding descriptor ${bindingExpression}`);\n return null;\n }\n\n const identifier = identifierMatch[1];\n let property = identifier ? valueExpression.replace(`${identifier}#`, \"\") : valueExpression;\n\n return {\n type,\n subject,\n modifiers,\n identifier,\n property,\n attr: name,\n };\n });\n}\n","const modifierHandlers = [];\n\nexport function modifier(name, handler) {\n modifierHandlers.push({\n name,\n handler,\n });\n}\n\nexport function applyModifiers(value, modifiers = []) {\n return modifiers.reduce((value, modifier) => {\n const { name, args } = modifier;\n if (modifierExists(name)) {\n return applyModifier(value, name, args);\n } else {\n console.error(`Unknown modifier '${modifier}'`);\n return value;\n }\n }, value);\n}\n\nfunction applyModifier(value, name, args = []) {\n return getModifier(name).handler(value, args);\n}\n\nfunction modifierExists(name) {\n return !!getModifier(name);\n}\n\nfunction getModifier(name) {\n return modifierHandlers.find((modifier) => modifier.name === name);\n}\n\nexport function parseModifier(modifier) {\n const matches = modifier.match(/^([^\\(]+)(?=\\((?=(.*)\\)$)|$)/);\n\n if (matches && typeof matches[2] !== \"undefined\") {\n const argStr = matches[2].trim();\n const firstChar = argStr[0];\n const lastChar = argStr[argStr.length - 1];\n let argValue = null;\n\n if (\n (firstChar === \"'\" && lastChar === \"'\") ||\n (firstChar === \"`\" && lastChar === \"`\") ||\n (firstChar === `\"` && lastChar === `\"`)\n ) {\n argValue = argStr.slice(1, argStr.length - 1);\n } else {\n argValue = JSON.parse(argStr);\n }\n\n return { name: matches[1], args: [argValue] };\n } else {\n return { name: modifier, args: [] };\n }\n}\n","import { modifier } from \"../modifiers\";\n\nmodifier(\"downcase\", (value) => value.toString().toLowerCase());\n","import { modifier } from \"../modifiers\";\n\nmodifier(\"gt\", (value, args = []) => {\n if (args.length === 0) {\n console.warn(\"Missing argument for `:gt` modifier\");\n return false;\n }\n\n return value > args[0];\n});\n","import { modifier } from \"../modifiers\";\n\nmodifier(\"gte\", (value, args = []) => {\n if (args.length === 0) {\n console.warn(\"Missing argument for `:gte` modifier\");\n return false;\n }\n\n return value >= args[0];\n});\n","import { modifier } from \"../modifiers\";\nimport { isEqual } from \"../utils\";\n\nmodifier(\"is\", (value, args = []) => {\n if (args.length === 0) {\n console.warn(\"Missing argument for `:is` modifier\");\n return false;\n } else {\n return isEqual(value, args[0]);\n }\n});\n","import { modifier } from \"../modifiers\";\nimport { isEqual } from \"../utils\";\n\nmodifier(\"isNot\", (value, args = []) => {\n if (args.length === 0) {\n console.warn(\"Missing argument for `:isNot` modifier\");\n return false;\n } else {\n return !isEqual(value, args[0]);\n }\n});\n","import { modifier } from \"../modifiers\";\n\nmodifier(\"lt\", (value, args = []) => {\n if (args.length === 0) {\n console.warn(\"Missing argument for `:lt` modifier\");\n return false;\n }\n\n return value < args[0];\n});\n","import { modifier } from \"../modifiers\";\n\nmodifier(\"lte\", (value, args = []) => {\n if (args.length === 0) {\n console.warn(\"Missing argument for `:lte` modifier\");\n return false;\n }\n\n return value <= args[0];\n});\n","import { modifier } from \"../modifiers\";\n\nmodifier(\"not\", (value) => !value);\n","import { modifier } from \"../modifiers\";\n\nmodifier(\"strip\", (value) => value.toString().trim());\n","import { modifier } from \"../modifiers\";\n\nmodifier(\"upcase\", (value) => value.toString().toUpperCase());\n","import { directive } from \"../directives\";\nimport { mutateDom } from \"../mutation\";\nimport { bind } from \"../attributes\";\n\ndirective(\"attr\", (el, { property, subject, modifiers }, { effect, evaluate, modify }) => {\n effect(() => {\n mutateDom(() => {\n const value = modify(evaluate(property), modifiers);\n bind(el, subject, value);\n });\n });\n});\n","import { setClasses } from \"./classes\";\n\n// As per HTML spec table https://html.spec.whatwg.org/multipage/indices.html#attributes-3:boolean-attribute\nconst booleanAttributes = new Set([\n \"allowfullscreen\",\n \"async\",\n \"autofocus\",\n \"autoplay\",\n \"checked\",\n \"controls\",\n \"default\",\n \"defer\",\n \"disabled\",\n \"formnovalidate\",\n \"inert\",\n \"ismap\",\n \"itemscope\",\n \"loop\",\n \"multiple\",\n \"muted\",\n \"nomodule\",\n \"novalidate\",\n \"open\",\n \"playsinline\",\n \"readonly\",\n \"required\",\n \"reversed\",\n \"selected\",\n]);\n\nconst preserveIfFalsey = [\"aria-pressed\", \"aria-checked\", \"aria-expanded\", \"aria-selected\"];\n\nexport function bind(element, name, value) {\n switch (name) {\n case \"class\":\n bindClasses(element, value);\n break;\n\n case \"checked\":\n case \"selected\":\n bindAttributeAndProperty(element, name, value);\n break;\n\n default:\n bindAttribute(element, name, value);\n break;\n }\n}\n\nfunction bindClasses(element, value) {\n if (element.__stimulusX_undoClasses) element.__stimulusX_undoClasses();\n element.__stimulusX_undoClasses = setClasses(element, value);\n}\n\nfunction bindAttribute(el, name, value) {\n if ([null, undefined, false].includes(value) && attributeShouldntBePreservedIfFalsy(name)) {\n el.removeAttribute(name);\n } else {\n if (isBooleanAttr(name)) value = name;\n setIfChanged(el, name, value);\n }\n}\n\nfunction bindAttributeAndProperty(el, name, value) {\n bindAttribute(el, name, value);\n setPropertyIfChanged(el, name, value);\n}\n\nfunction setIfChanged(el, attrName, value) {\n if (el.getAttribute(attrName) != value) {\n el.setAttribute(attrName, value);\n }\n}\n\nfunction setPropertyIfChanged(el, propName, value) {\n if (el[propName] !== value) {\n el[propName] = value;\n }\n}\n\nfunction isBooleanAttr(attrName) {\n return booleanAttributes.has(attrName);\n}\n\nfunction attributeShouldntBePreservedIfFalsy(name) {\n return !preserveIfFalsey.includes(name);\n}\n","export function setClasses(el, value) {\n if (Array.isArray(value)) {\n return setClassesFromString(el, value.join(\" \"));\n } else if (typeof value === \"object\" && value !== null) {\n return setClassesFromObject(el, value);\n }\n return setClassesFromString(el, value);\n}\n\nfunction setClassesFromString(el, classString) {\n classString = classString || \"\";\n let missingClasses = (classString) =>\n classString\n .split(\" \")\n .filter((i) => !el.classList.contains(i))\n .filter(Boolean);\n\n let classes = missingClasses(classString);\n el.classList.add(...classes);\n\n return () => el.classList.remove(...classes);\n}\n\nfunction setClassesFromObject(el, classObject) {\n let split = (classString) => classString.split(\" \").filter(Boolean);\n\n let forAdd = Object.entries(classObject)\n .flatMap(([classString, bool]) => (bool ? split(classString) : false))\n .filter(Boolean);\n let forRemove = Object.entries(classObject)\n .flatMap(([classString, bool]) => (!bool ? split(classString) : false))\n .filter(Boolean);\n\n let added = [];\n let removed = [];\n\n forRemove.forEach((i) => {\n if (el.classList.contains(i)) {\n el.classList.remove(i);\n removed.push(i);\n }\n });\n\n forAdd.forEach((i) => {\n if (!el.classList.contains(i)) {\n el.classList.add(i);\n added.push(i);\n }\n });\n\n return () => {\n removed.forEach((i) => el.classList.add(i));\n added.forEach((i) => el.classList.remove(i));\n };\n}\n","import { directive } from \"../directives\";\nimport { mutateDom } from \"../mutation\";\n\ndirective(\"text\", (el, { property, modifiers }, { effect, evaluate, modify }) => {\n effect(() =>\n mutateDom(() => {\n const value = modify(evaluate(property), modifiers);\n el.textContent = value?.toString();\n })\n );\n});\n"],"names":[],"version":3,"file":"stimulus-x.js.map","sourceRoot":"../"}
--------------------------------------------------------------------------------