Skip to content

useControlUntouched

Returns whether an AbstractControl is untouched (has not been interacted with) as a signal. The signal updates reactively whenever the control's untouched state changes. Works with FormControl, FormGroup, and FormArray.

Usage

typescript
import { useControlUntouched } from 'ng-reactive-utils';

@Component({
  template: `
    <input [formControl]="emailControl" />

    @if (isUntouched()) {
      <span class="hint">Click to enter your email</span>
    }
  `,
})
class EmailFieldComponent {
  emailControl = new FormControl('');
  isUntouched = useControlUntouched(this.emailControl);
}

Advanced Usage

typescript
import { useControlUntouched } from 'ng-reactive-utils';

@Component({
  template: `
    <div class="form-field" [class.pristine]="isUntouched()">
      <label>Password</label>
      <input type="password" [formControl]="passwordControl" />

      @if (isUntouched()) {
        <div class="requirements">Password must be at least 8 characters</div>
      } @else if (passwordControl.invalid) {
        <div class="error">Please enter a valid password</div>
      }
    </div>
  `,
})
class PasswordFieldComponent {
  passwordControl = new FormControl('', [Validators.minLength(8)]);
  isUntouched = useControlUntouched(this.passwordControl);
}

Parameters

ParameterTypeDefaultDescription
controlAbstractControlrequiredThe control to check untouched state for

Returns

Signal<boolean> - A readonly signal containing the untouched state (true if not interacted with)

Notes

  • Works with FormControl, FormGroup, and FormArray
  • Uses control.events (Angular's unified event stream) filtered to TouchedChangeEvent — not statusChanges, which does not emit on touched-state changes
  • Returns true when the control has not been blurred or programmatically touched
  • Returns false once markAsTouched() is called or the control loses focus
  • Opposite of useControlTouched
  • Useful for showing hints before user interaction

Source

ts
import { Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { AbstractControl, TouchedChangeEvent } from '@angular/forms';
import { filter, map } from 'rxjs';

/**
 * Returns whether an AbstractControl is untouched (has not been interacted with) as a signal.
 * The signal updates reactively whenever the control's untouched state changes,
 * including when markAsTouched() or markAsUntouched() are called directly.
 * Works with FormControl, FormGroup, and FormArray.
 *
 * @param control - The AbstractControl to check untouched state for
 * @returns A signal containing the untouched state (true if not interacted with)
 *
 * @example
 * ```typescript
 * @Component({
 *   template: `
 *     <input [formControl]="emailControl" />
 *     @if (isUntouched()) {
 *       <span>Please fill out this field</span>
 *     }
 *   `
 * })
 * class MyComponent {
 *   emailControl = new FormControl('');
 *   isUntouched = useControlUntouched(this.emailControl);
 * }
 * ```
 */
export const useControlUntouched = (control: AbstractControl): Signal<boolean> => {
  return toSignal(
    control.events.pipe(
      filter((event): event is TouchedChangeEvent => event instanceof TouchedChangeEvent),
      map((event) => !event.touched),
    ),
    { initialValue: control.untouched },
  ) as Signal<boolean>;
};

Released under the MIT License.