stimulus-use-actions 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -66
- package/index.js +101 -9
- package/package.json +2 -1
- package/.github/workflows/ci.yml +0 -17
- package/AGENTS.md +0 -39
- package/tests/controller-export.test.js +0 -37
- package/tests/integration.test.js +0 -102
- package/tests/static-actions.test.js +0 -60
- package/tests/window.test.js +0 -40
- package/vitest.config.mjs +0 -18
package/README.md
CHANGED
|
@@ -1,98 +1,110 @@
|
|
|
1
1
|
# stimulus-use-actions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Declare Stimulus `data-action` bindings in JavaScript instead of HTML markup.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/botandrose/stimulus-use-actions/actions/workflows/ci.yml)
|
|
6
6
|
|
|
7
7
|
Requires Stimulus v3+ and ES modules.
|
|
8
8
|
|
|
9
|
-
## Quick
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
Import `Controller` from this package instead of `@hotwired/stimulus`. Declare
|
|
12
|
+
`static actions` alongside your `static targets` -- that's it.
|
|
13
|
+
|
|
10
14
|
```js
|
|
11
|
-
|
|
12
|
-
import { Controller } from "@hotwired/stimulus";
|
|
13
|
-
import useActions from "stimulus-use-actions";
|
|
15
|
+
import { Controller } from "stimulus-use-actions"
|
|
14
16
|
|
|
15
17
|
export default class extends Controller {
|
|
16
|
-
static targets = ["
|
|
18
|
+
static targets = ["field", "checkbox"]
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// window-scoped actions
|
|
23
|
-
window: ["resize->reflow"],
|
|
24
|
-
});
|
|
20
|
+
static actions = {
|
|
21
|
+
field: "input->update",
|
|
22
|
+
checkbox: "change->rerender",
|
|
23
|
+
window: "resize->layout",
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
update(event) { /* ... */ }
|
|
27
|
+
rerender(event) { /* ... */ }
|
|
28
|
+
layout(event) { /* ... */ }
|
|
30
29
|
}
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
Actions are bound via **event delegation** on the controller element. This
|
|
33
|
+
means:
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
- Listeners are added on `connect()` and removed on `disconnect()`
|
|
36
|
+
- Target elements can appear and disappear in the DOM at any time -- events are
|
|
37
|
+
still captured
|
|
38
|
+
- No need to call anything in `connect()` yourself
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
static targets = ["button"];
|
|
40
|
+
### Keys
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
window: "resize->reflow",
|
|
45
|
-
};
|
|
42
|
+
Each key in `static actions` is a **target name** matching an entry in
|
|
43
|
+
`static targets`, or the special key `window`.
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
| Key | Listens on | Matches events from |
|
|
46
|
+
| --------- | ------------------- | -------------------------------------------- |
|
|
47
|
+
| `field` | controller element | descendants with `data-<id>-target="field"` |
|
|
48
|
+
| `window` | `window` | the window itself |
|
|
49
|
+
|
|
50
|
+
### Values
|
|
51
|
+
|
|
52
|
+
Values use Stimulus action descriptor syntax with an **explicit event**:
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
| Value | Meaning |
|
|
55
|
+
| ------------------------------ | ------------------------------------------ |
|
|
56
|
+
| `"input->update"` | listen for `input`, call `this.update()` |
|
|
57
|
+
| `"click->save"` | listen for `click`, call `this.save()` |
|
|
58
|
+
| `["click->a", "keyup->b"]` | multiple actions on one target |
|
|
59
|
+
|
|
60
|
+
Stimulus modifiers work as usual, e.g. `"keyup.enter->submit"`.
|
|
61
|
+
|
|
62
|
+
## Lower-Level API: `useActions`
|
|
63
|
+
|
|
64
|
+
For cases where you need manual control (conditional bindings, dynamic action
|
|
65
|
+
maps, etc.), import `useActions` directly:
|
|
57
66
|
|
|
58
67
|
```js
|
|
59
|
-
import
|
|
68
|
+
import { Controller } from "@hotwired/stimulus"
|
|
69
|
+
import useActions from "stimulus-use-actions"
|
|
60
70
|
|
|
61
|
-
class
|
|
62
|
-
static targets = ["button"]
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
connect() { useActions(this); }
|
|
71
|
-
}
|
|
71
|
+
export default class extends Controller {
|
|
72
|
+
static targets = ["button"]
|
|
73
|
+
|
|
74
|
+
connect() {
|
|
75
|
+
useActions(this, {
|
|
76
|
+
buttonTargets: ["click->submit", "keyup->preview"],
|
|
77
|
+
window: "resize->reflow",
|
|
78
|
+
})
|
|
79
|
+
}
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
submit() { /* ... */ }
|
|
82
|
+
preview() { /* ... */ }
|
|
83
|
+
reflow() { /* ... */ }
|
|
84
|
+
}
|
|
75
85
|
```
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
`useActions` binds directly to target elements (no delegation). It does **not**
|
|
88
|
+
clean up on disconnect -- Stimulus handles this through its own binding
|
|
89
|
+
observer.
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
- `actions`: map of target keys to action descriptors.
|
|
82
|
-
- Target keys: `<name>Target`, `<name>Targets`, or the special key `window`.
|
|
83
|
-
- Values: a string (e.g., `"click->save"`, `"save"`) or an array of such strings.
|
|
84
|
-
- If omitted, actions are read from `controller.constructor.actions`.
|
|
91
|
+
### `useActions(controller, actions?)`
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
93
|
+
- **controller** -- your Stimulus controller instance (`this`)
|
|
94
|
+
- **actions** -- map of target keys to action descriptors
|
|
95
|
+
- Keys: `<name>Target`, `<name>Targets`, or `window`
|
|
96
|
+
- Values: a string or array of `"event->method"` strings
|
|
97
|
+
- Event inference (e.g. `"submit"` without `click->`) is supported here --
|
|
98
|
+
Stimulus infers the default event for the element
|
|
99
|
+
- If omitted, reads from `controller.constructor.actions`
|
|
90
100
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- For `<name>Target`, the single element receives each action.
|
|
94
|
-
- `window` binds to the `@window` target (e.g., `"resize->reflow"`).
|
|
101
|
+
A `withActions(BaseController)` wrapper is also available -- it returns a
|
|
102
|
+
subclass that auto-calls `useActions(this)` in `connect()`.
|
|
95
103
|
|
|
96
104
|
## Notes
|
|
97
|
-
|
|
98
|
-
-
|
|
105
|
+
|
|
106
|
+
- `useActions` binds once per call -- calling repeatedly adds duplicate
|
|
107
|
+
bindings.
|
|
108
|
+
- The `Controller` base class uses delegation -- no duplicates, automatic
|
|
109
|
+
cleanup.
|
|
110
|
+
- Keep method names in sync with your controller; mismatches throw at runtime.
|
package/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { Controller as StimulusController } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// --- Direct binding via Stimulus internals (useActions, withActions) ---
|
|
4
|
+
|
|
1
5
|
function useActionsImpl(controller, actions) {
|
|
2
|
-
// Allow calling without explicit map: read from static/class property
|
|
3
6
|
if (!actions) actions = controller?.constructor?.actions || controller?.actions;
|
|
4
7
|
if (!actions || typeof actions !== "object") return;
|
|
5
8
|
const bindingObserver = controller.context.bindingObserver
|
|
@@ -33,23 +36,112 @@ function useActionsImpl(controller, actions) {
|
|
|
33
36
|
|
|
34
37
|
export default useActionsImpl
|
|
35
38
|
|
|
36
|
-
// Helper to wrap a Stimulus Controller class so actions bind automatically
|
|
37
39
|
export function withActions(BaseController) {
|
|
38
40
|
return class WithActions extends BaseController {
|
|
39
41
|
connect() {
|
|
40
|
-
|
|
41
|
-
// Bind actions declared on the class via `static actions = {}`
|
|
42
|
-
// Call with only controller; it will resolve static actions
|
|
42
|
+
super.connect()
|
|
43
43
|
useActionsImpl(this)
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
import { Controller as StimulusController } from "@hotwired/stimulus"
|
|
48
47
|
|
|
49
|
-
//
|
|
48
|
+
// --- Delegated binding (Controller) ---
|
|
49
|
+
|
|
50
|
+
const KEY_MAP = {
|
|
51
|
+
enter: "Enter",
|
|
52
|
+
tab: "Tab",
|
|
53
|
+
esc: "Escape",
|
|
54
|
+
space: " ",
|
|
55
|
+
up: "ArrowUp",
|
|
56
|
+
down: "ArrowDown",
|
|
57
|
+
left: "ArrowLeft",
|
|
58
|
+
right: "ArrowRight",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseDescriptor(descriptor) {
|
|
62
|
+
const arrowIndex = descriptor.indexOf("->")
|
|
63
|
+
if (arrowIndex === -1) return null
|
|
64
|
+
const eventPart = descriptor.substring(0, arrowIndex)
|
|
65
|
+
const methodPart = descriptor.substring(arrowIndex + 2)
|
|
66
|
+
const dotIndex = eventPart.indexOf(".")
|
|
67
|
+
let eventName, filter
|
|
68
|
+
if (dotIndex === -1) {
|
|
69
|
+
eventName = eventPart
|
|
70
|
+
filter = null
|
|
71
|
+
} else {
|
|
72
|
+
eventName = eventPart.substring(0, dotIndex)
|
|
73
|
+
filter = eventPart.substring(dotIndex + 1)
|
|
74
|
+
}
|
|
75
|
+
const parts = methodPart.split(":")
|
|
76
|
+
const methodName = parts[0]
|
|
77
|
+
const options = parts.slice(1)
|
|
78
|
+
return { eventName, filter, methodName, options }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function bindDelegatedActions(controller) {
|
|
82
|
+
const actions = controller.constructor.actions
|
|
83
|
+
if (!actions || typeof actions !== "object") return []
|
|
84
|
+
const identifier = controller.identifier
|
|
85
|
+
const listeners = []
|
|
86
|
+
|
|
87
|
+
Object.entries(actions).forEach(([key, descriptors]) => {
|
|
88
|
+
const descriptorList = Array.isArray(descriptors) ? descriptors : [descriptors]
|
|
89
|
+
const isWindow = key === "window"
|
|
90
|
+
let targetName
|
|
91
|
+
if (!isWindow) {
|
|
92
|
+
if (key.endsWith("Targets")) targetName = key.slice(0, -7)
|
|
93
|
+
else if (key.endsWith("Target")) targetName = key.slice(0, -6)
|
|
94
|
+
else targetName = key
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
descriptorList.forEach(descriptor => {
|
|
98
|
+
const parsed = parseDescriptor(descriptor)
|
|
99
|
+
if (!parsed) return
|
|
100
|
+
const { eventName, filter, methodName, options } = parsed
|
|
101
|
+
|
|
102
|
+
if (isWindow) {
|
|
103
|
+
const handler = (event) => {
|
|
104
|
+
if (filter && event.key !== (KEY_MAP[filter] || filter)) return
|
|
105
|
+
if (options.includes("stop")) event.stopPropagation()
|
|
106
|
+
if (options.includes("prevent")) event.preventDefault()
|
|
107
|
+
controller[methodName](event)
|
|
108
|
+
}
|
|
109
|
+
window.addEventListener(eventName, handler)
|
|
110
|
+
listeners.push({ target: window, eventName, handler })
|
|
111
|
+
} else {
|
|
112
|
+
const selector = `[data-${identifier}-target~="${targetName}"]`
|
|
113
|
+
const handler = (event) => {
|
|
114
|
+
const matched = event.target.closest(selector)
|
|
115
|
+
if (!matched || !controller.element.contains(matched)) return
|
|
116
|
+
if (filter && event.key !== (KEY_MAP[filter] || filter)) return
|
|
117
|
+
if (options.includes("self") && event.target !== matched) return
|
|
118
|
+
if (options.includes("stop")) event.stopPropagation()
|
|
119
|
+
if (options.includes("prevent")) event.preventDefault()
|
|
120
|
+
controller[methodName](event)
|
|
121
|
+
}
|
|
122
|
+
controller.element.addEventListener(eventName, handler)
|
|
123
|
+
listeners.push({ target: controller.element, eventName, handler })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return listeners
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function unbindDelegatedActions(listeners) {
|
|
132
|
+
listeners.forEach(({ target, eventName, handler }) => {
|
|
133
|
+
target.removeEventListener(eventName, handler)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
50
137
|
export class Controller extends StimulusController {
|
|
51
138
|
connect() {
|
|
52
|
-
|
|
53
|
-
|
|
139
|
+
super.connect()
|
|
140
|
+
this._useActionListeners = bindDelegatedActions(this)
|
|
141
|
+
}
|
|
142
|
+
disconnect() {
|
|
143
|
+
super.disconnect()
|
|
144
|
+
unbindDelegatedActions(this._useActionListeners)
|
|
145
|
+
this._useActionListeners = []
|
|
54
146
|
}
|
|
55
147
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stimulus-use-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Stimulus mixin for declaring actions in the controller",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"files": ["index.js"],
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "vitest run",
|
|
8
9
|
"test:watch": "vitest",
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
on: [ push, pull_request ]
|
|
3
|
-
jobs:
|
|
4
|
-
test:
|
|
5
|
-
runs-on: ubuntu-latest
|
|
6
|
-
steps:
|
|
7
|
-
- name: Checkout
|
|
8
|
-
uses: actions/checkout@v4
|
|
9
|
-
- name: Setup Node
|
|
10
|
-
uses: actions/setup-node@v4
|
|
11
|
-
with:
|
|
12
|
-
node-version: 20
|
|
13
|
-
cache: npm
|
|
14
|
-
- name: Install dependencies
|
|
15
|
-
run: npm ci
|
|
16
|
-
- name: Run tests with coverage
|
|
17
|
-
run: npm run ci
|
package/AGENTS.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Repository Guidelines
|
|
2
|
-
|
|
3
|
-
## Project Structure & Module Organization
|
|
4
|
-
- Root files: `index.js` (library entry), `package.json` (metadata/scripts).
|
|
5
|
-
- No build step; the module exports an ES module function for Stimulus controllers.
|
|
6
|
-
- Suggested growth: place future sources in `src/` and re-export from `index.js`; put tests in `tests/`.
|
|
7
|
-
|
|
8
|
-
## Build, Test, and Development Commands
|
|
9
|
-
- `npm install`: installs dev tools if added later. Currently no runtime deps.
|
|
10
|
-
- `npm test`: placeholder that exits with error. If you add tests, replace with a real runner (e.g., `vitest` or `jest`).
|
|
11
|
-
- Local check: import `index.js` in a small sandbox app to validate Stimulus behavior.
|
|
12
|
-
|
|
13
|
-
## Coding Style & Naming Conventions
|
|
14
|
-
- Indentation: 2 spaces; avoid semicolons; prefer double quotes for strings; use template literals where helpful.
|
|
15
|
-
- Exports: default export is the library entry. Keep API surface small and documented in JSDoc.
|
|
16
|
-
- Naming: reflect Stimulus conventions. Keys in the `actions` map should match controller properties (e.g., `buttonTarget`, `buttonTargets`, or `window`). Action strings may be "click->method" or "method".
|
|
17
|
-
|
|
18
|
-
## Testing Guidelines
|
|
19
|
-
- Framework: Vitest with `happy-dom`. Name files `*.test.js`.
|
|
20
|
-
- Location: co-locate tests next to files or under `tests/`.
|
|
21
|
-
- Coverage: enforced at 100% (lines, functions, branches, statements). Use `npm run coverage`.
|
|
22
|
-
- Scope: cover multiple targets, `window` target, and event prefixes like `click->`.
|
|
23
|
-
- Run: `npm test` for a single run; `npm run test:watch` in dev.
|
|
24
|
-
|
|
25
|
-
## Commit & Pull Request Guidelines
|
|
26
|
-
- Commits: use Conventional Commits when possible (e.g., `feat: add window target support`, `fix: handle single target elements`). Keep commits focused.
|
|
27
|
-
- PRs: include a concise description, rationale, example usage, and any behavioral changes. Link issues. Add before/after snippets when touching the API.
|
|
28
|
-
|
|
29
|
-
## Example Usage (for sanity checks)
|
|
30
|
-
```js
|
|
31
|
-
import useActions from "./index.js";
|
|
32
|
-
// Inside a Stimulus controller
|
|
33
|
-
connect() {
|
|
34
|
-
useActions(this, {
|
|
35
|
-
buttonTargets: ["click->submit", "keyup->preview"],
|
|
36
|
-
window: "resize->reflow",
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
```
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { Application } from "@hotwired/stimulus";
|
|
3
|
-
import { Controller } from "../index.js";
|
|
4
|
-
|
|
5
|
-
function nextTick() { return new Promise((r) => setTimeout(r, 0)); }
|
|
6
|
-
|
|
7
|
-
describe("exported Controller base", () => {
|
|
8
|
-
let app, root;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
document.body.innerHTML = "";
|
|
12
|
-
root = document.createElement("div");
|
|
13
|
-
document.body.appendChild(root);
|
|
14
|
-
app = Application.start(root);
|
|
15
|
-
await nextTick();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("auto-binds static actions without calling useActions manually", async () => {
|
|
19
|
-
root.innerHTML = `
|
|
20
|
-
<div data-controller="demo">
|
|
21
|
-
<button id="btn" data-demo-target="button"></button>
|
|
22
|
-
</div>
|
|
23
|
-
`;
|
|
24
|
-
|
|
25
|
-
const calls = { submit: 0 };
|
|
26
|
-
class DemoController extends Controller {
|
|
27
|
-
static targets = ["button"];
|
|
28
|
-
static actions = { buttonTarget: "click->submit" };
|
|
29
|
-
submit() { calls.submit++; }
|
|
30
|
-
}
|
|
31
|
-
app.register("demo", DemoController);
|
|
32
|
-
await nextTick();
|
|
33
|
-
document.getElementById("btn").dispatchEvent(new Event("click", { bubbles: true }));
|
|
34
|
-
expect(calls.submit).toBe(1);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { Application, Controller } from "@hotwired/stimulus";
|
|
3
|
-
import useActions from "../index.js";
|
|
4
|
-
|
|
5
|
-
function nextTick() {
|
|
6
|
-
return new Promise((r) => setTimeout(r, 0));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
describe("Stimulus integration", () => {
|
|
10
|
-
let app, root;
|
|
11
|
-
|
|
12
|
-
beforeEach(async () => {
|
|
13
|
-
document.body.innerHTML = "";
|
|
14
|
-
root = document.createElement("div");
|
|
15
|
-
document.body.appendChild(root);
|
|
16
|
-
app = Application.start(root);
|
|
17
|
-
await nextTick();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("handles plural targets with prefixed events", async () => {
|
|
21
|
-
// Build DOM with controller and two target elements
|
|
22
|
-
root.innerHTML = `
|
|
23
|
-
<div data-controller="demo">
|
|
24
|
-
<button id="a" data-demo-target="button"></button>
|
|
25
|
-
<button id="b" data-demo-target="button"></button>
|
|
26
|
-
</div>
|
|
27
|
-
`;
|
|
28
|
-
|
|
29
|
-
const calls = { submit: 0, preview: 0, reflow: 0 };
|
|
30
|
-
|
|
31
|
-
class DemoController extends Controller {
|
|
32
|
-
static targets = ["button"];
|
|
33
|
-
connect() {
|
|
34
|
-
useActions(this, {
|
|
35
|
-
buttonTargets: ["click->submit", "keyup->preview"],
|
|
36
|
-
window: "resize->reflow",
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
submit() { calls.submit++; }
|
|
40
|
-
preview() { calls.preview++; }
|
|
41
|
-
reflow() { calls.reflow++; }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
app.register("demo", DemoController);
|
|
45
|
-
await nextTick();
|
|
46
|
-
|
|
47
|
-
const btnA = document.getElementById("a");
|
|
48
|
-
const btnB = document.getElementById("b");
|
|
49
|
-
|
|
50
|
-
// Fire events on buttons
|
|
51
|
-
btnA.dispatchEvent(new Event("click", { bubbles: true }));
|
|
52
|
-
btnB.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true }));
|
|
53
|
-
// Fire resize on window
|
|
54
|
-
window.dispatchEvent(new Event("resize"));
|
|
55
|
-
|
|
56
|
-
expect(calls.submit).toBe(1);
|
|
57
|
-
expect(calls.preview).toBe(1);
|
|
58
|
-
expect(calls.reflow).toBe(1);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("handles single target without event prefix (defaults to click)", async () => {
|
|
62
|
-
root.innerHTML = `
|
|
63
|
-
<div data-controller="demo">
|
|
64
|
-
<button id="only" data-demo-target="button"></button>
|
|
65
|
-
</div>
|
|
66
|
-
`;
|
|
67
|
-
|
|
68
|
-
let called = 0;
|
|
69
|
-
class DemoController extends Controller {
|
|
70
|
-
static targets = ["button"];
|
|
71
|
-
connect() { useActions(this, { buttonTarget: "submit" }); }
|
|
72
|
-
submit() { called++; }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
app.register("demo", DemoController);
|
|
76
|
-
await nextTick();
|
|
77
|
-
const btn = document.getElementById("only");
|
|
78
|
-
btn.dispatchEvent(new Event("click", { bubbles: true }));
|
|
79
|
-
expect(called).toBe(1);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("supports multiple actions array on a single target", async () => {
|
|
83
|
-
root.innerHTML = `
|
|
84
|
-
<div data-controller="demo">
|
|
85
|
-
<button id="only" data-demo-target="button"></button>
|
|
86
|
-
</div>
|
|
87
|
-
`;
|
|
88
|
-
const called = { a: 0, b: 0 };
|
|
89
|
-
class DemoController extends Controller {
|
|
90
|
-
static targets = ["button"];
|
|
91
|
-
connect() { useActions(this, { buttonTarget: ["click->a", "click->b"] }); }
|
|
92
|
-
a() { called.a++; }
|
|
93
|
-
b() { called.b++; }
|
|
94
|
-
}
|
|
95
|
-
app.register("demo", DemoController);
|
|
96
|
-
await nextTick();
|
|
97
|
-
const btn = document.getElementById("only");
|
|
98
|
-
btn.dispatchEvent(new Event("click", { bubbles: true }));
|
|
99
|
-
expect(called.a).toBe(1);
|
|
100
|
-
expect(called.b).toBe(1);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { Application, Controller } from "@hotwired/stimulus";
|
|
3
|
-
import useActions, { withActions } from "../index.js";
|
|
4
|
-
|
|
5
|
-
function nextTick() { return new Promise((r) => setTimeout(r, 0)); }
|
|
6
|
-
|
|
7
|
-
describe("static actions support", () => {
|
|
8
|
-
let app, root;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
document.body.innerHTML = "";
|
|
12
|
-
root = document.createElement("div");
|
|
13
|
-
document.body.appendChild(root);
|
|
14
|
-
app = Application.start(root);
|
|
15
|
-
await nextTick();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("binds from static actions when calling useActions(this)", async () => {
|
|
19
|
-
root.innerHTML = `
|
|
20
|
-
<div data-controller="demo">
|
|
21
|
-
<button id="one" data-demo-target="button"></button>
|
|
22
|
-
</div>
|
|
23
|
-
`;
|
|
24
|
-
|
|
25
|
-
const calls = { submit: 0 };
|
|
26
|
-
class DemoController extends Controller {
|
|
27
|
-
static targets = ["button"];
|
|
28
|
-
static actions = { buttonTarget: "click->submit" };
|
|
29
|
-
connect() { useActions(this); }
|
|
30
|
-
submit() { calls.submit++; }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
app.register("demo", DemoController);
|
|
34
|
-
await nextTick();
|
|
35
|
-
document.getElementById("one").dispatchEvent(new Event("click", { bubbles: true }));
|
|
36
|
-
expect(calls.submit).toBe(1);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("auto-binds via withActions() without calling connect helper", async () => {
|
|
40
|
-
root.innerHTML = `
|
|
41
|
-
<div data-controller="demo">
|
|
42
|
-
<button id="two" data-demo-target="button"></button>
|
|
43
|
-
</div>
|
|
44
|
-
`;
|
|
45
|
-
|
|
46
|
-
const calls = { submit: 0 };
|
|
47
|
-
class Base extends Controller {
|
|
48
|
-
static targets = ["button"];
|
|
49
|
-
static actions = { buttonTarget: "click->submit" };
|
|
50
|
-
submit() { calls.submit++; }
|
|
51
|
-
}
|
|
52
|
-
const DemoController = withActions(Base);
|
|
53
|
-
|
|
54
|
-
app.register("demo", DemoController);
|
|
55
|
-
await nextTick();
|
|
56
|
-
document.getElementById("two").dispatchEvent(new Event("click", { bubbles: true }));
|
|
57
|
-
expect(calls.submit).toBe(1);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
package/tests/window.test.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { Application, Controller } from "@hotwired/stimulus";
|
|
3
|
-
import useActions from "../index.js";
|
|
4
|
-
|
|
5
|
-
function nextTick() { return new Promise((r) => setTimeout(r, 0)); }
|
|
6
|
-
|
|
7
|
-
describe("Window actions", () => {
|
|
8
|
-
let app, root;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
document.body.innerHTML = "";
|
|
12
|
-
root = document.createElement("div");
|
|
13
|
-
document.body.appendChild(root);
|
|
14
|
-
app = Application.start(root);
|
|
15
|
-
await nextTick();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("handles multiple window events via array", async () => {
|
|
19
|
-
root.innerHTML = `<div data-controller="demo"></div>`;
|
|
20
|
-
const hits = { resize: 0, scroll: 0 };
|
|
21
|
-
|
|
22
|
-
class DemoController extends Controller {
|
|
23
|
-
connect() {
|
|
24
|
-
useActions(this, { window: ["resize->onResize", "scroll->onScroll"] });
|
|
25
|
-
}
|
|
26
|
-
onResize() { hits.resize++; }
|
|
27
|
-
onScroll() { hits.scroll++; }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
app.register("demo", DemoController);
|
|
31
|
-
await nextTick();
|
|
32
|
-
|
|
33
|
-
window.dispatchEvent(new Event("resize"));
|
|
34
|
-
window.dispatchEvent(new Event("scroll"));
|
|
35
|
-
|
|
36
|
-
expect(hits.resize).toBe(1);
|
|
37
|
-
expect(hits.scroll).toBe(1);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
package/vitest.config.mjs
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
environment: "happy-dom",
|
|
6
|
-
coverage: {
|
|
7
|
-
provider: "v8",
|
|
8
|
-
reportsDirectory: "./coverage",
|
|
9
|
-
reporter: ["text", "lcov", "json-summary", "html"],
|
|
10
|
-
thresholds: {
|
|
11
|
-
lines: 100,
|
|
12
|
-
functions: 100,
|
|
13
|
-
branches: 100,
|
|
14
|
-
statements: 100,
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
});
|