Skip to content

Commit 9f7d995

Browse files
feat: use delegatedSignal and debounce via appSettings in ESS solution
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 02f3147 commit 9f7d995

4 files changed

Lines changed: 67 additions & 27 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface AppSettings {
2+
readonly debounceTimeMs: number;
3+
}
4+
5+
export const appSettings: AppSettings = {
6+
debounceTimeMs: 300,
7+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { linkedSignal, WritableSignal } from '@angular/core';
2+
3+
export function delegatedSignal<T>(
4+
getter: () => T,
5+
setter: (value: T) => void,
6+
): WritableSignal<T> {
7+
const read = linkedSignal(getter);
8+
9+
return Object.assign(read, {
10+
set: setter,
11+
update(fn: (value: T) => T) {
12+
setter(fn(read()));
13+
},
14+
asReadonly() {
15+
return read;
16+
},
17+
});
18+
}

‎src/app/domains/ticketing/feature-booking/flight-search/flight-search.spec.ts‎

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
66
import { provideRouter } from '@angular/router';
77
import { page } from 'vitest/browser';
88

9-
import { provideLanguageService } from '../../../shared/util-common/language';
109
import { createTestFlight } from '../../../../testing/create-test-flight';
1110
import { provideTestConfig } from '../../../../testing/provide-test-config';
11+
import { appSettings } from '../../../shared/util-common/app-settings';
12+
import { provideLanguageService } from '../../../shared/util-common/language';
1213
import { FlightSearch } from './flight-search';
1314
import { FlightStore } from './flight-store';
1415

@@ -28,6 +29,9 @@ describe('flight-search', () => {
2829
],
2930
}).compileComponents();
3031

32+
// Turn off debouncing so the form propagates changes immediately
33+
vi.spyOn(appSettings, 'debounceTimeMs', 'get').mockReturnValue(0);
34+
3135
fixture = TestBed.createComponent(FlightSearch);
3236
component = fixture.componentInstance;
3337

@@ -36,9 +40,11 @@ describe('flight-search', () => {
3640
// Await the initial data loading (from = Graz, to = Hamburg)
3741
const request = await vi.waitFor(
3842
() => ctrl.expectOne('/flight?from=Graz&to=Hamburg'),
39-
{ interval: 50, timeout: 1000 },
43+
{ interval: 0 },
4044
);
4145
request.flush([]);
46+
47+
await fixture.whenStable();
4248
});
4349

4450
afterEach(() => {
@@ -57,34 +63,37 @@ describe('flight-search', () => {
5763
await expect.element(button).toBeDisabled();
5864
});
5965

60-
it('enables the search button when from and to are given', async () => {
61-
await page.getByLabelText('From').fill('Paris');
66+
it('loads flights when the search criteria change', async () => {
6267
await page.getByLabelText('To').fill('London');
6368

64-
const button = page.getByRole('button', { name: 'Search' });
65-
await expect.element(button).toBeEnabled();
69+
const request = await vi.waitFor(
70+
() => ctrl.expectOne('/flight?from=Graz&to=London'),
71+
{ interval: 0 },
72+
);
73+
74+
request.flush([
75+
createTestFlight(1),
76+
createTestFlight(2),
77+
createTestFlight(3),
78+
]);
79+
80+
await fixture.whenStable();
6681
});
6782

68-
it('searches for flights when the search button is clicked', async () => {
83+
it('delegates the filter changes to the store', async () => {
6984
const store = TestBed.inject(FlightStore);
7085
vi.spyOn(store, 'updateFilter');
7186

72-
await page.getByLabelText('From').fill('Paris');
7387
await page.getByLabelText('To').fill('London');
7488

75-
const button = page.getByRole('button', { name: 'Search' });
76-
await button.click();
77-
78-
const request = await vi.waitFor(() =>
79-
ctrl.expectOne('/flight?from=Paris&to=London'),
89+
const request = await vi.waitFor(
90+
() => ctrl.expectOne('/flight?from=Graz&to=London'),
91+
{ interval: 0 },
8092
);
93+
request.flush([]);
8194

82-
request.flush([
83-
createTestFlight(1),
84-
createTestFlight(2),
85-
createTestFlight(3),
86-
]);
95+
await fixture.whenStable();
8796

88-
expect(store.updateFilter).toBeCalledWith('Paris', 'London');
97+
expect(store.updateFilter).toBeCalledWith('Graz', 'London');
8998
});
9099
});

‎src/app/domains/ticketing/feature-booking/flight-search/flight-search.ts‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import {
55
computed,
66
effect,
77
inject,
8-
linkedSignal,
98
signal,
109
} from '@angular/core';
11-
import { form, FormField } from '@angular/forms/signals';
10+
import { debounce, form, FormField } from '@angular/forms/signals';
1211
import { RouterLink } from '@angular/router';
1312

13+
import { appSettings } from '../../../shared/util-common/app-settings';
14+
import { delegatedSignal } from '../../../shared/util-common/delegated-signal';
1415
import { LanguageService } from '../../../shared/util-common/language';
1516
import { FlightCard } from '../../ui/flight-card/flight-card';
1617
import { FlightStore } from './flight-store';
@@ -25,11 +26,16 @@ export class FlightSearch {
2526
private flightStore = inject(FlightStore);
2627
private languageService = inject(LanguageService);
2728

28-
protected readonly filter = linkedSignal(() => ({
29-
from: this.flightStore.from(),
30-
to: this.flightStore.to(),
31-
}));
32-
protected readonly filterForm = form(this.filter);
29+
protected readonly filter = delegatedSignal(
30+
() => ({
31+
from: this.flightStore.from(),
32+
to: this.flightStore.to(),
33+
}),
34+
(value) => this.flightStore.updateFilter(value.from, value.to),
35+
);
36+
protected readonly filterForm = form(this.filter, (path) => {
37+
debounce(path, appSettings.debounceTimeMs);
38+
});
3339

3440
protected readonly flightRoute = computed(
3541
() => this.filter().from + ' to ' + this.filter().to,
@@ -57,7 +63,7 @@ export class FlightSearch {
5763
}
5864

5965
protected search(): void {
60-
this.flightStore.updateFilter(this.filter().from, this.filter().to);
66+
this.flightStore.reload();
6167
}
6268

6369
protected updateBasket(flightId: number, selected: boolean): void {

0 commit comments

Comments
 (0)