mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(forms): add 'blur' option to debounce rule
Expands the `debounce` rule configuration to accept `'blur'`. When this option is provided, the rule will delay model synchronization until the field loses focus (is touched). This introduces a debouncer that defers resolution until the framework automatically aborts pending debounces upon touch events.
This commit is contained in:
parent
f01901d766
commit
c767d678cf
3 changed files with 84 additions and 11 deletions
|
|
@ -91,7 +91,7 @@ export function createMetadataKey<TWrite>(): MetadataKey<Signal<TWrite | undefin
|
|||
export function createMetadataKey<TWrite, TAcc>(reducer: MetadataReducer<TAcc, TWrite>): MetadataKey<Signal<TAcc>, TWrite, TAcc>;
|
||||
|
||||
// @public
|
||||
export function debounce<TValue, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, durationOrDebouncer: number | Debouncer<TValue, TPathKind>): void;
|
||||
export function debounce<TValue, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, config: number | 'blur' | Debouncer<TValue, TPathKind>): void;
|
||||
|
||||
// @public
|
||||
export type Debouncer<TValue, TPathKind extends PathKind = PathKind.Root> = (context: FieldContext<TValue, TPathKind>, abortSignal: AbortSignal) => Promise<void> | void;
|
||||
|
|
|
|||
|
|
@ -18,27 +18,45 @@ import type {Debouncer, PathKind, SchemaPath, SchemaPathRules} from '../types';
|
|||
* the field is touched, or the most recently debounced update resolves.
|
||||
*
|
||||
* @param path The target path to debounce.
|
||||
* @param durationOrDebouncer Either a debounce duration in milliseconds, or a custom
|
||||
* {@link Debouncer} function.
|
||||
* @param config A debounce configuration, which can be either a debounce duration in milliseconds,
|
||||
* `'blur'` to debounce until the field is blurred, or a custom {@link Debouncer} function.
|
||||
*
|
||||
* @experimental 21.0.0
|
||||
*/
|
||||
export function debounce<TValue, TPathKind extends PathKind = PathKind.Root>(
|
||||
path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,
|
||||
durationOrDebouncer: number | Debouncer<TValue, TPathKind>,
|
||||
config: number | 'blur' | Debouncer<TValue, TPathKind>,
|
||||
): void {
|
||||
assertPathIsCurrent(path);
|
||||
|
||||
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
||||
const debouncer =
|
||||
typeof durationOrDebouncer === 'function'
|
||||
? durationOrDebouncer
|
||||
: durationOrDebouncer > 0
|
||||
? debounceForDuration(durationOrDebouncer)
|
||||
: immediate;
|
||||
const debouncer = normalizeDebouncer(config);
|
||||
pathNode.builder.addMetadataRule(DEBOUNCER, () => debouncer);
|
||||
}
|
||||
|
||||
function normalizeDebouncer<TValue, TPathKind extends PathKind>(
|
||||
debouncer: number | 'blur' | Debouncer<TValue, TPathKind>,
|
||||
) {
|
||||
// If it's already a debounce function, return it as-is.
|
||||
if (typeof debouncer === 'function') {
|
||||
return debouncer;
|
||||
}
|
||||
// If it's 'blur', return a debouncer that never resolves. The field will still be updated when
|
||||
// the control is blurred.
|
||||
if (debouncer === 'blur') {
|
||||
return debounceUntilBlur();
|
||||
}
|
||||
// If it's a non-zero number, return a timer-based debouncer.
|
||||
if (debouncer > 0) {
|
||||
return debounceForDuration(debouncer);
|
||||
}
|
||||
// Otherwise it's 0, so we return a function that will synchronize the model without delay.
|
||||
return immediate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a debouncer that will wait for the given duration before resolving.
|
||||
*/
|
||||
function debounceForDuration(durationInMilliseconds: number): Debouncer<unknown> {
|
||||
return (_context, abortSignal) => {
|
||||
return new Promise((resolve) => {
|
||||
|
|
@ -59,4 +77,16 @@ function debounceForDuration(durationInMilliseconds: number): Debouncer<unknown>
|
|||
};
|
||||
}
|
||||
|
||||
function immediate() {}
|
||||
/**
|
||||
* Creates a debouncer that will wait indefinitely, relying on the node to synchronize pending
|
||||
* updates when blurred.
|
||||
*/
|
||||
function debounceUntilBlur(): Debouncer<unknown> {
|
||||
return (_context, abortSignal) => {
|
||||
return new Promise((resolve) => {
|
||||
abortSignal.addEventListener('abort', () => resolve(), {once: true});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function immediate(): void {}
|
||||
|
|
|
|||
|
|
@ -268,6 +268,49 @@ describe('debounce', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('until blurred', () => {
|
||||
it('should synchronize value immediately on touch', () => {
|
||||
const address = signal({street: ''});
|
||||
const addressForm = form(
|
||||
address,
|
||||
(address) => {
|
||||
debounce(address.street, 'blur');
|
||||
},
|
||||
options(),
|
||||
);
|
||||
const street = addressForm.street();
|
||||
|
||||
street.controlValue.set('1600 Amphitheatre Pkwy');
|
||||
expect(street.controlValue()).toBe('1600 Amphitheatre Pkwy');
|
||||
expect(street.value()).toBe('');
|
||||
|
||||
street.markAsTouched();
|
||||
expect(street.value()).toBe('1600 Amphitheatre Pkwy');
|
||||
});
|
||||
|
||||
it('should be ignored if value is directly set before blur', () => {
|
||||
const address = signal({street: ''});
|
||||
const addressForm = form(
|
||||
address,
|
||||
(address) => {
|
||||
debounce(address.street, 'blur');
|
||||
},
|
||||
options(),
|
||||
);
|
||||
const street = addressForm.street();
|
||||
|
||||
street.controlValue.set('1600 Amphitheatre Pkwy');
|
||||
expect(street.value()).toBe('');
|
||||
|
||||
street.value.set('2000 N Shoreline Blvd');
|
||||
expect(street.value()).toBe('2000 N Shoreline Blvd');
|
||||
expect(street.controlValue()).toBe('2000 N Shoreline Blvd');
|
||||
|
||||
street.markAsTouched();
|
||||
expect(street.value()).toBe('2000 N Shoreline Blvd');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inheritance', () => {
|
||||
it('should inherit debounce from parent', async () => {
|
||||
const address = signal({street: ''});
|
||||
|
|
|
|||
Loading…
Reference in a new issue