fix: avoid loading indicator issues when using browser history for navigation

This commit is contained in:
Björn Fromme
2025-12-09 15:13:04 +01:00
parent 420efb25ad
commit 32c73ab7b1
9 changed files with 87 additions and 106 deletions
+2
View File
@@ -1,4 +1,6 @@
import './bootstrap.js'
import './loading.js'
import './styles/app.css'
import htmx from 'htmx.org'
-99
View File
@@ -1,99 +0,0 @@
import {Controller} from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['indicator']
static classes = ['hidden']
static values = {
visible: {
type: Boolean,
default: false
}
}
connect() {
this.debounceTimeout = null
// Bind event handlers to preserve context
this.boundHandleBeforeRequest = this.handleBeforeRequest.bind(this)
this.boundHandleAfterRequest = this.handleAfterRequest.bind(this)
this.boundHandleTimeout = this.handleTimeout.bind(this)
this.boundHandlePageShow = this.handlePageShow.bind(this)
this.boundHandleHistoryRestore = this.handleHistoryRestore.bind(this)
// Listen to HTMX events
document.body.addEventListener('htmx:beforeRequest', this.boundHandleBeforeRequest)
document.body.addEventListener('htmx:afterRequest', this.boundHandleAfterRequest)
document.body.addEventListener('htmx:timeout', this.boundHandleTimeout)
document.body.addEventListener('htmx:historyRestore', this.boundHandleHistoryRestore)
// Listen for browser back/forward navigation
window.addEventListener('pageshow', this.boundHandlePageShow)
}
disconnect() {
// Clean up event listeners
document.body.removeEventListener('htmx:beforeRequest', this.boundHandleBeforeRequest)
document.body.removeEventListener('htmx:afterRequest', this.boundHandleAfterRequest)
document.body.removeEventListener('htmx:timeout', this.boundHandleTimeout)
document.body.removeEventListener('htmx:historyRestore', this.boundHandleHistoryRestore)
window.removeEventListener('pageshow', this.boundHandlePageShow)
// Clear any pending timeout
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
}
}
handleBeforeRequest() {
// Don't start a new debounce if already visible
if (true === this.visibleValue) {
return
}
// Show indicator after 200ms delay for field refreshes
this.debounceTimeout = setTimeout(() => {
this.show()
}, 200)
}
handleAfterRequest() {
// Clear debounce timer if request completes before 200ms
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
this.debounceTimeout = null
}
this.hide()
}
handleTimeout() {
// Hide loading indicator
this.hide()
// Show user-friendly error message
alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.')
}
handlePageShow(event) {
// event.persisted is true when page is restored from bfcache (back/forward navigation)
if (true === event.persisted) {
this.hide()
}
}
handleHistoryRestore() {
// HTMX history restore - hide loading indicator
this.hide()
}
show() {
this.visibleValue = true
this.indicatorTarget.classList.remove(this.hiddenClass)
}
hide() {
this.visibleValue = false
this.indicatorTarget.classList.add(this.hiddenClass)
}
}
+78
View File
@@ -0,0 +1,78 @@
const INDICATOR_ID = 'loading-indicator'
const HIDDEN_CLASS = 'invisible'
const GRACE_PERIOD_MS = 500
const DEBOUNCE_MS = 200
let debounceTimeout = null
let historyRestoreGracePeriod = false
function getIndicator() {
return document.getElementById(INDICATOR_ID)
}
function show() {
const indicator = getIndicator()
if (indicator) {
indicator.classList.remove(HIDDEN_CLASS)
}
}
function hide() {
if (debounceTimeout) {
clearTimeout(debounceTimeout)
debounceTimeout = null
}
const indicator = getIndicator()
if (indicator) {
indicator.classList.add(HIDDEN_CLASS)
}
}
function handleBeforeRequest() {
// Ignore requests triggered during history restore grace period
if (true === historyRestoreGracePeriod) {
return
}
// Don't start a new debounce if already pending
if (debounceTimeout) {
return
}
// Show indicator after delay
debounceTimeout = setTimeout(() => {
show()
}, DEBOUNCE_MS)
}
function handleHistoryRestore() {
hide()
// Set grace period to ignore change events triggered by history restore
historyRestoreGracePeriod = true
setTimeout(() => {
historyRestoreGracePeriod = false
}, GRACE_PERIOD_MS)
}
function handleTimeout() {
hide()
alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.')
}
function handleBeforeHistorySave() {
// Hide indicator before HTMX saves the page to history cache
hide()
}
// Initialize event listeners
document.body.addEventListener('htmx:beforeRequest', handleBeforeRequest)
document.body.addEventListener('htmx:afterRequest', hide)
document.body.addEventListener('htmx:timeout', handleTimeout)
document.body.addEventListener('htmx:historyRestore', handleHistoryRestore)
document.body.addEventListener('htmx:sendError', hide)
document.body.addEventListener('htmx:responseError', hide)
document.body.addEventListener('htmx:beforeHistorySave', handleBeforeHistorySave)
window.addEventListener('pageshow', hide)
window.addEventListener('popstate', handleHistoryRestore)