Skip to content

Expose markAsUntouched() and markAsPristine() methods in Signal Forms public API #65031

@d-koppenhagen

Description

@d-koppenhagen

Which @angular/* package(s) are relevant/related to the feature request?

forms

Description

The new Angular Signal Forms currently implement markAsUntouched() and markAsPristine() methods in the internal FieldNode class, but these methods are not exposed in the public API through the FieldState interface or FieldTree type.
This prevents developers from manually resetting the touched/dirty state of individual fields, which might be for for forms with dependent fields.

Current Behavior:

  • Methods exist in FieldNode class but are not accessible through public API-
  • Developers cannot reset field state when implementing dependent field logic
  • Only the reset() method is available, which resets the entire field and its descendants

Expected Behavior:

  • markAsUntouched() and markAsPristine() methods should be available on field instances
  • Developers should be able to reset individual field states programmatically

Use Case:

Forms with conditional/dependent fields where field visibility and validation depend on other field values.
When dependencies change, developers need to reset the touched/pristine state of dependent fields.

Example Scenario:

import { Component, input } from '@angular/core';
import { Field, FieldTree, hidden, required, schema } from '@angular/forms/signals';
import { FormError } from '../form-error/form-error';

export interface GenderIdentity {
  gender: '' | 'male' | 'female' | 'diverse';
  salutation: string; // e. g. "Mx.", "Dr.", etc.
  pronoun: string; // e. g. "they/them"
}

export const initialGenderIdentityState: GenderIdentity = {
  gender: '',
  salutation: '',
  pronoun: '',
};

export const identitySchema = schema<GenderIdentity>((path) => {
  hidden(path.salutation, (ctx) => {
    return !ctx.valueOf(path.gender) || ctx.valueOf(path.gender) !== 'diverse';
  });
  hidden(path.pronoun, (ctx) => {
    return !ctx.valueOf(path.gender) || ctx.valueOf(path.gender) !== 'diverse';
  });

  required(path.salutation, {
    when: (ctx) => ctx.valueOf(path.gender) === 'diverse',
    message: 'Please choose a salutation, when diverse gender selected',
  });
  required(path.pronoun, {
    when: (ctx) => ctx.valueOf(path.gender) === 'diverse',
    message: 'Please choose a pronoun, when diverse gender selected',
  });
});

@Component({
  selector: 'app-identity-form',
  imports: [Field, FormError],
  template: `
    <label>
      Gender
      <select
        name="gender-identity"
        [field]="identity().gender"
        (change)="maybeUpdateSalutationAndPronoun()"
      >
        <option value="" selected>Please select</option>
        <option value="male">Male</option>
        <option value="female">Female</option>
        <option value="diverse">Diverse</option>
      </select>
    </label>

    <div class="group-with-gap">
      @if (!identity().salutation().hidden()) {
      <label
        >Salutation
        <input type="text" placeholder="e. g. Mx." [field]="identity().salutation" />
        <app-form-error [fieldRef]="identity().salutation" />
      </label>
      } @if (!identity().pronoun().hidden()) {
      <label
        >Pronoun
        <input type="text" placeholder="e. g. they/them" [field]="identity().pronoun" />
        <app-form-error [fieldRef]="identity().pronoun" />
      </label>
      }
    </div>
  `,
  styleUrl: './identity-form.scss',
})
export class IdentityForm {
  readonly identity = input.required<FieldTree<GenderIdentity>>();

  protected maybeUpdateSalutationAndPronoun() {
    const gender = this.identity().gender().value();
    if (gender !== 'diverse') {
      this.identity().salutation().value.set('');
      this.identity().pronoun().value.set('');
    } else {
      this.identity().salutation().markAsUntouched();
      this.identity().salutation().markAsPristine();
      this.identity().pronoun().markAsUntouched();
      this.identity().pronoun().markAsPristine();
    }
  }
}

Environment:

  • Angular version: 21.0.0-rc.1
  • Package: @angular/forms/signals

Proposed solution

Expose the existing markAsUntouched() and markAsPristine() methods in the public FieldState interface, making them available through FieldTree instances.
The implementation already exists in FieldNode class and just needs to be added to the public API contract.

Alternatives considered

  1. Using reset() method: This resets the entire field and its descendants, which is too broad for the use case of resetting individual field states
  2. Workarounds: No viable workarounds exist for this specific requirement
  3. Manual state management: Would require reimplementing form state logic outside of Angular's Signal Forms system

The proposed solution is the most straightforward as it leverages existing, tested implementation and simply exposes it through the public API.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions