diff --git a/adev/src/content/guide/forms/signals/custom-controls.md b/adev/src/content/guide/forms/signals/custom-controls.md index 0dddc428f4b2..f561ce566754 100644 --- a/adev/src/content/guide/forms/signals/custom-controls.md +++ b/adev/src/content/guide/forms/signals/custom-controls.md @@ -340,6 +340,54 @@ When the user types an invalid email, the FormField directive automatically upda Most state properties use `input()` (read-only from the form). Use `model()` for `touched` when your control updates it on user interaction. The `touched` property uniquely supports `model()`, `input()`, or `OutputRef` depending on your needs. +### Working with `debounce('blur')` + +The [`debounce('blur')`](api/forms/signals/debounce) rule delays updates from the UI to the form model until the field is blurred, instead of applying them on every keystroke. Built-in controls report a blur to the form automatically. A custom control only participates if it emits its `touch` output in response to the native `blur` event: + +```angular-ts +import {Component, model, output} from '@angular/core'; +import {FormValueControl} from '@angular/forms/signals'; + +@Component({ + selector: 'app-custom-input', + template: ` + + `, +}) +export class CustomInput implements FormValueControl { + value = model(''); + touch = output(); +} +``` + +With the `touch` output in place, `debounce('blur')` behaves the same for your control as it does for built-in inputs: + +```angular-ts +import {Component, signal} from '@angular/core'; +import {debounce, form, FormField} from '@angular/forms/signals'; +import {CustomInput} from './custom-input'; + +@Component({ + selector: 'app-root', + imports: [CustomInput, FormField], + template: ``, +}) +export class App { + userModel = signal({name: ''}); + + userForm = form(this.userModel, (schemaPath) => { + debounce(schemaPath.name, 'blur'); + }); +} +``` + +IMPORTANT: Emit `touch` on `blur` (when focus leaves the control), not on `focus`. Without the `touch` output the field never registers as blurred, so `debounce('blur')` has no effect on your control. + ## Value transformation Controls sometimes display values differently than the form model stores them - a date picker might display "January 15, 2024" while storing "2024-01-15", or a currency input might show "$1,234.56" while storing 1234.56. diff --git a/packages/forms/signals/src/api/control.ts b/packages/forms/signals/src/api/control.ts index b5c66447c163..8fe73db8c7b2 100644 --- a/packages/forms/signals/src/api/control.ts +++ b/packages/forms/signals/src/api/control.ts @@ -113,7 +113,10 @@ export interface FormUiControl { | InputSignal | InputSignalWithTransform; /** - * An output to emit when the control is touched. + * An output to emit when the user finishes interacting with the control, marking the field as + * touched. Emit this in response to the native `blur` event (when focus leaves the control), not + * `focus`. The `Field` directive listens to this output to update the field's touched status, + * which blur-based rules such as `debounce('blur')` rely on. */ readonly touch?: OutputRef; /** diff --git a/packages/forms/signals/src/api/rules/debounce.ts b/packages/forms/signals/src/api/rules/debounce.ts index fb01bb93e108..5bde1515562f 100644 --- a/packages/forms/signals/src/api/rules/debounce.ts +++ b/packages/forms/signals/src/api/rules/debounce.ts @@ -21,6 +21,9 @@ import type {Debouncer, PathKind, SchemaPath, SchemaPathRules} from '../types'; * @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. * + * @see [Custom form controls](guide/forms/signals/custom-controls) for using `debounce('blur')` with + * a custom `FormValueControl`. + * * @publicApi 22.0 */ export function debounce(