Skip to content

useFormState

Converts a FormGroup into a reactive state object with signals for all form properties. This provides a comprehensive view of the form's state that updates reactively.

Usage

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

@Component({
  template: `
    <form [formGroup]="form">
      <input formControlName="name" />
      <input formControlName="email" />

      @if (formState.invalid()) {
        <span class="error">Form has errors</span>
      }

      @if (formState.dirty()) {
        <span class="warning">You have unsaved changes</span>
      }

      <button [disabled]="formState.invalid() || formState.pending()">Submit</button>
    </form>

    <pre>{{ formState.value() | json }}</pre>
  `,
})
class UserFormComponent {
  form = new FormGroup({
    name: new FormControl('', Validators.required),
    email: new FormControl('', [Validators.required, Validators.email]),
  });

  formState = useFormState<{ name: string; email: string }>(this.form);
}

Parameters

ParameterTypeDefaultDescription
formFormGrouprequiredThe FormGroup to convert to signals

Returns

FormState<T> - An object containing signals for all form state properties:

| Property | Type | Description | | ----------- | --------------------------- | ----------------------------------------------------------------- | --------------------------------- | | value | Signal<T> | The current value of the form | | status | Signal<FormControlStatus> | The validation status ('VALID', 'INVALID', 'PENDING', 'DISABLED') | | valid | Signal<boolean> | Whether the form is valid | | invalid | Signal<boolean> | Whether the form is invalid | | pending | Signal<boolean> | Whether async validators are running | | disabled | Signal<boolean> | Whether the form is disabled | | enabled | Signal<boolean> | Whether the form is enabled | | dirty | Signal<boolean> | Whether the form has been modified | | pristine | Signal<boolean> | Whether the form has not been modified | | touched | Signal<boolean> | Whether the form has been interacted with | | untouched | Signal<boolean> | Whether the form has not been interacted with | | errors | Signal<ValidationErrors | null> | The validation errors of the form |

Notes

  • Composes all individual form composables (useFormValue, useFormValid, useFormTouched, etc.) into a single convenience object
  • invalid is computed(() => !valid()) and enabled is computed(() => !disabled()) — they are derived signals, not independent subscriptions, so they are always atomically consistent with their counterparts
  • touched and untouched use merged child control events (see useFormTouched for details on the shallow-only limitation)
  • Type parameter T must extend object and should match your form's value structure
  • For individual properties, consider using the specific composables like useFormValue, useFormValid, etc. to avoid subscribing to all observables when only one is needed

Source

ts
import { computed, Signal } from '@angular/core';
import { FormGroup, FormControlStatus, ValidationErrors } from '@angular/forms';
import { useFormValue } from '../use-form-value/use-form-value.composable';
import { useFormStatus } from '../use-form-status/use-form-status.composable';
import { useFormValid } from '../use-form-valid/use-form-valid.composable';
import { useFormPending } from '../use-form-pending/use-form-pending.composable';
import { useFormDisabled } from '../use-form-disabled/use-form-disabled.composable';
import { useFormDirty } from '../use-form-dirty/use-form-dirty.composable';
import { useFormPristine } from '../use-form-pristine/use-form-pristine.composable';
import { useFormTouched } from '../use-form-touched/use-form-touched.composable';
import { useFormUntouched } from '../use-form-untouched/use-form-untouched.composable';
import { useFormErrors } from '../use-form-errors/use-form-errors.composable';

/**
 * Represents the reactive state of a FormGroup as signals.
 */
export interface FormState<T> {
  /** The current value of the form */
  value: Signal<T>;
  /** The validation status of the form */
  status: Signal<FormControlStatus>;
  /** Whether the form is valid */
  valid: Signal<boolean>;
  /** Whether the form is invalid */
  invalid: Signal<boolean>;
  /** Whether the form has pending async validators */
  pending: Signal<boolean>;
  /** Whether the form is disabled */
  disabled: Signal<boolean>;
  /** Whether the form is enabled */
  enabled: Signal<boolean>;
  /** Whether the form value has been modified */
  dirty: Signal<boolean>;
  /** Whether the form value has not been modified */
  pristine: Signal<boolean>;
  /** Whether the form has been interacted with */
  touched: Signal<boolean>;
  /** Whether the form has not been interacted with */
  untouched: Signal<boolean>;
  /** The validation errors of the form */
  errors: Signal<ValidationErrors | null>;
}

/**
 * Converts a FormGroup into a reactive state object with signals for all form properties.
 * This provides a comprehensive view of the form's state that updates reactively.
 *
 * @param form - The FormGroup to convert to signals
 * @returns An object containing signals for all form state properties
 *
 * @example
 * ```typescript
 * @Component({
 *   template: `
 *     <form [formGroup]="form">
 *       <input formControlName="name" />
 *       @if (formState.invalid()) {
 *         <span>Form has errors</span>
 *       }
 *     </form>
 *   `
 * })
 * class MyComponent {
 *   form = new FormGroup({
 *     name: new FormControl('')
 *   });
 *   formState = useFormState(this.form);
 * }
 * ```
 */
export const useFormState = <T extends object>(form: FormGroup): FormState<T> => {
  const valid = useFormValid(form);
  const disabled = useFormDisabled(form);

  return {
    value: useFormValue(form),
    status: useFormStatus(form),
    valid,
    invalid: computed(() => !valid()),
    pending: useFormPending(form),
    disabled,
    enabled: computed(() => !disabled()),
    dirty: useFormDirty(form),
    pristine: useFormPristine(form),
    touched: useFormTouched(form),
    untouched: useFormUntouched(form),
    errors: useFormErrors(form),
  };
};

Released under the MIT License.