fix(viewer): search causing blank Handsontable
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m46s
Build / Build-and-test-development (pull_request) Successful in 9m26s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m52s

Closes #206
This commit is contained in:
s
2026-02-10 14:24:06 +01:00
parent ad27358deb
commit 338c7a2e41

View File

@@ -160,11 +160,7 @@ export class ViewerComponent
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
// CRITICAL: Prevent "colToProp method cannot be called because this Handsontable instance has been destroyed" error
// This callback can be triggered even after the instance is destroyed during rapid table switching
if (
!this.hotInstance ||
this.hotInstance.isDestroyed ||
this.isTableSwitching
) {
if (!this.hotInstance || this.hotInstance.isDestroyed) {
// Graceful fallback: apply only dark mode styling when instance is unavailable
th.classList.add(globals.handsontable.darkTableHeaderClass)
return
@@ -253,9 +249,6 @@ export class ViewerComponent
private isTableSwitching: boolean = false
private switchingTimeout: any = null
// Prevents duplicate setupHot() calls within short time windows
private lastSetupTime: number = 0
public viewboxOpen: boolean = false
constructor(
@@ -761,10 +754,6 @@ export class ViewerComponent
// This prevents callbacks from accessing destroyed instances during table switching
this.isTableSwitching = true
// CLEANUP: Ensure any existing Handsontable instance is properly destroyed
// This prevents "instance destroyed" errors
this.cleanupHotInstance()
this.loadingTableView = true
let libDataset: any
@@ -1177,7 +1166,6 @@ export class ViewerComponent
* Purpose: Prevents "instance destroyed" errors and memory leaks during table switching
*
* Called from:
* - viewData() - before loading new table data
* - setupHot() - before creating new instance
* - ngOnDestroy() - component cleanup
*
@@ -1187,6 +1175,7 @@ export class ViewerComponent
* - Sets instance to null to prevent stale references
*/
private cleanupHotInstance() {
clearTimeout(this.setupHotTimer)
if (this.hotInstance && !this.hotInstance.isDestroyed) {
try {
this.hotInstance.destroy()
@@ -1195,39 +1184,32 @@ export class ViewerComponent
}
}
this.hotInstance = null
this.hooksAttached = false
}
private setupHotTimer: any = null
/**
* PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above)
*
* 1. Duplicate call prevention (500ms window)
* 2. Reduced timeout delays (200ms + 50ms vs original 1000ms + 200ms)
* 3. Multiple validation checks to prevent race conditions
* 4. Forced render for immediate primary key styling
*
* Timeline: 50ms (viewData) + 200ms (main) + 50ms (component ready) = ~300ms total
* Previous: 100ms + 600ms + 100ms = 800ms (plus render delays = ~2 seconds)
* Configures Handsontable instance with settings, styling and hooks.
* Instance lifecycle is managed by Angular's hot-table component via [data] and [settings] bindings.
* This method only applies additional config that can't go through bindings (hooks, PK styling).
* Debounced to avoid expensive render() calls on large tables.
*/
private setupHot() {
// DUPLICATE PREVENTION: Avoid multiple setup calls during rapid table switching
const now = Date.now()
if (now - this.lastSetupTime < 500) {
return
}
this.lastSetupTime = now
clearTimeout(this.setupHotTimer)
setTimeout(() => {
// VALIDATION: Don't setup if we're currently switching tables or still loading
if (this.loadingTableView || !this.libDataset) {
this.setupHotTimer = setTimeout(() => {
if (!this.hotInstance || this.hotInstance.isDestroyed) {
this.hotInstance = this.hotTableComponent?.hotInstance
}
if (this.hotInstance && !this.hotInstance.isDestroyed) {
this.configureHotInstance()
return
}
// CLEANUP: Ensure clean slate before new setup
this.cleanupHotInstance()
// TIMING: Wait for Angular component to be ready (optimized from 100ms to 50ms)
// Instance not ready yet — Angular may still be creating the component
setTimeout(() => {
// DOUBLE-CHECK: Ensure we're still in valid state after delays
if (
this.isTableSwitching ||
this.loadingTableView ||
@@ -1237,65 +1219,72 @@ export class ViewerComponent
}
this.hotInstance = this.hotTableComponent?.hotInstance
this.configureHotInstance()
}, 250)
}, 50)
}
if (this.hotInstance && !this.hotInstance.isDestroyed) {
this.hotInstance.updateSettings({
height: this.hotTable.height,
modifyColWidth: (width: any, col: any) => {
if (width > 500) return 500
else return width
},
afterGetColHeader: (col: number, th: any) => {
// CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors
if (
!this.hotInstance ||
this.hotInstance.isDestroyed ||
this.isTableSwitching
) {
th.classList.add(globals.handsontable.darkTableHeaderClass)
return
}
private hooksAttached = false
try {
const column = this.hotInstance.colToProp(col) as string
/**
* Applies settings that can't go through Angular [settings] binding:
* - Primary key column header styling
* - Column width cap
* - ARIA accessibility hooks (attached once per instance)
*/
private configureHotInstance() {
if (!this.hotInstance || this.hotInstance.isDestroyed) return
// PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
const isPKCol = column && this.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// DARK MODE: Apply to all headers
th.classList.add(globals.handsontable.darkTableHeaderClass)
} catch (error) {
// SAFETY NET: Ensure basic styling is always applied
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
}
})
// Add hooks for accessibility fixes
this.hotInstance.addHook('afterRender', () => {
// Fix ARIA accessibility issues after each render
this.fixAriaAccessibility()
})
this.hotInstance.addHook('afterChange', () => {
// Fix ARIA accessibility issues after any data change
setTimeout(() => {
this.fixAriaAccessibility()
}, 50)
})
// Force immediate render to apply primary key styling
// Without this, styling would wait for ~2 seconds to be applied
// With this, styling appears in ~300ms total (workaround needed for HOT version 16 and above)
setTimeout(() => {
if (this.hotInstance && !this.hotInstance.isDestroyed) {
this.hotInstance.render()
}
}, 10)
this.hotInstance.updateSettings({
height: this.hotTable.height,
modifyColWidth: (width: any, col: any) => {
if (width > 500) return 500
else return width
},
afterGetColHeader: (col: number, th: any) => {
// CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors
if (!this.hotInstance || this.hotInstance.isDestroyed) {
th.classList.add(globals.handsontable.darkTableHeaderClass)
return
}
}, 50) // Optimized Angular component readiness delay
}, 200) // Optimized main setup delay (was 600ms)
try {
const column = this.hotInstance.colToProp(col) as string
// PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
const isPKCol = column && this.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// DARK MODE: Apply to all headers
th.classList.add(globals.handsontable.darkTableHeaderClass)
} catch (error) {
// SAFETY NET: Ensure basic styling is always applied
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
}
})
// Add hooks for accessibility fixes
// Hooks are attached once per instance to avoid accumulating duplicate listeners
if (!this.hooksAttached) {
this.hotInstance.addHook('afterRender', () => {
// Fix ARIA accessibility issues after each render
this.fixAriaAccessibility()
})
this.hotInstance.addHook('afterChange', () => {
// Fix ARIA accessibility issues after any data change
setTimeout(() => {
this.fixAriaAccessibility()
}, 50)
})
this.hooksAttached = true
}
// Force immediate render to apply primary key styling
this.hotInstance.render()
}
async loadWithParameters() {