Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DemoQueryParamStateDemo {
readonly search = injectQueryParam('q', parseAsString.withDefault(''));
readonly search = injectQueryParam(
'q',
parseAsString.withDefault('').withOptions({ debounceMs: 300 }),
);

readonly page = injectQueryParam('page', parseAsInteger.withDefault(1));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { DestroyRef, WritableSignal, inject, signal } from '@angular/core';
import {
DestroyRef,
WritableSignal,
debounced,
effect,
inject,
signal,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { QueryParamSyncService } from './query-param-sync';
Expand Down Expand Up @@ -68,7 +75,7 @@ export function injectQueryParam<T>(
sync.write(key, null, parser._options);
}

// Subscribe to route changes and keep signal in sync
// External URL changes (back/forward, manual edits) flow into the signal.
route.queryParamMap
.pipe(takeUntilDestroyed(destroyRef))
.subscribe((params) => {
Expand All @@ -81,19 +88,32 @@ export function injectQueryParam<T>(
}
});

// Signal is the source of truth; URL is a derived view of it.
const debounceMs = parser._options?.debounceMs ?? 0;
const debouncedView = debounceMs > 0 ? debounced(sig, debounceMs) : null;
const read = debouncedView ? () => debouncedView.value() : () => sig();

let firstRun = true;
effect(() => {
const value = read();
if (firstRun) {
firstRun = false;
return;
}
const raw =
value === null || isDefault(value) ? null : parser.serialize(value);
if (raw === sync.getRaw(key)) return;
sync.write(key, raw, parser._options);
});

return {
value: sig as WritableSignal<T>,

set(value: T | null): void {
const raw =
value === null || isDefault(value) ? null : parser.serialize(value);
sync.write(key, raw, parser._options);
// Optimistic local update for instant UI response
sig.set(value ?? defaultValue);
},

clear(): void {
sync.write(key, null, parser._options);
sig.set(defaultValue);
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export interface Parser<T> {
export interface QueryParamOptions {
/** 'replace' (default) or 'push' — controls browser history behaviour */
history?: 'replace' | 'push';
/**
* Debounce URL writes by this many milliseconds. The signal is still
* updated synchronously — only the navigation is delayed. Defaults to 0
* (writes flushed on the next microtask). Useful for inputs where the
* user types rapidly and you don't want a history entry per keystroke.
*/
debounceMs?: number;
}

export interface ParserBuilder<T> extends Parser<T> {
Expand Down
Loading