├── .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 |
29 |
30 |
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 | //
41 | //
42 | //
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 | //
51 | //
52 | //
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 = ``; 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 |
34 |
35 |
36 |
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 |
46 |
47 |
48 |
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 |
59 |
60 |
61 |
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 |
34 |
35 |
36 | `); 37 | 38 | expect(getTestElement("output").textContent).toBe("DEFAULT STRING"); 39 | }); 40 | 41 | test("chained modifiers", async () => { 42 | const { getTestElement } = await context.testDOM(` 43 |
44 |
45 |
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 |
31 |
32 | 33 |
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 |
51 |
52 |
53 | `); 54 | 55 | await vi.waitFor(() => expect(getTestElement("count").textContent).toBe("2")); 56 | 57 | await context.performTurboStreamAction( 58 | "replace", 59 | "subject", 60 | ` 61 |
62 |
63 |
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 |
75 |
76 | 77 |
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 |
95 |
96 |
97 | `); 98 | 99 | expect(getTestElement("count").textContent).toBe("2"); 100 | 101 | await context.performTurboStreamAction( 102 | "morph", 103 | "subject", 104 | ` 105 |
106 |
107 |
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 |
28 |
29 |
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 |
42 |
43 |
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 |
76 |
77 |
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 |
89 |
90 |
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 |
110 |
111 |
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 |
42 |
43 |
44 |
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 |
54 |
55 |
56 |
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 |
68 |
69 |
70 |
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 |
80 |
81 |
82 |
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 |
94 |
95 |
96 |
97 |
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 |
108 |
109 |
110 |
111 |
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 |
42 |
43 |
44 |
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 |
54 |
55 |
56 |
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 |
68 |
69 |
70 |
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 |
80 |
81 |
82 |
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 |
94 |
95 |
96 |
97 |
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 |
108 |
109 |
110 |
111 |
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 |
33 |
34 |
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 |
45 |
46 |
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 |
55 |
56 |
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 |
65 |
66 |
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 |
75 |
76 |
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 |
92 |
93 |
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 |
110 |
117 |
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 | ![NPM Version](https://img.shields.io/npm/v/stimulus-x) 9 | [![CI](https://github.com/allmarkedup/stimulus-x/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/allmarkedup/stimulus-x/actions/workflows/ci.yml) 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 | 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 | 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 |
405 |
406 |
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 |

String transform modifiers

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":"../"} --------------------------------------------------------------------------------