From c932a497028ff62ab1c61caf26b3ead3715955b6 Mon Sep 17 00:00:00 2001 From: Cameron Dutro Date: Thu, 9 Jul 2026 18:18:17 -0700 Subject: [PATCH 1/3] Introduce eventHandlerMissing as a fallback when event handler methods aren't defined --- docs/reference/actions.md | 12 ++++++++++++ src/core/binding.ts | 6 +++++- src/core/controller.ts | 6 ++++++ src/core/index.ts | 1 + src/tests/controllers/log_controller.ts | 5 +++++ src/tests/modules/core/action_tests.ts | 7 ++++++- 6 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/reference/actions.md b/docs/reference/actions.md index 6aa43db3..8e519175 100644 --- a/docs/reference/actions.md +++ b/docs/reference/actions.md @@ -280,6 +280,18 @@ highlight(event) { } ``` +## Missing Action Handlers + +If the method specified by an action descriptor doesn't exist on the controller, Stimulus will call the special `actionHandlerMissing` method. You can override this method in your controller to dynamically handle events. The method receives the action and the event as arguments: + +```javascript +actionHandlerMissing(action, event) { + if (action.methodName === "next") { + // ... + } +} +``` + ## Naming Conventions Always use camelCase to specify action names, since they map directly to methods on your controller. diff --git a/src/core/binding.ts b/src/core/binding.ts index 33958667..71253e7f 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -41,10 +41,14 @@ export class Binding { get method(): Function { const method = (this.controller as any)[this.methodName] + if (typeof method == "function") { return method } - throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`) + + return (event: ActionEvent) => { + this.controller.actionHandlerMissing(this.action, event) + } } private applyEventModifiers(event: Event): boolean { diff --git a/src/core/controller.ts b/src/core/controller.ts index 4b412716..d94ed95d 100644 --- a/src/core/controller.ts +++ b/src/core/controller.ts @@ -1,3 +1,5 @@ +import { Action } from "./action" +import { ActionEvent } from "./action_event" import { Application } from "./application" import { ClassPropertiesBlessing } from "./class_properties" import { Constructor } from "./constructor" @@ -100,4 +102,8 @@ export class Controller { target.dispatchEvent(event) return event } + + actionHandlerMissing(action: Action, _event: ActionEvent) { + throw new Error(`Action "${action}" references undefined method "${action.methodName}"`) + } } diff --git a/src/core/index.ts b/src/core/index.ts index 7fa3ec6b..881bfcae 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,3 +1,4 @@ +export { Action } from "./action" export { ActionEvent } from "./action_event" export { Application } from "./application" export { Context } from "./context" diff --git a/src/tests/controllers/log_controller.ts b/src/tests/controllers/log_controller.ts index 2468b22c..05d81b1e 100644 --- a/src/tests/controllers/log_controller.ts +++ b/src/tests/controllers/log_controller.ts @@ -1,3 +1,4 @@ +import { Action } from "../../core/action" import { ActionEvent } from "../../core/action_event" import { Controller } from "../../core/controller" @@ -56,6 +57,10 @@ export class LogController extends Controller { event.stopImmediatePropagation() } + actionHandlerMissing(action: Action, event: ActionEvent) { + this.recordAction(`${action.methodName}_missing`, event) + } + get actionLog() { return (this.constructor as typeof LogController).actionLog } diff --git a/src/tests/modules/core/action_tests.ts b/src/tests/modules/core/action_tests.ts index 4e3c7718..cbfcca9a 100644 --- a/src/tests/modules/core/action_tests.ts +++ b/src/tests/modules/core/action_tests.ts @@ -5,7 +5,7 @@ export default class ActionTests extends LogControllerTestCase { fixtureHTML = `
-
+
@@ -66,4 +66,9 @@ export default class ActionTests extends LogControllerTestCase { await this.triggerEvent("#svgChild", "mousedown") this.assertActions({ name: "log", eventType: "click" }, { name: "log", eventType: "mousedown" }) } + + async "test missing actions routed through actionHandlerMissing"() { + await this.triggerEvent("#outer", "mousedown") + this.assertActions({ name: "iAmMissing_missing", eventType: "mousedown" }) + } } From 52545442e0f55e74f5a979cf825319d75fa7c8a5 Mon Sep 17 00:00:00 2001 From: Cameron Dutro Date: Thu, 9 Jul 2026 20:32:38 -0700 Subject: [PATCH 2/3] Document the fact that `actionHandlerMissing` throws an error by default Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/reference/actions.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/reference/actions.md b/docs/reference/actions.md index 8e519175..211b367c 100644 --- a/docs/reference/actions.md +++ b/docs/reference/actions.md @@ -282,8 +282,7 @@ highlight(event) { ## Missing Action Handlers -If the method specified by an action descriptor doesn't exist on the controller, Stimulus will call the special `actionHandlerMissing` method. You can override this method in your controller to dynamically handle events. The method receives the action and the event as arguments: - +If the method specified by an action descriptor doesn't exist on the controller, Stimulus will call the special `actionHandlerMissing` method. By default, `actionHandlerMissing` throws an error for the missing method, but you can override it in your controller to dynamically handle events. The method receives the action and the event as arguments: ```javascript actionHandlerMissing(action, event) { if (action.methodName === "next") { From b0fe5af2b4fb3ba140ad23ae9f0e2428c5f57a86 Mon Sep 17 00:00:00 2001 From: Cameron Dutro Date: Thu, 9 Jul 2026 22:34:34 -0700 Subject: [PATCH 3/3] Return regular curried function to preserve this passed by .call(); import types --- src/core/binding.ts | 6 ++++-- src/core/controller.ts | 4 ++-- src/tests/controllers/log_controller.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/binding.ts b/src/core/binding.ts index 71253e7f..62b8d19b 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -46,8 +46,10 @@ export class Binding { return method } - return (event: ActionEvent) => { - this.controller.actionHandlerMissing(this.action, event) + const action = this.action + + return function (this: Controller, event: ActionEvent) { + this.actionHandlerMissing(action, event) } } diff --git a/src/core/controller.ts b/src/core/controller.ts index d94ed95d..cde98f6c 100644 --- a/src/core/controller.ts +++ b/src/core/controller.ts @@ -1,5 +1,5 @@ -import { Action } from "./action" -import { ActionEvent } from "./action_event" +import type { Action } from "./action" +import type { ActionEvent } from "./action_event" import { Application } from "./application" import { ClassPropertiesBlessing } from "./class_properties" import { Constructor } from "./constructor" diff --git a/src/tests/controllers/log_controller.ts b/src/tests/controllers/log_controller.ts index 05d81b1e..e59164b3 100644 --- a/src/tests/controllers/log_controller.ts +++ b/src/tests/controllers/log_controller.ts @@ -1,4 +1,4 @@ -import { Action } from "../../core/action" +import type { Action } from "../../core/action" import { ActionEvent } from "../../core/action_event" import { Controller } from "../../core/controller"