Skip to content

Commit

Permalink
Ensure that events are always sent on blur
Browse files Browse the repository at this point in the history
Relates to: phoenixframework#3033
Relates to: phoenixframework#2318
Fixes: phoenixframework#3076

The old "fix" from phoenixframework#3033 had a fatal flaw because it would call an old
callback on blur.

This new commit adjusts the behavior to properly use the correct callback
by hooking into the cycle logic and ignoring the throttle in case of blur,
leaving the rest of the logic nearly untouched. This requires us to clear
the throttle timeout though, because otherwise we would call the callback
again after a blur, or multiple times when new events happen after blur
but before the next throttle timeout triggers.

The debounce/throttle code is not that easy to understand, but this time
I'm more confident that the solution is actually correct.
(famous last words)
  • Loading branch information
SteffenDE committed Feb 7, 2024
1 parent 4b8d63a commit f76cc9d
Showing 1 changed file with 23 additions and 9 deletions.
32 changes: 23 additions & 9 deletions assets/js/phoenix_live_view/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,20 @@ 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
throttle ? this.deletePrivate(el, THROTTLED) : callback()
}
let currentCycle = this.incCycle(el, DEBOUNCE_TRIGGER, trigger)
if(isNaN(timeout)){ return logError(`invalid throttle/debounce value: ${value}`) }
if(throttle){
Expand All @@ -236,10 +249,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(() => {
Expand All @@ -258,20 +275,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)
}
},

Expand Down

0 comments on commit f76cc9d

Please sign in to comment.