Skip to content

Commit

Permalink
throttle/debounce user callback after preventDefault/stopPropagation …
Browse files Browse the repository at this point in the history
…is executed (#3481)

* throttle/debounce user callback after preventDefault/stopPropagation is executed

* spelling

* add tests

* cleaner test

* nit
  • Loading branch information
hudon committed May 11, 2023
1 parent a0a6b74 commit c6cee92
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 14 deletions.
31 changes: 17 additions & 14 deletions packages/alpinejs/src/utils/on.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ export default function on (el, event, modifiers, callback) {
if (modifiers.includes('capture')) options.capture = true
if (modifiers.includes('window')) listenerTarget = window
if (modifiers.includes('document')) listenerTarget = document

// By wrapping the handler with debounce & throttle first, we ensure that the wrapping logic itself is not
// throttled/debounced, only the user's callback is. This way, if the user expects
// `e.preventDefault()` to happen, it'll still happen even if their callback gets throttled.
if (modifiers.includes('debounce')) {
let nextModifier = modifiers[modifiers.indexOf('debounce')+1] || 'invalid-wait'
let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250

handler = debounce(handler, wait)
}
if (modifiers.includes('throttle')) {
let nextModifier = modifiers[modifiers.indexOf('throttle')+1] || 'invalid-wait'
let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250

handler = throttle(handler, wait)
}

if (modifiers.includes('prevent')) handler = wrapHandler(handler, (next, e) => { e.preventDefault(); next(e) })
if (modifiers.includes('stop')) handler = wrapHandler(handler, (next, e) => { e.stopPropagation(); next(e) })
if (modifiers.includes('self')) handler = wrapHandler(handler, (next, e) => { e.target === el && next(e) })
Expand Down Expand Up @@ -59,20 +76,6 @@ export default function on (el, event, modifiers, callback) {
next(e)
})

if (modifiers.includes('debounce')) {
let nextModifier = modifiers[modifiers.indexOf('debounce')+1] || 'invalid-wait'
let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250

handler = debounce(handler, wait)
}

if (modifiers.includes('throttle')) {
let nextModifier = modifiers[modifiers.indexOf('throttle')+1] || 'invalid-wait'
let wait = isNumeric(nextModifier.split('ms')[0]) ? Number(nextModifier.split('ms')[0]) : 250

handler = throttle(handler, wait)
}

listenerTarget.addEventListener(event, handler, options)

return () => {
Expand Down
33 changes: 33 additions & 0 deletions tests/cypress/integration/directives/x-on.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ test('.stop modifier',
}
)


test('.stop modifier with a .throttle',
html`
<div x-data="{ foo: 'bar' }">
<button x-on:click="foo = 'baz'">
<h1>h1</h1>
<h2 @click.stop.throttle>h2</h2>
</button>
</div>
`,
({ get }) => {
get('div').should(haveData('foo', 'bar'))
get('h2').click()
get('h2').click()
get('div').should(haveData('foo', 'bar'))
get('h1').click()
get('div').should(haveData('foo', 'baz'))
}
)

test('.capture modifier',
html`
<div x-data="{ foo: 'bar', count: 0 }">
Expand Down Expand Up @@ -178,6 +198,19 @@ test('.prevent modifier',
}
)

test('.prevent modifier with a .debounce',
html`
<div x-data="{}">
<input type="checkbox" x-on:click.prevent.debounce>
</div>
`,
({ get }) => {
get('input').check()
get('input').check()
get('input').should(notBeChecked())
}
)

test('.window modifier',
html`
<div x-data="{ foo: 'bar' }">
Expand Down

0 comments on commit c6cee92

Please sign in to comment.