diff --git a/docs/reference/actions.md b/docs/reference/actions.md
index 6aa43db3..487699f3 100644
--- a/docs/reference/actions.md
+++ b/docs/reference/actions.md
@@ -209,6 +209,43 @@ application.registerActionOption("open", ({ event, value }) => {
})
```
+Beyond boolean values, a custom action option can also capture a string value by
+wrapping it in parentheses. Declaring the option as `:name(value)` will yield a
+`value` argument set to the string `"value"` in the callback. This is useful for
+parameterizing an option, such as a `:throttled(500)` option that ignores events
+arriving within a configurable number of milliseconds of the previous one:
+
+
+
+```html
+
+
+```
+
+```javascript
+import { Application } from "@hotwired/stimulus"
+
+const application = Application.start()
+
+let lastInvokedAt = 0
+
+application.registerActionOption("throttled", ({ value }) => {
+ const wait = Number(value)
+ const elapsed = Date.now() - lastInvokedAt
+
+ if (elapsed > wait) {
+ lastInvokedAt = Date.now()
+ return true
+ } else {
+ return false
+ }
+})
+```
+
+Values captured this way are always passed as strings, so convert them to the
+type you need (for example, with `Number(value)`) inside the callback.
+
In order to prevent the event from being routed to the controller action, the
`registerActionOption` callback function must return `false`. Otherwise, to
route the event to the controller action, return `true`.
@@ -218,7 +255,7 @@ The callback accepts a single object argument with the following keys:
| Name | Description |
| ---------- | ----------------------------------------------------------------------------------------------------- |
| name | String: The option's name (`"open"` in the example above) |
-| value | Boolean: The value of the option (`:open` would yield `true`, `:!open` would yield `false`) |
+| value | Boolean or String: The value of the option (`:open` yields `true`, `:!open` yields `false`, `:name(value)` yields the string `"value"`) |
| event | [Event][]: The event instance, including with the `params` action parameters on the submitter element |
| element | [Element]: The element where the action descriptor is declared |
| controller | The `Controller` instance which would receive the method call |
diff --git a/src/core/action_descriptor.ts b/src/core/action_descriptor.ts
index 35a4a211..a40a5054 100644
--- a/src/core/action_descriptor.ts
+++ b/src/core/action_descriptor.ts
@@ -4,7 +4,7 @@ export type ActionDescriptorFilters = Record
export type ActionDescriptorFilter = (options: ActionDescriptorFilterOptions) => boolean
type ActionDescriptorFilterOptions = {
name: string
- value: boolean
+ value: boolean | string
event: Event
element: Element
controller: Controller
@@ -76,7 +76,15 @@ function parseEventTarget(eventTargetName: string): EventTarget | undefined {
function parseEventOptions(eventOptions: string): AddEventListenerOptions {
return eventOptions
.split(":")
- .reduce((options, token) => Object.assign(options, { [token.replace(/^!/, "")]: !/^!/.test(token) }), {})
+ .reduce((options, token) => {
+ const valueMatch = token.match(/^([^(]+)\(([^)]*)\)$/)
+
+ if (valueMatch) {
+ return Object.assign(options, { [valueMatch[1]]: valueMatch[2] })
+ }
+
+ return Object.assign(options, { [token.replace(/^!/, "")]: !/^!/.test(token) })
+ }, {})
}
export function stringifyEventTarget(eventTarget: EventTarget) {
diff --git a/src/tests/modules/core/event_options_tests.ts b/src/tests/modules/core/event_options_tests.ts
index 45027cd3..97534411 100644
--- a/src/tests/modules/core/event_options_tests.ts
+++ b/src/tests/modules/core/event_options_tests.ts
@@ -255,6 +255,53 @@ export default class EventOptionsTests extends LogControllerTestCase {
this.assertActions({ name: "log", eventType: "toggle" })
}
+ async "test custom option with parenthesized value"() {
+ let lastValue: boolean | string | undefined
+
+ this.application.registerActionOption("throttled", ({ value }) => {
+ lastValue = value
+ return true
+ })
+ await this.setAction(this.buttonElement, "click->c#log:throttled(500)")
+
+ await this.triggerEvent(this.buttonElement, "click")
+
+ this.assertActions({ name: "log", identifier: "c", eventType: "click", currentTarget: this.buttonElement })
+ this.assert.equal(lastValue, "500")
+ }
+
+ async "test custom option value alongside boolean options"() {
+ const received: { [key: string]: boolean | string } = {}
+
+ this.application.registerActionOption("throttled", ({ value }) => {
+ received.throttled = value
+ return true
+ })
+ this.application.registerActionOption("flag", ({ value }) => {
+ received.flag = value
+ return true
+ })
+ await this.setAction(this.buttonElement, "click->c#log:throttled(500):flag")
+
+ await this.triggerEvent(this.buttonElement, "click")
+
+ this.assertActions({ name: "log", identifier: "c", eventType: "click", currentTarget: this.buttonElement })
+ this.assert.equal(received.throttled, "500")
+ this.assert.equal(received.flag, true)
+ }
+
+ async "test custom option value controls whether action runs"() {
+ this.application.registerActionOption("enabled", ({ value }) => value === "yes")
+
+ await this.setAction(this.buttonElement, "click->c#log:enabled(no)")
+ await this.triggerEvent(this.buttonElement, "click")
+ this.assertNoActions()
+
+ await this.setAction(this.buttonElement, "click->c#log:enabled(yes)")
+ await this.triggerEvent(this.buttonElement, "click")
+ this.assertActions({ name: "log", identifier: "c", eventType: "click", currentTarget: this.buttonElement })
+ }
+
async "test custom action option callback event contains params"() {
let lastActionEventParams: Object = {}