diff --git a/assets/js/phoenix_live_view/dom.js b/assets/js/phoenix_live_view/dom.js index d5bf79e390..814724c578 100644 --- a/assets/js/phoenix_live_view/dom.js +++ b/assets/js/phoenix_live_view/dom.js @@ -221,7 +221,23 @@ let DOM = { default: let timeout = parseInt(value) - let trigger = () => throttle ? this.deletePrivate(el, THROTTLED) : callback() + let trigger = (blur) => { + if(blur){ + // if the input is blurred, we need to cancel the next throttle timeout + // therefore we store the timer id in the THROTTLED private attribute + if(throttle && this.private(el, THROTTLED)){ + clearTimeout(this.private(el, THROTTLED)) + this.deletePrivate(el, THROTTLED) + } + // on debounce we just trigger the callback + return callback() + } + // no blur, remove the throttle attribute if we are in throttle mode + if(throttle) this.deletePrivate(el, THROTTLED) + // always call the callback to ensure that the latest event is processed, + // even when throttle is active + callback() + } let currentCycle = this.incCycle(el, DEBOUNCE_TRIGGER, trigger) if(isNaN(timeout)){ return logError(`invalid throttle/debounce value: ${value}`) } if(throttle){ @@ -236,10 +252,14 @@ let DOM = { return false } else { callback() - this.putPrivate(el, THROTTLED, true) - setTimeout(() => { + // store the throttle timer id in the THROTTLED private attribute, + // so that we can cancel it if the input is blurred + // otherwise, when new events happen after blur, but before the old + // timeout is triggered, we would actually trigger the callback multiple times + const t = setTimeout(() => { if(asyncFilter()){ this.triggerCycle(el, DEBOUNCE_TRIGGER) } }, timeout) + this.putPrivate(el, THROTTLED, t) } } else { setTimeout(() => { @@ -258,20 +278,17 @@ let DOM = { }) } if(this.once(el, "bind-debounce")){ - el.addEventListener("blur", () => { - // always trigger callback on blur - callback() - }) + el.addEventListener("blur", () => this.triggerCycle(el, DEBOUNCE_TRIGGER, null, [true])) } } }, - triggerCycle(el, key, currentCycle){ + triggerCycle(el, key, currentCycle, params=[]){ let [cycle, trigger] = this.private(el, key) if(!currentCycle){ currentCycle = cycle } if(currentCycle === cycle){ this.incCycle(el, key) - trigger() + trigger(...params) } }, diff --git a/assets/test/debounce_test.js b/assets/test/debounce_test.js index 9b5f96170a..d2758c251c 100644 --- a/assets/test/debounce_test.js +++ b/assets/test/debounce_test.js @@ -169,16 +169,21 @@ describe("throttle", function (){ DOM.dispatchEvent(el, "click") expect(calls).toBe(1) expect(el.innerText).toBe("now:1") - after(250, () => { + after(100, () => { expect(calls).toBe(1) - expect(el.innerText).toBe("now:1") - DOM.dispatchEvent(el, "click") - DOM.dispatchEvent(el, "click") - DOM.dispatchEvent(el, "click") - after(250, () => { + // now wait another 150ms (after 100ms, so 200 total we expect 2 events) + after(150, () => { expect(calls).toBe(2) expect(el.innerText).toBe("now:2") - done() + DOM.dispatchEvent(el, "click") + DOM.dispatchEvent(el, "click") + DOM.dispatchEvent(el, "click") + // the first and last event are processed + after(250, () => { + expect(calls).toBe(4) + expect(el.innerText).toBe("now:4") + done() + }) }) }) }) @@ -255,13 +260,16 @@ describe("throttle keydown", function (){ el.dispatchEvent(pressA) expect(keyPresses["a"]).toBe(1) - after(250, () => { + after(100, () => { expect(keyPresses["a"]).toBe(1) - el.dispatchEvent(pressA) - el.dispatchEvent(pressA) - el.dispatchEvent(pressA) - expect(keyPresses["a"]).toBe(2) - done() + after(150, () => { + expect(keyPresses["a"]).toBe(2) + el.dispatchEvent(pressA) + el.dispatchEvent(pressA) + el.dispatchEvent(pressA) + expect(keyPresses["a"]).toBe(3) + done() + }) }) })