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" }) + } +}