useFormTouched
Returns whether a FormGroup has been touched (interacted with) as a signal. The signal updates reactively whenever the form's touched state changes.
Usage
typescript
import { useFormTouched } from 'ng-reactive-utils';
@Component({
template: `
<form [formGroup]="form">
<input formControlName="email" (blur)="onBlur()" />
@if (isTouched() && form.get('email')?.invalid) {
<span class="error">Please enter a valid email</span>
}
</form>
`,
})
class ContactFormComponent {
form = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
});
isTouched = useFormTouched(this.form);
onBlur() {
// Form will be marked as touched automatically on blur
}
}Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
form | FormGroup | required | The FormGroup to check touched state for |
Returns
Signal<boolean> - A readonly signal containing the touched state (true if interacted with)
Notes
- Merges
TouchedChangeEventevents from theFormGroupand all direct child controls — this is necessary because Angular does not propagateTouchedChangeEventfrom child controls up to the parent group - Uses
control.events(notstatusChanges) to listen for touched-state changes;statusChangesdoes not emit on touch changes - Returns the group-level
form.touchedvalue on each event, so the signal reflects the overall touched state of the form - Only tracks direct child controls — grandchild controls inside nested
FormGrouporFormArraychildren are not observed; useuseControlTouchedon the nested group directly if needed - Returns
truewhenmarkAsTouched()is called on the form or any direct child loses focus
Source
ts
import { Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormGroup, TouchedChangeEvent } from '@angular/forms';
import { filter, map, merge } from 'rxjs';
/**
* Returns whether a FormGroup has been touched (interacted with) as a signal.
* The signal updates reactively whenever any control in the form is touched or
* untouched, including programmatic calls to markAsTouched() / markAsUntouched().
*
* @param form - The FormGroup to check touched state for
* @returns A signal containing the touched state (true if interacted with)
*
* @example
* ```typescript
* @Component({
* template: `
* <form [formGroup]="form">
* <input formControlName="email" />
* @if (isTouched() && form.get('email')?.invalid) {
* <span>Please enter a valid email</span>
* }
* </form>
* `
* })
* class MyComponent {
* form = new FormGroup({
* email: new FormControl('', Validators.email)
* });
* isTouched = useFormTouched(this.form);
* }
* ```
*/
export const useFormTouched = (form: FormGroup): Signal<boolean> => {
// TouchedChangeEvent on a FormGroup only fires when markAsTouched() / markAsUntouched()
// is called on the form itself — it does not propagate from child control blur events.
// Merging events from all child controls ensures the signal stays accurate when a user
// interacts with any field in the form.
const allControls = [form, ...Object.values(form.controls)];
const anyTouchedChange$ = merge(...allControls.map((control) => control.events)).pipe(
filter((event): event is TouchedChangeEvent => event instanceof TouchedChangeEvent),
map(() => form.touched),
);
return toSignal(anyTouchedChange$, { initialValue: form.touched }) as Signal<boolean>;
};