diff --git a/goldens/public-api/forms/signals/compat/index.api.md b/goldens/public-api/forms/signals/compat/index.api.md index fc3c5bbd257..0e45c68051b 100644 --- a/goldens/public-api/forms/signals/compat/index.api.md +++ b/goldens/public-api/forms/signals/compat/index.api.md @@ -11,6 +11,7 @@ import { FormControlStatus } from '@angular/forms'; import * as i0 from '@angular/core'; import { Injector } from '@angular/core'; import { Signal } from '@angular/core'; +import { StandardSchemaV1 } from '@standard-schema/spec'; import { WritableSignal } from '@angular/core'; // @public diff --git a/goldens/public-api/forms/signals/index.api.md b/goldens/public-api/forms/signals/index.api.md index 6426f18306b..e26759c287d 100644 --- a/goldens/public-api/forms/signals/index.api.md +++ b/goldens/public-api/forms/signals/index.api.md @@ -138,6 +138,10 @@ export type FieldContext = T export interface FieldState extends ReadonlyFieldState { readonly controlValue: WritableSignal; readonly fieldTree: FieldTree; + getError(kind: K): (Extract & ValidationError.WithFieldTree) | undefined; + // (undocumented) getError(kind: string): ValidationError.WithFieldTree | undefined; markAsDirty(): void; markAsTouched(options?: MarkAsTouchedOptions): void; diff --git a/packages/forms/signals/src/api/types.ts b/packages/forms/signals/src/api/types.ts index 22217455e6d..33fdf7aa1bf 100644 --- a/packages/forms/signals/src/api/types.ts +++ b/packages/forms/signals/src/api/types.ts @@ -9,7 +9,7 @@ import {Injector, Signal, WritableSignal} from '@angular/core'; import {AbstractControl} from '@angular/forms'; import type {FormField} from '../directive/form_field'; -import type {MetadataKey, ValidationError} from './rules'; +import type {MetadataKey, NgValidationError, ValidationError} from './rules'; /** * Symbol used to retain generic type information when it would otherwise be lost. @@ -545,6 +545,9 @@ export interface FieldState< * @param kind The kind of error (e.g. 'required', 'min'). * @returns The first matching error, or `undefined` if none. */ + getError( + kind: K, + ): (Extract & ValidationError.WithFieldTree) | undefined; getError(kind: string): ValidationError.WithFieldTree | undefined; /** diff --git a/packages/forms/signals/src/field/node.ts b/packages/forms/signals/src/field/node.ts index bf47c280d0c..ab70fecea53 100644 --- a/packages/forms/signals/src/field/node.ts +++ b/packages/forms/signals/src/field/node.ts @@ -25,7 +25,7 @@ import { REQUIRED, IS_ASYNC_VALIDATION_RESOURCE, } from '../api/rules/metadata'; -import type {ValidationError} from '../api/rules/validation/validation_errors'; +import type {NgValidationError, ValidationError} from '../api/rules/validation/validation_errors'; import type { DisabledReason, FieldContext, @@ -258,6 +258,10 @@ export class FieldNode implements FieldState { return this.metadataState.get(key); } + getError( + kind: K, + ): (Extract & ValidationError.WithFieldTree) | undefined; + getError(kind: string): ValidationError.WithFieldTree | undefined; getError(kind: string): ValidationError.WithFieldTree | undefined { return this.errors().find((e) => e.kind === kind); } diff --git a/packages/forms/signals/test/node/field_node.spec.ts b/packages/forms/signals/test/node/field_node.spec.ts index a630a7b99f5..62422ae90fa 100644 --- a/packages/forms/signals/test/node/field_node.spec.ts +++ b/packages/forms/signals/test/node/field_node.spec.ts @@ -14,17 +14,28 @@ import { applyEach, debounce, disabled, + EmailValidationError, form, FormField, hidden, + MaxDateValidationError, + MaxLengthValidationError, + MaxValidationError, + MinDateValidationError, + MinLengthValidationError, + MinValidationError, + NativeInputParseError, + PatternValidationError, readonly, required, requiredError, + RequiredValidationError, Schema, schema, SchemaOrSchemaFn, SchemaPath, SchemaPathTree, + StandardSchemaValidationError, validate, validateTree, ValidationError, @@ -79,6 +90,60 @@ describe('FieldNode', () => { expect(f.c).toBeUndefined(); }); + it('infers built-in error types from getError', () => { + const f = form(signal({a: 1}), {injector: TestBed.inject(Injector)}); + const field = f.a(); + + { + const error = field.getError('required'); + let t: (RequiredValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('min'); + let t: (MinValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('minDate'); + let t: (MinDateValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('max'); + let t: (MaxValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('maxDate'); + let t: (MaxDateValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('minLength'); + let t: (MinLengthValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('maxLength'); + let t: (MaxLengthValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('pattern'); + let t: (PatternValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('email'); + let t: (EmailValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('standardSchema'); + let t: (StandardSchemaValidationError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('parse'); + let t: (NativeInputParseError & ValidationError.WithFieldTree) | undefined = error; + } + { + const error = field.getError('custom'); + let t: ValidationError.WithFieldTree | undefined = error; + } + }); + it('can get a child inside of a computed', () => { const f = form( signal({