Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds new Action Options, namely :stop and :prevent #535

Merged
merged 11 commits into from Apr 30, 2022
7 changes: 7 additions & 0 deletions docs/reference/actions.md
Expand Up @@ -101,6 +101,13 @@ Action option | DOM event listener option
`:passive` | `{ passive: true }`
`:!passive` | `{ passive: false }`

On top of that, Stimulus also supports the following action options which are not natively supported by the DOM event listener options:

Custom action option | Description
-------------------- | -----------
`:stop` | calls `.stopPropagation()` on the event before invoking the method
`:prevent` | calls `.preventDefault()` on the event before invoking the method

## Event Objects

An _action method_ is the method in a controller which serves as an action's event listener.
Expand Down
3 changes: 2 additions & 1 deletion src/core/action.ts
@@ -1,13 +1,14 @@
import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } from "./action_descriptor"
import { Token } from "../mutation-observers"
import { camelize } from "./string_helpers"
import { EventModifiers } from "./event_modifiers"

export class Action {
readonly element: Element
readonly index: number
readonly eventTarget: EventTarget
readonly eventName: string
readonly eventOptions: AddEventListenerOptions
readonly eventOptions: EventModifiers
readonly identifier: string
readonly methodName: string

Expand Down
6 changes: 4 additions & 2 deletions src/core/action_descriptor.ts
@@ -1,6 +1,8 @@
import { EventModifiers } from "./event_modifiers"

export interface ActionDescriptor {
eventTarget: EventTarget
eventOptions: AddEventListenerOptions
eventOptions: EventModifiers
eventName: string
identifier: string
methodName: string
Expand Down Expand Up @@ -29,7 +31,7 @@ function parseEventTarget(eventTargetName: string): EventTarget | undefined {
}
}

function parseEventOptions(eventOptions: string): AddEventListenerOptions {
function parseEventOptions(eventOptions: string): EventModifiers {
return eventOptions.split(":").reduce((options, token) =>
Object.assign(options, { [token.replace(/^!/, "")]: !/^!/.test(token) })
, {})
Expand Down
18 changes: 17 additions & 1 deletion src/core/binding.ts
Expand Up @@ -3,6 +3,7 @@ import { ActionEvent } from "./action_event"
import { Context } from "./context"
import { Controller } from "./controller"
import { Scope } from "./scope"
import { EventModifiers } from "./event_modifiers"

export class Binding {
readonly context: Context
Expand All @@ -21,7 +22,7 @@ export class Binding {
return this.action.eventTarget
}

get eventOptions(): AddEventListenerOptions {
get eventOptions(): EventModifiers {
return this.action.eventOptions
}

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

handleEvent(event: Event) {
if (this.willBeInvokedByEvent(event)) {
this.processStopPropagation(event);
this.processPreventDefault(event);

this.invokeWithEvent(event)
}
}
Expand All @@ -47,6 +51,18 @@ export class Binding {
throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`)
}

private processStopPropagation(event: Event) {
if (this.eventOptions.stop) {
event.stopPropagation();
}
}

private processPreventDefault(event: Event) {
if (this.eventOptions.prevent) {
event.preventDefault();
}
}

private invokeWithEvent(event: Event) {
const { target, currentTarget } = event
try {
Expand Down
4 changes: 4 additions & 0 deletions src/core/event_modifiers.ts
@@ -0,0 +1,4 @@
export interface EventModifiers extends AddEventListenerOptions {
stop?: boolean;
prevent?: boolean;
}
63 changes: 63 additions & 0 deletions src/tests/modules/core/event_options_tests.ts
Expand Up @@ -131,10 +131,73 @@ export default class EventOptionsTests extends LogControllerTestCase {
)
}

async "test stop option with implicit event"() {
this.elementActionValue = "click->c#log"
this.actionValue = "c#log2:stop"
await this.nextFrame

await this.triggerEvent(this.buttonElement, "click")

this.assertActions(
{ name: "log2", eventType: "click" }
)
}

async "test stop option with explicit event"() {
this.elementActionValue = "keydown->c#log"
this.actionValue = "keydown->c#log2:stop"
await this.nextFrame

await this.triggerEvent(this.buttonElement, "keydown")

this.assertActions(
{ name: "log2", eventType: "keydown" }
)
}

async "test event propagation without stop option"() {
this.elementActionValue = "click->c#log"
this.actionValue = "c#log2"
await this.nextFrame

await this.triggerEvent(this.buttonElement, "click")

this.assertActions(
{ name: "log2", eventType: "click" },
{ name: "log", eventType: "click" }
)
}

async "test prevent option with implicit event"() {
this.actionValue = "c#log:prevent"
await this.nextFrame

await this.triggerEvent(this.buttonElement, "click")

this.assertActions(
{ name: "log", eventType: "click", defaultPrevented: true }
)
}

async "test prevent option with explicit event"() {
this.actionValue = "keyup->c#log:prevent"
await this.nextFrame

await this.triggerEvent(this.buttonElement, "keyup")

this.assertActions(
{ name: "log", eventType: "keyup", defaultPrevented: true }
)
}

set actionValue(value: string) {
this.buttonElement.setAttribute("data-action", value)
}

set elementActionValue(value: string) {
this.element.setAttribute("data-action", value)
}
radiantshaw marked this conversation as resolved.
Show resolved Hide resolved

get element() {
return this.findElement("div")
}
Expand Down