Skip to content

Commit 531ec30

Browse files
author
Nipun Paradkar
authored
Adds new Action Options, namely :stop and :prevent (#535)
* Add an event action option for stopping propagation * Add test case for stopping propagation ... by specifying the event explicitly. * Add test case for ensuring event propagation without stop option * Improve test case descriptions ... by using the words "implicit" and "explicit" instead of "default" and "specified". * Replace `logPropagationContinued` with `log` and `log2` * Add an event action option for preventing default * Add test case for using `:prevent` with explicit event name * Update `README` about the new action options * Rename `ExtendedAddEventListenerOptions` to `EventModifiers` * Use delegated getter method for `eventOptions` ... inside `Binding`. * Process `.preventDefault` and `.stopPropagation` in separate functions
1 parent c502444 commit 531ec30

File tree

6 files changed

+97
-4
lines changed

6 files changed

+97
-4
lines changed

docs/reference/actions.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ Action option | DOM event listener option
101101
`:passive` | `{ passive: true }`
102102
`:!passive` | `{ passive: false }`
103103

104+
On top of that, Stimulus also supports the following action options which are not natively supported by the DOM event listener options:
105+
106+
Custom action option | Description
107+
-------------------- | -----------
108+
`:stop` | calls `.stopPropagation()` on the event before invoking the method
109+
`:prevent` | calls `.preventDefault()` on the event before invoking the method
110+
104111
## Event Objects
105112

106113
An _action method_ is the method in a controller which serves as an action's event listener.

src/core/action.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } from "./action_descriptor"
22
import { Token } from "../mutation-observers"
33
import { camelize } from "./string_helpers"
4+
import { EventModifiers } from "./event_modifiers"
45

56
export class Action {
67
readonly element: Element
78
readonly index: number
89
readonly eventTarget: EventTarget
910
readonly eventName: string
10-
readonly eventOptions: AddEventListenerOptions
11+
readonly eventOptions: EventModifiers
1112
readonly identifier: string
1213
readonly methodName: string
1314

src/core/action_descriptor.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { EventModifiers } from "./event_modifiers"
2+
13
export interface ActionDescriptor {
24
eventTarget: EventTarget
3-
eventOptions: AddEventListenerOptions
5+
eventOptions: EventModifiers
46
eventName: string
57
identifier: string
68
methodName: string
@@ -29,7 +31,7 @@ function parseEventTarget(eventTargetName: string): EventTarget | undefined {
2931
}
3032
}
3133

32-
function parseEventOptions(eventOptions: string): AddEventListenerOptions {
34+
function parseEventOptions(eventOptions: string): EventModifiers {
3335
return eventOptions.split(":").reduce((options, token) =>
3436
Object.assign(options, { [token.replace(/^!/, "")]: !/^!/.test(token) })
3537
, {})

src/core/binding.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ActionEvent } from "./action_event"
33
import { Context } from "./context"
44
import { Controller } from "./controller"
55
import { Scope } from "./scope"
6+
import { EventModifiers } from "./event_modifiers"
67

78
export class Binding {
89
readonly context: Context
@@ -21,7 +22,7 @@ export class Binding {
2122
return this.action.eventTarget
2223
}
2324

24-
get eventOptions(): AddEventListenerOptions {
25+
get eventOptions(): EventModifiers {
2526
return this.action.eventOptions
2627
}
2728

@@ -31,6 +32,9 @@ export class Binding {
3132

3233
handleEvent(event: Event) {
3334
if (this.willBeInvokedByEvent(event)) {
35+
this.processStopPropagation(event);
36+
this.processPreventDefault(event);
37+
3438
this.invokeWithEvent(event)
3539
}
3640
}
@@ -47,6 +51,18 @@ export class Binding {
4751
throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`)
4852
}
4953

54+
private processStopPropagation(event: Event) {
55+
if (this.eventOptions.stop) {
56+
event.stopPropagation();
57+
}
58+
}
59+
60+
private processPreventDefault(event: Event) {
61+
if (this.eventOptions.prevent) {
62+
event.preventDefault();
63+
}
64+
}
65+
5066
private invokeWithEvent(event: Event) {
5167
const { target, currentTarget } = event
5268
try {

src/core/event_modifiers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface EventModifiers extends AddEventListenerOptions {
2+
stop?: boolean;
3+
prevent?: boolean;
4+
}

src/tests/modules/core/event_options_tests.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,73 @@ export default class EventOptionsTests extends LogControllerTestCase {
131131
)
132132
}
133133

134+
async "test stop option with implicit event"() {
135+
this.elementActionValue = "click->c#log"
136+
this.actionValue = "c#log2:stop"
137+
await this.nextFrame
138+
139+
await this.triggerEvent(this.buttonElement, "click")
140+
141+
this.assertActions(
142+
{ name: "log2", eventType: "click" }
143+
)
144+
}
145+
146+
async "test stop option with explicit event"() {
147+
this.elementActionValue = "keydown->c#log"
148+
this.actionValue = "keydown->c#log2:stop"
149+
await this.nextFrame
150+
151+
await this.triggerEvent(this.buttonElement, "keydown")
152+
153+
this.assertActions(
154+
{ name: "log2", eventType: "keydown" }
155+
)
156+
}
157+
158+
async "test event propagation without stop option"() {
159+
this.elementActionValue = "click->c#log"
160+
this.actionValue = "c#log2"
161+
await this.nextFrame
162+
163+
await this.triggerEvent(this.buttonElement, "click")
164+
165+
this.assertActions(
166+
{ name: "log2", eventType: "click" },
167+
{ name: "log", eventType: "click" }
168+
)
169+
}
170+
171+
async "test prevent option with implicit event"() {
172+
this.actionValue = "c#log:prevent"
173+
await this.nextFrame
174+
175+
await this.triggerEvent(this.buttonElement, "click")
176+
177+
this.assertActions(
178+
{ name: "log", eventType: "click", defaultPrevented: true }
179+
)
180+
}
181+
182+
async "test prevent option with explicit event"() {
183+
this.actionValue = "keyup->c#log:prevent"
184+
await this.nextFrame
185+
186+
await this.triggerEvent(this.buttonElement, "keyup")
187+
188+
this.assertActions(
189+
{ name: "log", eventType: "keyup", defaultPrevented: true }
190+
)
191+
}
192+
134193
set actionValue(value: string) {
135194
this.buttonElement.setAttribute("data-action", value)
136195
}
137196

197+
set elementActionValue(value: string) {
198+
this.element.setAttribute("data-action", value)
199+
}
200+
138201
get element() {
139202
return this.findElement("div")
140203
}

0 commit comments

Comments
 (0)