diff --git a/src/app/features/website/catalog/components/filters-panel/filters-panel.component.ts b/src/app/features/website/catalog/components/filters-panel/filters-panel.component.ts index afa3347..52a6417 100644 --- a/src/app/features/website/catalog/components/filters-panel/filters-panel.component.ts +++ b/src/app/features/website/catalog/components/filters-panel/filters-panel.component.ts @@ -1,8 +1,10 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, Output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { FilterGroup, SearchFilterState } from '../../../../../core/search/models/search.model'; import { TranslatePipe } from '../../../../../i18n/translate.pipe'; +const RANGE_DEBOUNCE_MS = 350; + @Component({ selector: 'app-catalog-filters-panel', standalone: true, @@ -11,12 +13,20 @@ import { TranslatePipe } from '../../../../../i18n/translate.pipe'; styleUrls: ['./filters-panel.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class CatalogFiltersPanelComponent { +export class CatalogFiltersPanelComponent implements OnDestroy { @Input() definitions: FilterGroup[] = []; @Input() state: SearchFilterState = { values: {}, ranges: {}, toggles: {} }; @Output() stateChange = new EventEmitter(); + private readonly debounceTimers = new Map>(); + + ngOnDestroy(): void { + for (const timer of this.debounceTimers.values()) { + clearTimeout(timer); + } + } + readonly collapsed = signal>({}); toggleGroup(filterId: string): void { @@ -79,34 +89,51 @@ export class CatalogFiltersPanelComponent { updateRange(filterId: string, key: 'min' | 'max', rawValue: string): void { const value = rawValue.trim().length ? Number(rawValue) : undefined; - const current = this.state.ranges[filterId] ?? {}; - this.stateChange.emit({ - ...this.state, - ranges: { - ...this.state.ranges, - [filterId]: { - ...current, - [key]: Number.isFinite(value as number) ? value : undefined + this.debounce(`range:${filterId}:${key}`, () => { + const current = this.state.ranges[filterId] ?? {}; + this.stateChange.emit({ + ...this.state, + ranges: { + ...this.state.ranges, + [filterId]: { + ...current, + [key]: Number.isFinite(value as number) ? value : undefined + } } - } + }); }); } updateSlider(filterId: string, value: string): void { const numeric = Number(value); - this.stateChange.emit({ - ...this.state, - ranges: { - ...this.state.ranges, - [filterId]: { - ...this.state.ranges[filterId], - max: Number.isFinite(numeric) ? numeric : undefined, + + this.debounce(`slider:${filterId}`, () => { + this.stateChange.emit({ + ...this.state, + ranges: { + ...this.state.ranges, + [filterId]: { + ...this.state.ranges[filterId], + max: Number.isFinite(numeric) ? numeric : undefined, + }, }, - }, + }); }); } + /** Debounces range/slider input so full catalog filter recompute doesn't run on every keystroke/drag event. */ + private debounce(key: string, action: () => void): void { + const existing = this.debounceTimers.get(key); + if (existing) { + clearTimeout(existing); + } + this.debounceTimers.set(key, setTimeout(() => { + this.debounceTimers.delete(key); + action(); + }, RANGE_DEBOUNCE_MS)); + } + updateToggle(filterId: string, checked: boolean): void { this.stateChange.emit({ ...this.state,