Skip to content

Commit

Permalink
Adds new Action Options, namely :stop and :prevent (#535)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
radiantshaw committed Apr 30, 2022
1 parent c502444 commit 531ec30
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 4 deletions.
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)
}

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

0 comments on commit 531ec30

Please sign in to comment.