feat: htmx powered requests, improved loading indicator

feat: loading indicator and htmx form submit
This commit is contained in:
Björn Fromme
2026-03-16 11:59:10 +01:00
parent 023ecdebc9
commit 83589ee677
20 changed files with 658 additions and 197 deletions
+73 -4
View File
@@ -1,12 +1,81 @@
import { Controller} from '@hotwired/stimulus'
import {Controller} from '@hotwired/stimulus'
export default class extends Controller {
static targets = [ 'indicator' ]
static classes = [ 'hidden' ]
static targets = ['indicator']
static classes = ['hidden']
connect() {
this.isVisible = false
this.debounceTimeout = null
// Bind event handlers to preserve context
this.boundHandleBeforeRequest = this.handleBeforeRequest.bind(this)
this.boundHandleAfterRequest = this.handleAfterRequest.bind(this)
// Listen to HTMX events
document.body.addEventListener('htmx:beforeRequest', this.boundHandleBeforeRequest)
document.body.addEventListener('htmx:afterRequest', this.boundHandleAfterRequest)
}
disconnect() {
// Clean up event listeners
document.body.removeEventListener('htmx:beforeRequest', this.boundHandleBeforeRequest)
document.body.removeEventListener('htmx:afterRequest', this.boundHandleAfterRequest)
// Clear any pending timeout
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
}
}
handleBeforeRequest(event) {
// Don't start a new debounce if already visible
if (this.isVisible) {
return
}
// Show indicator after 200ms delay for field refreshes
this.debounceTimeout = setTimeout(() => {
this.show()
}, 200)
}
handleAfterRequest(event) {
// Clear debounce timer if request completes before 200ms
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
this.debounceTimeout = null
}
// Check if response contains HX-Redirect header
const xhr = event.detail.xhr
const hxRedirect = xhr.getResponseHeader('HX-Redirect')
// Keep loading indicator visible if redirecting
if (hxRedirect) {
// Ensure indicator is visible during redirect
if (!this.isVisible) {
this.show()
}
return
}
// Hide indicator if it's visible
this.hide()
}
show() {
this.isVisible = true
this.indicatorTarget.classList.remove(this.hiddenClass)
}
hide() {
this.isVisible = false
this.indicatorTarget.classList.add(this.hiddenClass)
}
toggle() {
this.indicatorTarget.classList.remove(this.hiddenClass)
this.element.scrollIntoView({ block: 'start', behavior: 'smooth'})
}
}