diff --git a/docs/reference/actions.md b/docs/reference/actions.md index 6aa43db3..211b367c 100644 --- a/docs/reference/actions.md +++ b/docs/reference/actions.md @@ -280,6 +280,17 @@ 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. 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") { + // ... + } +} +``` + ## 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..62b8d19b 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -41,10 +41,16 @@ 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}"`) + + const action = this.action + + return function (this: Controller, event: ActionEvent) { + this.actionHandlerMissing(action, event) + } } private applyEventModifiers(event: Event): boolean { diff --git a/src/core/controller.ts b/src/core/controller.ts index 4b412716..cde98f6c 100644 --- a/src/core/controller.ts +++ b/src/core/controller.ts @@ -1,3 +1,5 @@ +import type { Action } from "./action" +import type { 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..e59164b3 100644 --- a/src/tests/controllers/log_controller.ts +++ b/src/tests/controllers/log_controller.ts @@ -1,3 +1,4 @@ +import type { 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" }) + } }