From 338c7a2e418c47e34331bd04718cd816f978837c Mon Sep 17 00:00:00 2001 From: s Date: Tue, 10 Feb 2026 14:24:06 +0100 Subject: [PATCH] fix(viewer): search causing blank Handsontable Closes #206 --- client/src/app/viewer/viewer.component.ts | 171 ++++++++++------------ 1 file changed, 80 insertions(+), 91 deletions(-) diff --git a/client/src/app/viewer/viewer.component.ts b/client/src/app/viewer/viewer.component.ts index 3a607ed..b34bd0c 100644 --- a/client/src/app/viewer/viewer.component.ts +++ b/client/src/app/viewer/viewer.component.ts @@ -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() {