diff --git a/docs/reference/actions.md b/docs/reference/actions.md
index aebae6ab..5d75894d 100644
--- a/docs/reference/actions.md
+++ b/docs/reference/actions.md
@@ -52,7 +52,7 @@ Stimulus lets you shorten the action descriptors for some common element/event p
```
-The full set of these shorthand pairs is as follows:
+The built-in set of these shorthand pairs is as follows:
Element | Default Event
----------------- | -------------
@@ -65,6 +65,27 @@ input type=submit | click
select | change
textarea | input
+This built-in set can be extended with the `Application.registerDefaultEventNames` method, for example to associate default event names with custom elements.
+
+
+
+```js
+import { Application } from "@hotwired/stimulus"
+
+const app = new Application()
+app.registerDefaultEventNames({ "custom-button": "click" })
+app.start()
+```
+
+The above allows you to omit the event name on custom buttons:
+
+
+
+```html
+…
+```
+
+Custom default event names must be registered before calling `start()`, or the application will not be able to add the correct event listeners.
## KeyboardEvent Filter
diff --git a/src/core/action.ts b/src/core/action.ts
index 3350ea84..7d478b3b 100644
--- a/src/core/action.ts
+++ b/src/core/action.ts
@@ -23,7 +23,7 @@ export class Action {
this.element = element
this.index = index
this.eventTarget = descriptor.eventTarget || element
- this.eventName = descriptor.eventName || getDefaultEventNameForElement(element) || error("missing event name")
+ this.eventName = descriptor.eventName || getDefaultEventNameForElement(element, schema) || error("missing event name")
this.eventOptions = descriptor.eventOptions || {}
this.identifier = descriptor.identifier || error("missing identifier")
this.methodName = descriptor.methodName || error("missing method name")
@@ -86,20 +86,13 @@ export class Action {
}
}
-const defaultEventNames: { [tagName: string]: (element: Element) => string } = {
- a: () => "click",
- button: () => "click",
- form: () => "submit",
- details: () => "toggle",
- input: (e) => (e.getAttribute("type") == "submit" ? "click" : "input"),
- select: () => "change",
- textarea: () => "input",
-}
-
-export function getDefaultEventNameForElement(element: Element): string | undefined {
- const tagName = element.tagName.toLowerCase()
- if (tagName in defaultEventNames) {
- return defaultEventNames[tagName](element)
+function getDefaultEventNameForElement(element: Element, schema: Schema): string | undefined {
+ let eventName = schema.defaultEventNames[element.localName]
+ if (typeof eventName === 'function') {
+ return eventName(element)
+ }
+ if (typeof eventName === 'string') {
+ return eventName
}
}
diff --git a/src/core/application.ts b/src/core/application.ts
index b8b92715..22f1858f 100644
--- a/src/core/application.ts
+++ b/src/core/application.ts
@@ -22,7 +22,7 @@ export class Application implements ErrorHandler {
return application
}
- constructor(element: Element = document.documentElement, schema: Schema = defaultSchema) {
+ constructor(element: Element = document.documentElement, schema: Schema = cloneDefaultSchema()) {
this.element = element
this.schema = schema
this.dispatcher = new Dispatcher(this)
@@ -53,6 +53,10 @@ export class Application implements ErrorHandler {
this.actionDescriptorFilters[name] = filter
}
+ registerDefaultEventNames(extensions: Schema['defaultEventNames']) {
+ Object.assign(this.schema.defaultEventNames, extensions)
+ }
+
load(...definitions: Definition[]): void
load(definitions: Definition[]): void
load(head: Definition | Definition[], ...rest: Definition[]) {
@@ -116,3 +120,11 @@ function domReady() {
}
})
}
+
+function cloneDefaultSchema(): Schema {
+ return {
+ ...defaultSchema,
+ keyMappings: { ...defaultSchema.keyMappings },
+ defaultEventNames: { ...defaultSchema.defaultEventNames }
+ }
+}
diff --git a/src/core/schema.ts b/src/core/schema.ts
index 20845d20..4a93daa6 100644
--- a/src/core/schema.ts
+++ b/src/core/schema.ts
@@ -5,6 +5,7 @@ export interface Schema {
targetAttributeForScope(identifier: string): string
outletAttributeForScope(identifier: string, outlet: string): string
keyMappings: { [key: string]: string }
+ defaultEventNames: { [tagName: string]: string | ((element: Element) => string) }
}
export const defaultSchema: Schema = {
@@ -29,6 +30,15 @@ export const defaultSchema: Schema = {
// [0-9]
...objectFromEntries("0123456789".split("").map((n) => [n, n])),
},
+ defaultEventNames: {
+ a: "click",
+ button: "click",
+ form: "submit",
+ details: "toggle",
+ input: (element) => (element.getAttribute("type") == "submit" ? "click" : "input"),
+ select: "change",
+ textarea: "input",
+ }
}
function objectFromEntries(array: [string, any][]): object {
diff --git a/src/tests/modules/core/action_custom_default_event_tests.ts b/src/tests/modules/core/action_custom_default_event_tests.ts
new file mode 100644
index 00000000..5017e5a4
--- /dev/null
+++ b/src/tests/modules/core/action_custom_default_event_tests.ts
@@ -0,0 +1,20 @@
+import { TestApplication } from "../../cases/application_test_case"
+import { LogControllerTestCase } from "../../cases/log_controller_test_case"
+import { Schema, defaultSchema } from "../../../core/schema"
+import { Application } from "../../../core/application"
+
+export default class ActionKeyboardFilterTests extends LogControllerTestCase {
+ schema: Schema = {
+ ...defaultSchema,
+ defaultEventNames: { ...defaultSchema.defaultEventNames, "some-element": () => "click" },
+ }
+ application: Application = new TestApplication(this.fixtureElement, this.schema)
+
+ identifier = "c"
+ fixtureHTML = ``
+
+ async "test default event"() {
+ await this.triggerEvent("some-element", "click")
+ this.assertActions({ name: "log", eventType: "click" })
+ }
+}