Explorar o código

throttle/debounce user callback after preventDefault/stopPropagation is executed (#3481)

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

* spelling

* add tests

* cleaner test

* nit
James Hudon %!s(int64=2) %!d(string=hai) anos
pai
achega
c6cee92474

+ 17 - 14
packages/alpinejs/src/utils/on.js

@@ -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) })
@@ -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 () => {

+ 33 - 0
tests/cypress/integration/directives/x-on.spec.js

@@ -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 }">
@@ -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' }">