From 83e84b7e4cbbc60ae5cc55ae2ec26c04b03b9c32 Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Fri, 17 Feb 2023 12:04:07 +0100 Subject: [PATCH 1/7] Move defaultEventNames into Schema This allows applications to extend the mapping with support for custom elements. Closes #660 --- src/core/action.ts | 21 ++----------------- src/core/schema.ts | 17 +++++++++++++++ .../core/action_custom_default_event_tests.ts | 20 ++++++++++++++++++ 3 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 src/tests/modules/core/action_custom_default_event_tests.ts diff --git a/src/core/action.ts b/src/core/action.ts index 3350ea84..4774a782 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -1,6 +1,6 @@ import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } from "./action_descriptor" import { Token } from "../mutation-observers" -import { Schema } from "./schema" +import { getDefaultEventNameForElement, Schema } from "./schema" import { camelize } from "./string_helpers" import { hasProperty } from "./utils" @@ -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,23 +86,6 @@ 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 error(message: string): never { throw new Error(message) } diff --git a/src/core/schema.ts b/src/core/schema.ts index 20845d20..99a326ce 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]: (element: Element) => string } } export const defaultSchema: Schema = { @@ -29,6 +30,22 @@ export const defaultSchema: Schema = { // [0-9] ...objectFromEntries("0123456789".split("").map((n) => [n, n])), }, + defaultEventNames: { + 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, schema = defaultSchema): string | undefined { + const tagName = element.tagName.toLowerCase() + if (tagName in schema.defaultEventNames) { + return schema.defaultEventNames[tagName](element) + } } 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" }) + } +} From e26e13dc17a3c6a50a61ceac4a470e2763f24130 Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Sat, 18 Feb 2023 11:35:26 +0100 Subject: [PATCH 2/7] Use a more descriptive parameter name Co-authored-by: LB (Ben Johnston) --- src/core/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/schema.ts b/src/core/schema.ts index 99a326ce..09778552 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -35,7 +35,7 @@ export const defaultSchema: Schema = { button: () => "click", form: () => "submit", details: () => "toggle", - input: (e) => (e.getAttribute("type") == "submit" ? "click" : "input"), + input: (element) => (element.getAttribute("type") == "submit" ? "click" : "input"), select: () => "change", textarea: () => "input", } From 5d39ef266d15caa6d2a083dc7de48b45c946a80d Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Fri, 23 Jun 2023 00:53:59 +0200 Subject: [PATCH 3/7] Allow plain strings in Schema.defaultEventNames --- src/core/action.ts | 12 +++++++++++- src/core/schema.ts | 21 +++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/core/action.ts b/src/core/action.ts index 4774a782..7d478b3b 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -1,6 +1,6 @@ import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } from "./action_descriptor" import { Token } from "../mutation-observers" -import { getDefaultEventNameForElement, Schema } from "./schema" +import { Schema } from "./schema" import { camelize } from "./string_helpers" import { hasProperty } from "./utils" @@ -86,6 +86,16 @@ export class Action { } } +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 + } +} + function error(message: string): never { throw new Error(message) } diff --git a/src/core/schema.ts b/src/core/schema.ts index 09778552..4a93daa6 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -5,7 +5,7 @@ export interface Schema { targetAttributeForScope(identifier: string): string outletAttributeForScope(identifier: string, outlet: string): string keyMappings: { [key: string]: string } - defaultEventNames: { [tagName: string]: (element: Element) => string } + defaultEventNames: { [tagName: string]: string | ((element: Element) => string) } } export const defaultSchema: Schema = { @@ -31,20 +31,13 @@ export const defaultSchema: Schema = { ...objectFromEntries("0123456789".split("").map((n) => [n, n])), }, defaultEventNames: { - a: () => "click", - button: () => "click", - form: () => "submit", - details: () => "toggle", + a: "click", + button: "click", + form: "submit", + details: "toggle", input: (element) => (element.getAttribute("type") == "submit" ? "click" : "input"), - select: () => "change", - textarea: () => "input", - } -} - -export function getDefaultEventNameForElement(element: Element, schema = defaultSchema): string | undefined { - const tagName = element.tagName.toLowerCase() - if (tagName in schema.defaultEventNames) { - return schema.defaultEventNames[tagName](element) + select: "change", + textarea: "input", } } From 7fd847d1058577f8964dbbce3ab89ab18c5b7612 Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Fri, 23 Jun 2023 01:51:13 +0200 Subject: [PATCH 4/7] Clone default schema in Application constructor --- src/core/application.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/core/application.ts b/src/core/application.ts index b8b92715..054b8b6d 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) @@ -116,3 +116,11 @@ function domReady() { } }) } + +function cloneDefaultSchema(): Schema { + return { + ...defaultSchema, + keyMappings: { ...defaultSchema.keyMappings }, + defaultEventNames: { ...defaultSchema.defaultEventNames } + } +} From 4ecb43d86f3abdfc3f38c30227d1b02964ca2e36 Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Fri, 23 Jun 2023 01:51:13 +0200 Subject: [PATCH 5/7] Add Application.registerDefaultEventNames and docs --- docs/reference/actions.md | 23 ++++++++++++++++++++++- src/core/application.ts | 4 ++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/reference/actions.md b/docs/reference/actions.md index aebae6ab..4a0c4335 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.registerDefaultEventName` 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. + +```html ## KeyboardEvent Filter diff --git a/src/core/application.ts b/src/core/application.ts index 054b8b6d..22f1858f 100644 --- a/src/core/application.ts +++ b/src/core/application.ts @@ -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[]) { From 80b66655bb84d5e26a4127b220668736b2cab39b Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Fri, 23 Jun 2023 14:00:15 +0200 Subject: [PATCH 6/7] Fix method name in docs --- docs/reference/actions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/actions.md b/docs/reference/actions.md index 4a0c4335..c2d063c8 100644 --- a/docs/reference/actions.md +++ b/docs/reference/actions.md @@ -65,7 +65,7 @@ input type=submit | click select | change textarea | input -This built-in set can be extended with the `Application.registerDefaultEventName` method, for example to associate default event names with custom elements. +This built-in set can be extended with the `Application.registerDefaultEventNames` method, for example to associate default event names with custom elements. From 7c464d983b9fb18e130ae2c98d5e1a3dcaed5a05 Mon Sep 17 00:00:00 2001 From: Stephen Sugden Date: Fri, 23 Jun 2023 14:13:34 +0200 Subject: [PATCH 7/7] Fix usage of callout in docs, remove a stray code block --- docs/reference/actions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/actions.md b/docs/reference/actions.md index c2d063c8..5d75894d 100644 --- a/docs/reference/actions.md +++ b/docs/reference/actions.md @@ -67,7 +67,7 @@ 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" @@ -79,14 +79,14 @@ 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. -```html - ## KeyboardEvent Filter There may be cases where [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) Actions should only call the Controller method when certain keystrokes are used.