stimulus-use-actions 0.3.3 → 0.3.5

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.
Files changed (3) hide show
  1. package/README.md +10 -3
  2. package/index.js +65 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,11 +20,13 @@ export default class extends Controller {
20
20
  static actions = {
21
21
  field: "input->update",
22
22
  checkbox: "change->rerender",
23
+ element: "submit->save",
23
24
  window: "resize->layout",
24
25
  }
25
26
 
26
27
  update(event) { /* ... */ }
27
28
  rerender(event) { /* ... */ }
29
+ save(event) { /* ... */ }
28
30
  layout(event) { /* ... */ }
29
31
  }
30
32
  ```
@@ -35,7 +37,7 @@ means:
35
37
  - Listeners are added on `connect()` and removed on `disconnect()`
36
38
  - Target elements can appear and disappear in the DOM at any time -- events are
37
39
  still captured
38
- - No need to call anything in `connect()` yourself
40
+ - No need to call `super.connect()` -- actions bind automatically
39
41
 
40
42
  ### Keys
41
43
 
@@ -45,6 +47,7 @@ Each key in `static actions` is a **target name** matching an entry in
45
47
  | Key | Listens on | Matches events from |
46
48
  | --------- | ------------------- | -------------------------------------------- |
47
49
  | `field` | controller element | descendants with `data-<id>-target="field"` |
50
+ | `element` | controller element | the controller element itself |
48
51
  | `window` | `window` | the window itself |
49
52
 
50
53
  ### Values
@@ -73,7 +76,11 @@ export default class extends Controller {
73
76
 
74
77
  connect() {
75
78
  useActions(this, {
76
- buttonTargets: ["click->submit", "keyup->preview"],
79
+ buttonTargets: [
80
+ "click->submit",
81
+ "keyup->preview",
82
+ ],
83
+ element: "submit->save",
77
84
  window: "resize->reflow",
78
85
  })
79
86
  }
@@ -92,7 +99,7 @@ observer.
92
99
 
93
100
  - **controller** -- your Stimulus controller instance (`this`)
94
101
  - **actions** -- map of target keys to action descriptors
95
- - Keys: `<name>Target`, `<name>Targets`, or `window`
102
+ - Keys: `<name>Target`, `<name>Targets`, `element`, or `window`
96
103
  - Values: a string or array of `"event->method"` strings
97
104
  - Event inference (e.g. `"submit"` without `click->`) is supported here --
98
105
  Stimulus infers the default event for the element
package/index.js CHANGED
@@ -78,6 +78,27 @@ function parseDescriptor(descriptor) {
78
78
  return { eventName, filter, methodName, options }
79
79
  }
80
80
 
81
+ function camelize(str) {
82
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
83
+ }
84
+
85
+ function typecast(value) {
86
+ try { return JSON.parse(value) } catch { return value }
87
+ }
88
+
89
+ function paramsForElement(element, identifier) {
90
+ const params = {}
91
+ const prefix = `data-${identifier}-`
92
+ const suffix = "-param"
93
+ for (const attr of element.attributes) {
94
+ if (attr.name.startsWith(prefix) && attr.name.endsWith(suffix)) {
95
+ const key = camelize(attr.name.slice(prefix.length, -suffix.length))
96
+ params[key] = typecast(attr.value)
97
+ }
98
+ }
99
+ return Object.freeze(params)
100
+ }
101
+
81
102
  function bindDelegatedActions(controller) {
82
103
  const actions = controller.constructor.actions
83
104
  if (!actions || typeof actions !== "object") return []
@@ -100,39 +121,42 @@ function bindDelegatedActions(controller) {
100
121
  if (!parsed) return
101
122
  const { eventName, filter, methodName, options } = parsed
102
123
 
124
+ let listenTarget, guard
103
125
  if (isWindow) {
104
- const handler = (event) => {
105
- if (filter && event.key !== (KEY_MAP[filter] || filter)) return
106
- if (options.includes("stop")) event.stopPropagation()
107
- if (options.includes("prevent")) event.preventDefault()
108
- controller[methodName](event)
109
- }
110
- window.addEventListener(eventName, handler)
111
- listeners.push({ target: window, eventName, handler })
126
+ listenTarget = window
127
+ guard = () => true
112
128
  } else if (isElement) {
113
- const handler = (event) => {
114
- if (filter && event.key !== (KEY_MAP[filter] || filter)) return
115
- if (options.includes("self") && event.target !== controller.element) return
116
- if (options.includes("stop")) event.stopPropagation()
117
- if (options.includes("prevent")) event.preventDefault()
118
- controller[methodName](event)
119
- }
120
- controller.element.addEventListener(eventName, handler)
121
- listeners.push({ target: controller.element, eventName, handler })
129
+ listenTarget = controller.element
130
+ guard = (event) => !options.includes("self") || event.target === controller.element
122
131
  } else {
123
132
  const selector = `[data-${identifier}-target~="${targetName}"]`
124
- const handler = (event) => {
133
+ listenTarget = controller.element
134
+ guard = (event) => {
125
135
  const matched = event.target.closest(selector)
126
- if (!matched || !controller.element.contains(matched)) return
127
- if (filter && event.key !== (KEY_MAP[filter] || filter)) return
128
- if (options.includes("self") && event.target !== matched) return
129
- if (options.includes("stop")) event.stopPropagation()
130
- if (options.includes("prevent")) event.preventDefault()
131
- controller[methodName](event)
136
+ if (!matched || !controller.element.contains(matched)) return false
137
+ if (options.includes("self") && event.target !== matched) return false
138
+ return matched
139
+ }
140
+ }
141
+
142
+ const handler = (event) => {
143
+ const match = guard(event)
144
+ if (!match) return
145
+ if (filter && event.key !== (KEY_MAP[filter] || filter)) return
146
+ if (options.includes("stop")) event.stopPropagation()
147
+ if (options.includes("prevent")) event.preventDefault()
148
+ if (match instanceof Element) {
149
+ Object.defineProperty(event, "currentTarget", { value: match, configurable: true })
150
+ Object.defineProperty(event, "params", {
151
+ value: paramsForElement(match, identifier),
152
+ configurable: true,
153
+ })
132
154
  }
133
- controller.element.addEventListener(eventName, handler)
134
- listeners.push({ target: controller.element, eventName, handler })
155
+ controller[methodName](event)
135
156
  }
157
+ const capture = !isWindow && !isElement
158
+ listenTarget.addEventListener(eventName, handler, capture)
159
+ listeners.push({ target: listenTarget, eventName, handler, capture })
136
160
  })
137
161
  })
138
162
 
@@ -140,19 +164,25 @@ function bindDelegatedActions(controller) {
140
164
  }
141
165
 
142
166
  function unbindDelegatedActions(listeners) {
143
- listeners.forEach(({ target, eventName, handler }) => {
144
- target.removeEventListener(eventName, handler)
167
+ listeners.forEach(({ target, eventName, handler, capture }) => {
168
+ target.removeEventListener(eventName, handler, capture)
145
169
  })
146
170
  }
147
171
 
148
172
  export class Controller extends StimulusController {
149
- connect() {
150
- super.connect()
151
- this._useActionListeners = bindDelegatedActions(this)
152
- }
153
- disconnect() {
154
- super.disconnect()
155
- unbindDelegatedActions(this._useActionListeners)
173
+ initialize() {
174
+ super.initialize()
156
175
  this._useActionListeners = []
176
+ const userConnect = this.connect
177
+ const userDisconnect = this.disconnect
178
+ this.connect = () => {
179
+ userConnect.call(this)
180
+ this._useActionListeners = bindDelegatedActions(this)
181
+ }
182
+ this.disconnect = () => {
183
+ unbindDelegatedActions(this._useActionListeners)
184
+ this._useActionListeners = []
185
+ userDisconnect.call(this)
186
+ }
157
187
  }
158
188
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stimulus-use-actions",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Stimulus mixin for declaring actions in the controller",
5
5
  "type": "module",
6
6
  "main": "index.js",