ngx-vest-forms
Version:
Opinionated template-driven forms library for Angular with Vest.js integration
1,192 lines (1,175 loc) • 114 kB
TypeScript
import * as _angular_core from '@angular/core';
import { Signal, AfterContentInit, OnDestroy, InputSignal, AfterViewInit, InjectionToken } from '@angular/core';
import { ValidationErrors, NgModel, NgModelGroup, AsyncValidator, AbstractControl, NgForm, AsyncValidatorFn, FormsModule, ControlContainer, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import * as ngx_vest_forms from 'ngx-vest-forms';
import { StaticSuite } from 'vest';
/**
* Angular's ValidationErrors is `{ [key: string]: any }`.
* We extend it with Vest-specific structure for better type safety.
*/
type VestValidationErrors = {
/** Vest error messages array */
errors?: readonly string[];
/** Vest warning messages array */
warnings?: readonly string[];
} & ValidationErrors;
/**
* Form control status values as defined by Angular.
* @see AbstractControl.status
*/
type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
/**
* Represents the core state of an Angular form control.
* Uses narrower types than Angular's defaults where possible.
*/
type FormControlState = {
readonly status: FormControlStatus | null;
readonly isValid: boolean;
readonly isInvalid: boolean;
readonly isPending: boolean;
readonly isDisabled: boolean;
readonly isTouched: boolean;
readonly isDirty: boolean;
readonly isPristine: boolean;
/** Errors from Angular validators or Vest validation */
readonly errors: VestValidationErrors | null;
};
declare class FormControlStateDirective {
#private;
protected readonly contentNgModel: _angular_core.Signal<NgModel | undefined>;
protected readonly contentNgModelGroup: _angular_core.Signal<NgModelGroup | undefined>;
constructor();
/**
* Main control state computed signal (merges robust touched/dirty)
*/
readonly controlState: _angular_core.Signal<FormControlState>;
/**
* Extracts error messages from Angular/Vest errors (recursively flattens)
*/
readonly errorMessages: _angular_core.Signal<string[]>;
/**
* ADVANCED: updateOn strategy (change/blur/submit) if available
*/
readonly updateOn: _angular_core.Signal<"change" | "blur" | "submit">;
/**
* ADVANCED: Composite/derived signals for advanced error display logic
*/
readonly isValidTouched: _angular_core.Signal<boolean>;
readonly isInvalidTouched: _angular_core.Signal<boolean>;
readonly shouldShowErrors: _angular_core.Signal<boolean>;
/**
* Extracts warning messages from Vest validation results.
* Checks two sources:
* 1. control.errors.warnings (when errors exist alongside warnings)
* 2. FormDirective.fieldWarnings (for warnings-only scenarios)
* This dual-source approach allows warnings to be displayed without affecting field validity.
*/
readonly warningMessages: _angular_core.Signal<string[]>;
/**
* Whether async validation is in progress
*/
readonly hasPendingValidation: _angular_core.Signal<boolean>;
/**
* Convenience signals for common state checks
*/
readonly isValid: _angular_core.Signal<boolean>;
readonly isInvalid: _angular_core.Signal<boolean>;
readonly isPending: _angular_core.Signal<boolean>;
readonly isTouched: _angular_core.Signal<boolean>;
readonly isDirty: _angular_core.Signal<boolean>;
readonly isPristine: _angular_core.Signal<boolean>;
readonly isDisabled: _angular_core.Signal<boolean>;
readonly hasErrors: _angular_core.Signal<boolean>;
/**
* Whether this control has been validated at least once.
* True after the first validation completes, even if the user hasn't touched the field.
* This is primarily used for warning display and other derived state that should react
* to validationConfig-triggered validation even before the user touches the field.
*/
readonly hasBeenValidated: _angular_core.Signal<boolean>;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormControlStateDirective, never>;
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<FormControlStateDirective, "[formControlState], [ngxControlState]", ["formControlState", "ngxControlState"], {}, {}, ["contentNgModel", "contentNgModelGroup"], never, true, never>;
}
/**
* Error display modes for form controls.
* - 'on-blur': Show errors after field is touched/blurred
* - 'on-submit': Show errors after form submission
* - 'on-blur-or-submit': Show errors after blur or form submission (default)
* - 'on-dirty': Show errors as soon as the field value changes
* - 'always': Show errors immediately, even on pristine fields
*/
type ScErrorDisplayMode = 'on-blur' | 'on-submit' | 'on-blur-or-submit' | 'on-dirty' | 'always';
/**
* Warning display modes for form controls.
* - 'on-touch': Show warnings after field is touched/blurred
* - 'on-validated-or-touch': Show warnings after validation runs or field is touched (default)
* - 'on-dirty': Show warnings as soon as the field value changes
* - 'always': Show warnings immediately, even on pristine fields
*/
type NgxWarningDisplayMode = 'on-touch' | 'on-validated-or-touch' | 'on-dirty' | 'always';
declare class FormErrorDisplayDirective {
#private;
/**
* Input signal for error display mode.
* Works seamlessly with hostDirectives in Angular 19+.
*/
readonly errorDisplayMode: _angular_core.InputSignal<ScErrorDisplayMode>;
/**
* Input signal for warning display mode.
* Controls whether warnings are shown only after touch or also after validation.
*/
readonly warningDisplayMode: _angular_core.InputSignal<NgxWarningDisplayMode>;
readonly controlState: Signal<FormControlState>;
readonly errorMessages: Signal<string[]>;
readonly warningMessages: Signal<string[]>;
readonly hasPendingValidation: Signal<boolean>;
readonly isTouched: Signal<boolean>;
readonly isDirty: Signal<boolean>;
readonly isValid: Signal<boolean>;
readonly isInvalid: Signal<boolean>;
readonly hasBeenValidated: Signal<boolean>;
/**
* Expose updateOn and formSubmitted as public signals for advanced consumers.
* updateOn: The ngModelOptions.updateOn value for the control (change/blur/submit)
* formSubmitted: true after the form is submitted (if NgForm is present)
*/
readonly updateOn: Signal<"change" | "blur" | "submit">;
/**
* Signal that tracks NgForm.submitted state reactively.
*
* Map form-level submit/reset events directly to boolean state.
*
* This keeps programmatic `NgForm.onSubmit()` reactive in zoneless mode and
* avoids depending on `NgForm.submitted`, whose getter intentionally reads an
* internal signal with `untracked()`.
*
* Note: when this directive is used outside an `NgForm` (no parent form), no
* subscription is wired up and this signal stays `false` for the lifetime of
* the directive. Consumers relying on submitted state must host the field
* inside an `NgForm` (or `ngxVestForm`).
*/
readonly formSubmitted: Signal<boolean>;
constructor();
/**
* Determines if errors should be shown based on the specified display mode
* and the control's state (touched/submitted/dirty).
*
* Note: We check both hasErrors (extracted error messages) AND isInvalid (Angular's validation state)
* because in some cases (like conditional validations via validationConfig), the control is marked
* as invalid by Angular before error messages are extracted from Vest. This ensures aria-invalid
* is set correctly even during the validation propagation delay.
*
* For validationConfig-triggered validations, a field may become invalid before it has been
* touched. Error visibility still respects the field's own `errorDisplayMode`, so untouched
* dependent fields can remain visually quiet until blur or submit.
*/
readonly shouldShowErrors: Signal<boolean>;
/**
* Errors to display (filtered for pending state)
*/
readonly errors: Signal<string[]>;
/**
* Warnings to display (filtered for pending state)
*/
readonly warnings: Signal<string[]>;
/**
* Whether the control is currently being validated (pending)
* Excludes pristine+untouched controls to prevent "Validating..." on initial load
*/
readonly isPending: Signal<boolean>;
/**
* Determines if warnings should be shown based on the specified display mode
* and the control's state (touched/validated/dirty).
*
* NOTE: Unlike errors, warnings can exist on VALID fields (warnings-only scenario).
* We don't require isInvalid() because Vest warn() tests don't affect field validity.
*
* UX Note: We include `hasBeenValidated` for `on-validated-or-touch` mode to support
* cross-field validation. If Field A triggers validation on Field B (via validationConfig),
* Field B should show warnings if it has them, even if the user hasn't touched Field B yet.
* Unlike errors (which block submission), warnings are informational and safe to show.
*/
readonly shouldShowWarnings: Signal<boolean>;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormErrorDisplayDirective, never>;
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<FormErrorDisplayDirective, "[formErrorDisplay], [ngxErrorDisplay]", ["formErrorDisplay", "ngxErrorDisplay"], { "errorDisplayMode": { "alias": "errorDisplayMode"; "required": false; "isSignal": true; }; "warningDisplayMode": { "alias": "warningDisplayMode"; "required": false; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof FormControlStateDirective; inputs: {}; outputs: {}; }]>;
}
type AriaAssociationMode = 'all-controls' | 'single-control' | 'none';
/**
* Splits an `aria-describedby` attribute value into normalized token IDs.
*/
declare function parseAriaIdTokens(value: string | null): string[];
/**
* Merges currently-active wrapper IDs into an existing `aria-describedby` value.
*
* Existing tokens owned by the wrapper are removed first, then current active IDs
* are appended while preserving non-owned tokens and token uniqueness.
*/
declare function mergeAriaDescribedBy(existing: string | null, activeIds: readonly string[], ownedIds: readonly string[]): string | null;
/**
* Resolves control targets based on ARIA association mode.
*/
declare function resolveAssociationTargets(controls: readonly HTMLElement[], mode: AriaAssociationMode): HTMLElement[];
/**
* Accessible form control wrapper built with WCAG 2.2 AA considerations.
*
* Wrap form fields to automatically display validation errors, warnings, and pending states
* with proper accessibility attributes.
*
* @usageNotes
*
* ### Basic Usage
* ```html
* <ngx-control-wrapper>
* <label for="email">Email</label>
* <input id="email" name="email" [ngModel]="formValue().email" />
* </ngx-control-wrapper>
* ```
*
* ### Error Display Modes
* Control when errors appear using the `errorDisplayMode` input:
* - `'on-blur-or-submit'` (default): Show errors after blur OR form submit
* - `'on-blur'`: Show errors only after blur
* - `'on-submit'`: Show errors only after form submit
* - `'on-dirty'`: Show errors as soon as the field value changes
* - `'always'`: Show errors immediately, even on pristine fields
*
* ```html
* <ngx-control-wrapper [errorDisplayMode]="'on-submit'">
* <input name="email" [ngModel]="formValue().email" />
* </ngx-control-wrapper>
* ```
*
* ### Warning Display Modes
* Control when warnings appear using the `warningDisplayMode` input:
* - `'on-validated-or-touch'` (default): Show warnings after validation runs or touch
* - `'on-touch'`: Show warnings only after touch/blur
* - `'on-dirty'`: Show warnings as soon as the field value changes
* - `'always'`: Show warnings immediately, even on pristine fields
*
* ```html
* <ngx-control-wrapper [warningDisplayMode]="'on-dirty'">
* <input name="email" [ngModel]="formValue().email" />
* </ngx-control-wrapper>
* ```
*
* ### Accessibility Features (Automatic)
* - Unique IDs for error/warning/pending regions
* - `aria-describedby` linking errors to form controls
* - `aria-invalid="true"` when errors are shown
* - Uses `role="status"` with `aria-live="polite"` for non-disruptive announcements
* - Debounced pending state to prevent flashing for quick validations
*
* ### WCAG 2.2 AA - Error Severity Levels
* This component uses `role="status"` for **field-level** validation messages:
* - **Errors**: Non-disruptive announcement (user can continue filling other fields)
* - **Warnings**: Informational only, doesn't block submission
* - **Pending**: Status update while async validation runs
*
* For **form-level blocking errors** (e.g., submission failed), implement a separate
* error summary component with `role="alert"` and `aria-live="assertive"`.
*
* @see {@link FormErrorDisplayDirective} for custom wrapper implementation
*
* Form-Level Blocking Errors:
* - For post-submit validation errors that block submission, implement a separate
* form-level error summary with role="alert" and aria-live="assertive"
* - This component uses role="status" for field-level errors (non-disruptive)
* - Example for form-level errors:
* ```html
* <!-- Keep in DOM; update text content on submit -->
* <div id="form-errors" role="alert" aria-live="assertive" aria-atomic="true"></div>
* ```
* - This separation provides immediate announcements for blocking form errors while
* keeping inline field errors non-disruptive. Follows WCAG ARIA21/ARIA22 guidance.
*
* Error & Warning Display Behavior:
* - The error display mode can be configured globally using the NGX_ERROR_DISPLAY_MODE_TOKEN injection token, or per instance using the `errorDisplayMode` input on FormErrorDisplayDirective (which this component uses as a hostDirective).
* - Possible error display values: 'on-blur' | 'on-submit' | 'on-blur-or-submit' | 'on-dirty' | 'always' (default: 'on-blur-or-submit')
* - The warning display mode can be configured globally using NGX_WARNING_DISPLAY_MODE_TOKEN, or per instance using the `warningDisplayMode` input on FormErrorDisplayDirective.
* - Possible warning display values: 'on-touch' | 'on-validated-or-touch' | 'on-dirty' | 'always' (default: 'on-validated-or-touch')
*
* Example (per instance):
* <div ngxControlWrapper>
* <label>
* <span>First name</span>
* <input type="text" name="firstName" [ngModel]="formValue().firstName" />
* </label>
* </div>
* /// To customize errorDisplayMode for this instance, use the errorDisplayMode input.
*
* Example (with warnings and pending):
* <ngx-control-wrapper>
* <input name="username" ngModel />
* </ngx-control-wrapper>
* /// If async validation is running for >500ms, a spinner and 'Validating…' will be shown.
* /// Once shown, the validation message stays visible for minimum 500ms to prevent flashing.
* /// If Vest warnings are present, they will be shown below errors.
*
* Example (global config):
* import { provide } from '@angular/core';
* import { NGX_ERROR_DISPLAY_MODE_TOKEN } from 'ngx-vest-forms';
* @Component({
* providers: [
* provide(NGX_ERROR_DISPLAY_MODE_TOKEN, { useValue: 'on-submit' })
* ]
* })
* export class MyComponent {}
*
* Best Practices:
* - Use for single-control wrappers.
* - For multi-control/group containers, prefer `ngx-form-group-wrapper`.
* - Do not manually display errors for individual fields; rely on this wrapper.
* - Validate with tools like Accessibility Insights and real screen reader testing.
*
* @see https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA19 - ARIA19: Using ARIA role=alert
* @see https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA22 - ARIA22: Using role=status
*/
declare class ControlWrapperComponent implements AfterContentInit, OnDestroy {
protected readonly errorDisplay: FormErrorDisplayDirective;
private readonly elementRef;
private readonly destroyRef;
/**
* Controls how this wrapper applies ARIA attributes to descendant controls.
*
* - `all-controls` (default, backwards compatible): apply `aria-describedby` / `aria-invalid`
* to all `input/select/textarea` elements inside the wrapper.
* - `single-control`: apply ARIA attributes only when exactly one control is found.
* (Useful for wrappers that sometimes contain helper buttons/controls.)
* - `none`: do not mutate descendant controls at all (group-safe mode).
*
* Notes:
* - Use `none` when wrapping a container (e.g. `NgModelGroup`) to avoid stamping ARIA
* across multiple child controls.
* - This does not affect whether messages render; it only affects ARIA wiring.
*/
readonly ariaAssociationMode: _angular_core.InputSignal<AriaAssociationMode>;
protected readonly uniqueId: string;
protected readonly errorId: string;
protected readonly warningId: string;
protected readonly pendingId: string;
private readonly formControls;
private readonly contentInitialized;
private mutationObserver;
/**
* Debounced pending state to prevent flashing for quick async validations.
* Uses createDebouncedPendingState utility with 500ms delay and 500ms minimum display.
*/
private readonly pendingState;
protected readonly showPendingMessage: _angular_core.Signal<boolean>;
/**
* Whether to display warnings.
* Delegates to FormErrorDisplayDirective's centralized shouldShowWarnings signal.
*
* This ensures consistent warning display behavior across all form components
* and supports the new 'on-dirty' and 'always' display modes.
*/
protected readonly shouldShowWarnings: _angular_core.Signal<boolean>;
/**
* Computed signal that builds aria-describedby string based on visible regions
*/
protected readonly ariaDescribedBy: _angular_core.Signal<string | null>;
/**
* IDs managed by this wrapper when composing aria-describedby.
*
* We remove only these from the consumer-provided aria-describedby tokens and then
* append the currently-relevant wrapper IDs. This prevents clobbering app-provided
* hint/help text associations.
*/
private readonly wrapperOwnedDescribedByIds;
constructor();
ngAfterContentInit(): void;
ngOnDestroy(): void;
/**
* Query and update the list of form controls within this wrapper.
* Called on init and whenever the DOM structure changes.
*/
private updateFormControls;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<ControlWrapperComponent, never>;
static ɵcmp: _angular_core.ɵɵComponentDeclaration<ControlWrapperComponent, "ngx-control-wrapper, sc-control-wrapper, [scControlWrapper], [ngxControlWrapper], [ngx-control-wrapper], [sc-control-wrapper]", never, { "ariaAssociationMode": { "alias": "ariaAssociationMode"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, [{ directive: typeof FormErrorDisplayDirective; inputs: { "errorDisplayMode": "errorDisplayMode"; "warningDisplayMode": "warningDisplayMode"; }; outputs: {}; }]>;
}
/**
* Group-safe wrapper for `NgModelGroup` containers.
*
* This component renders group-level errors/warnings/pending UI, but intentionally
* does **not** stamp `aria-describedby` / `aria-invalid` onto descendant controls.
*
* Use this when you want a wrapper around a container that has multiple inputs.
* For single inputs, prefer `<ngx-control-wrapper>`.
*/
declare class FormGroupWrapperComponent {
protected readonly errorDisplay: FormErrorDisplayDirective;
/**
* Controls the debounce behavior for the pending message.
* Defaults are conservative to avoid flashing.
*/
readonly pendingDebounce: _angular_core.InputSignal<{
showAfter: number;
minimumDisplay: number;
}>;
protected readonly uniqueId: string;
readonly errorId: string;
readonly warningId: string;
readonly pendingId: string;
private readonly pendingState;
protected readonly showPendingMessage: _angular_core.Signal<boolean>;
/**
* Helpful if consumers want to wire aria-describedby manually (e.g. fieldset/legend pattern).
*/
readonly describedByIds: _angular_core.Signal<string | null>;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormGroupWrapperComponent, never>;
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormGroupWrapperComponent, "ngx-form-group-wrapper, sc-form-group-wrapper, [ngxFormGroupWrapper], [scFormGroupWrapper]", ["ngxFormGroupWrapper"], { "pendingDebounce": { "alias": "pendingDebounce"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, [{ directive: typeof FormErrorDisplayDirective; inputs: { "errorDisplayMode": "errorDisplayMode"; "warningDisplayMode": "warningDisplayMode"; }; outputs: {}; }]>;
}
/**
* Wires a control container to its error/warning/pending regions.
*
* This directive is intended for custom wrappers/components.
* It composes `FormErrorDisplayDirective` (and thus `FormControlStateDirective`)
* and applies `aria-invalid` / `aria-describedby` to descendant controls.
*
* It does not render any UI; you can use the generated IDs to render messages.
*/
declare class FormErrorControlDirective implements AfterContentInit, OnDestroy {
protected readonly errorDisplay: FormErrorDisplayDirective;
private readonly elementRef;
/**
* Controls how this directive applies ARIA attributes to descendant controls.
*
* - `all-controls` (default): apply ARIA attributes to all input/select/textarea descendants.
* - `single-control`: apply ARIA attributes only when exactly one control is found.
* - `none`: do not mutate descendant controls.
*/
readonly ariaAssociationMode: _angular_core.InputSignal<AriaAssociationMode>;
/**
* Unique ID prefix for this instance.
* Use these IDs to render message regions and to support aria-describedby.
*/
protected readonly uniqueId: string;
readonly errorId: string;
readonly warningId: string;
readonly pendingId: string;
private readonly formControls;
private readonly contentInitialized;
private mutationObserver;
private readonly pendingState;
readonly showPendingMessage: _angular_core.Signal<boolean>;
/**
* aria-describedby value representing the *currently relevant* message regions.
*/
readonly ariaDescribedBy: _angular_core.Signal<string | null>;
private readonly ownedDescribedByIds;
constructor();
ngAfterContentInit(): void;
ngOnDestroy(): void;
private updateFormControls;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormErrorControlDirective, never>;
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<FormErrorControlDirective, "[formErrorControl], [ngxErrorControl]", ["formErrorControl", "ngxErrorControl"], { "ariaAssociationMode": { "alias": "ariaAssociationMode"; "required": false; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof FormErrorDisplayDirective; inputs: { "errorDisplayMode": "errorDisplayMode"; "warningDisplayMode": "warningDisplayMode"; }; outputs: {}; }]>;
}
/**
* Validation Options
*/
type ValidationOptions = {
/**
* debounceTime for the next validation
*/
debounceTime: number;
};
/**
* Hooks into `ngModelGroup`/`ngxModelGroup` and runs async group-level validation
* through the parent `FormDirective` Vest suite bridge.
*/
declare class FormModelGroupDirective implements AsyncValidator {
/**
* Per-group async validation options.
*
* Defaults to no debounce (`{ debounceTime: 0 }`).
*/
validationOptions: _angular_core.InputSignal<ValidationOptions>;
private readonly formDirective;
/**
* Runs async validation for the current model-group control.
*
* Returns `null` (fail-open) when used outside an `ngxVestForm` context.
*/
validate(control: AbstractControl): Observable<ValidationErrors | null>;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormModelGroupDirective, never>;
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<FormModelGroupDirective, "[ngModelGroup],[ngxModelGroup]", never, { "validationOptions": { "alias": "validationOptions"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
}
/**
* Hooks into `ngModel`/`ngxModel` and runs async field-level validation
* through the parent `FormDirective` Vest suite bridge.
*/
declare class FormModelDirective implements AsyncValidator {
/**
* Per-control async validation options.
*
* Defaults to no debounce (`{ debounceTime: 0 }`).
*/
validationOptions: _angular_core.InputSignal<ValidationOptions>;
/**
* Reference to the form that needs to be validated
* Injected optionally so that using ngModel outside of an ngxVestForm
* does not crash the application. In that case, validation becomes a no-op.
*/
private readonly formDirective;
/**
* Runs field-level async validation for this control.
*
* Returns `null` (fail-open) when used outside an `ngxVestForm` context.
*/
validate(control: AbstractControl): Observable<ValidationErrors | null>;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormModelDirective, never>;
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<FormModelDirective, "[ngModel],[ngxModel]", never, { "validationOptions": { "alias": "validationOptions"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
}
declare const ROOT_FORM = "rootForm";
/**
* Primitive types that should not be traversed for nested paths
*/
type Primitive = string | number | boolean | Date | null | undefined;
/**
* Helper type to extract the element type from an array.
* Used internally for type inference with array paths.
*
* @template T - The array type to extract from
* @example
* ```typescript
* type Element = ArrayElement<string[]>; // Result: string
* type Element2 = ArrayElement<NotArray>; // Result: never
* ```
*/
/**
* Recursively generates all valid field paths for a type as string literals.
* This provides full IDE autocomplete and compile-time validation for field names.
*
* **Key Features:**
* - Supports nested objects with dot notation (e.g., 'user.address.city')
* - Works with optional properties from DeepPartial types
* - Handles arrays and readonly arrays
* - Stops recursion at primitive types
* - Maximum depth of 10 levels to prevent infinite recursion
*
* **Type Safety Benefits:**
* - IDE autocomplete for all valid field paths
* - Compile-time errors for typos in field names
* - Refactoring support (rename property → all usages update)
* - Self-documenting code through type inference
*
* @template T - The model type to extract field paths from
* @template Prefix - Internal recursion prefix (do not use directly)
* @template Depth - Internal depth counter to prevent infinite recursion
*
* @example
* ```typescript
* type Model = {
* name: string;
* profile: {
* age: number;
* address: {
* city: string;
* }
* }
* };
*
* type Paths = FieldPath<Model>;
* /// Result: 'name' | 'profile' | 'profile.age' | 'profile.address' | 'profile.address.city'
* ```
*
* @example With DeepPartial
* ```typescript
* type FormModel = DeepPartial<{
* user: {
* email: string;
* phone: string;
* }
* }>;
*
* type Paths = FieldPath<FormModel>;
* /// Result: 'user' | 'user.email' | 'user.phone'
* ```
*/
type FieldPath<T, Prefix extends string = '', Depth extends readonly number[] = []> = Depth['length'] extends 10 ? never : T extends Primitive ? never : T extends ReadonlyArray<infer U> ? FieldPath<U, Prefix, [...Depth, 1]> : {
[K in keyof T & string]: T[K] extends Primitive ? `${Prefix}${K}` : T[K] extends ReadonlyArray<infer U> ? `${Prefix}${K}` | (U extends Primitive ? never : FieldPath<U, `${Prefix}${K}.`, [...Depth, 1]>) : `${Prefix}${K}` | FieldPath<T[K], `${Prefix}${K}.`, [...Depth, 1]>;
}[keyof T & string];
/**
* Type-safe validation configuration map.
* Maps trigger field paths to arrays of dependent field paths that should be revalidated.
*
* **Use Case:**
* Define which fields should trigger validation of other fields when they change.
* For example, when password changes, confirmPassword should be revalidated.
*
* **Type Safety:**
* - All keys must be valid field paths from the model
* - All dependent field paths must be valid
* - IDE autocomplete works for both keys and values
* - Compile-time errors for invalid field references
*
* @template T - The form model type
*
* @example
* ```typescript
* type FormModel = DeepPartial<{
* password: string;
* confirmPassword: string;
* addresses: {
* billing: { city: string; }
* }
* }>;
*
* /// ✅ Type-safe configuration with autocomplete
* const config: ValidationConfigMap<FormModel> = {
* password: ['confirmPassword'],
* 'addresses.billing.city': ['password'],
* };
*
* /// ❌ TypeScript error - invalid field name
* const badConfig: ValidationConfigMap<FormModel> = {
* passwordd: ['confirmPassword'], // Typo caught at compile time
* };
* ```
*/
type ValidationConfigMap<T> = Partial<Record<FieldPath<T>, Array<FieldPath<T>>>>;
/**
* Type-safe field name for use in Vest test() calls and form APIs.
* Combines valid field paths with the special ROOT_FORM constant.
*
* **Use Case:**
* When defining Vest test() calls, use this type for the field parameter
* to get autocomplete and type safety.
*
* **Special Values:**
* - Any valid FieldPath from the model
* - ROOT_FORM constant for form-level validations
*
* @template T - The form model type
*
* @example
* ```typescript
* import { ROOT_FORM } from 'ngx-vest-forms';
*
* type FormModel = DeepPartial<{
* email: string;
* user: { name: string; }
* }>;
*
* export const suite = staticSuite(
* (data: FormModel, field?: FormFieldName<FormModel>) => {
* only(field);
*
* /// ✅ Autocomplete works
* test('email', 'Required', () => {
* enforce(data.email).isNotBlank();
* });
*
* test('user.name', 'Required', () => {
* enforce(data.user?.name).isNotBlank();
* });
*
* /// Form-level validation
* test(ROOT_FORM, 'At least one contact method', () => {
* enforce(data.email || data.user?.name).isTruthy();
* });
* }
* );
* ```
*/
type FormFieldName<T> = FieldPath<T> | typeof ROOT_FORM;
/**
* Helper type to infer the value type at a given field path.
* Useful for creating type-safe utilities that work with field paths.
*
* @template T - The model type
* @template Path - The field path string
*
* @example
* ```typescript
* type Model = {
* user: {
* profile: {
* age: number;
* }
* }
* };
*
* type AgeType = FieldPathValue<Model, 'user.profile.age'>;
* /// Result: number
* ```
*/
type FieldPathValue<T, Path extends string> = Path extends keyof T ? T[Path] : Path extends `${infer K}.${infer Rest}` ? K extends keyof T ? FieldPathValue<NonNullable<T[K]>, Rest> : never : never;
/**
* Utility type to check if a path is valid for a given model.
* Returns the path if valid, never otherwise.
*
* @template T - The model type
* @template Path - The path to validate
*
* @example
* ```typescript
* type Model = { name: string; age: number; };
*
* type Valid = ValidateFieldPath<Model, 'name'>; // 'name'
* type Invalid = ValidateFieldPath<Model, 'invalid'>; // never
* ```
*/
type ValidateFieldPath<T, Path extends string> = Path extends FieldPath<T> ? Path : never;
/**
* Extract all leaf field paths (paths that point to primitive values).
* Useful when you only want paths to actual values, not intermediate objects.
*
* @template T - The model type
*
* @example
* ```typescript
* type Model = {
* user: {
* name: string;
* profile: {
* age: number;
* }
* }
* };
*
* type Leaves = LeafFieldPath<Model>;
* /// Result: 'user.name' | 'user.profile.age'
* /// Note: 'user' and 'user.profile' are excluded (not leaves)
* ```
*/
type LeafFieldPath<T, Prefix extends string = '', Depth extends readonly number[] = []> = Depth['length'] extends 10 ? never : T extends Primitive ? never : T extends ReadonlyArray<infer U> ? LeafFieldPath<U, Prefix, [...Depth, 1]> : {
[K in keyof T & string]: T[K] extends Primitive ? `${Prefix}${K}` : T[K] extends ReadonlyArray<infer U> ? U extends Primitive ? never : LeafFieldPath<U, `${Prefix}${K}.`, [...Depth, 1]> : LeafFieldPath<T[K], `${Prefix}${K}.`, [...Depth, 1]>;
}[keyof T & string];
type NgxFirstInvalidOptions = {
/** Scroll animation behavior (default: `'smooth'`, or `'auto'` when reduced motion is preferred). */
behavior?: ScrollBehavior;
/** Vertical alignment when scrolling (default: `'center'`). */
block?: ScrollLogicalPosition;
/** Horizontal alignment when scrolling (default: `'nearest'`). */
inline?: ScrollLogicalPosition;
/** Whether to focus after scrolling (default: `true`). */
focus?: boolean;
/** Passed to `focus()` when focusing (default: `true`). */
preventScrollOnFocus?: boolean;
/** Opens ancestor `<details>` elements before scroll/focus (default: `true`). */
openCollapsedParents?: boolean;
/** Selector used to locate the first invalid element. */
invalidSelector?: string;
/** Selector used to resolve the best focus target within the invalid element. */
focusSelector?: string;
};
declare const DEFAULT_INVALID_SELECTOR: string;
declare const DEFAULT_FOCUS_SELECTOR: string;
/**
* Represents the state of a form managed by scVestForm directive.
* This is the structure returned by NgxVestFormDirective.formState() or similar.
*/
type NgxFormState<TModel = unknown> = {
/** Whether the form is valid */
valid: boolean;
/** Map of field errors by field path */
errors: Record<string, string[]>;
/** Current form value (includes disabled fields) */
value: TModel | null;
};
/**
* Creates an empty NgxFormState with default values.
*
* This utility is helpful when you need to provide a fallback for components
* that require a non-null NgxFormState, such as when child components
* might not be initialized yet.
*
* @template TModel The type of the form model/value
* @returns A complete NgxFormState object with empty/default values
*
* @example
* ```typescript
* /// In a parent component that displays child form state
* protected readonly formState = computed(() =>
* this.childForm()?.formState() ?? createEmptyFormState()
* );
* ```
*
* @example
* ```typescript
* /// With specific typing
* interface MyFormModel {
* name: string;
* email: string;
* }
*
* const emptyState = createEmptyFormState<MyFormModel>();
* ```
*/
declare function createEmptyFormState<TModel = unknown>(): NgxFormState<TModel>;
/**
* FieldKey<T> gives you autocompletion for known string keys of T,
* but also allows any string (for dynamic/nested/cyclic field names).
*
* **Why use this?**
* - Provides IDE autocomplete for known model properties
* - Still accepts arbitrary strings for nested paths like `'addresses[0].street'`
* - Useful for cyclic dependencies and dynamic field names
*
* **How it works:**
* - Extracts string keys from T: `Extract<keyof T, string>`
* - Unions with open-ended string: `string & {}` (branding trick)
* - Result: autocomplete hints + accepts any string
*
* @template T The model type whose string keys should be suggested
*
* @example
* ```typescript
* interface User {
* name: string;
* email: string;
* age: number;
* }
*
* /// Type: 'name' | 'email' | 'age' | string
* type UserField = NgxFieldKey<User>;
*
* /// IDE suggests 'name', 'email', 'age'
* /// but also accepts 'addresses[0].street'
* const field: UserField = 'email'; // ✅ autocomplete
* const nested: UserField = 'profile.settings.theme'; // ✅ also valid
* ```
*
* Note: Use only string keys to avoid widening to number/symbol when T=any.
*/
type NgxFieldKey<T> = Extract<keyof T, string> | (string & {});
/**
* Represents a Vest validation suite for use with ngx-vest-forms.
*
* @description
* This type wraps Vest's `StaticSuite` with the specific generic parameters
* required for form validation in this library.
*
* **Why use this type?**
* - Simplifies the API by hiding complex generic parameters
* - Ensures type safety for model and field parameters
* - Accepts both string and FormFieldName<T> field parameters (from NgxTypedVestSuite)
* - Provides better template compatibility (no `$any()` casts needed)
* - Makes suite signatures consistent across the codebase
*
* **What it wraps:**
* ```typescript
* StaticSuite<string, string, (model: T, field?: string) => void>
* ```
*
* **Type parameters explained:**
* - First `string`: Field names (property paths like 'email' or 'addresses[0].street')
* - Second `string`: Group names (for organizing tests, e.g., 'step1', 'step2')
* - Third parameter: The validation function signature with bivariance trick
*
* **Field parameter:**
* The field parameter accepts `string | undefined` for:
* - Plain string field names like 'email'
* - Nested paths like 'addresses.billing.street'
* - undefined to run all tests
*
* **Bivariance for template compatibility:**
* The callback type uses a bivariant method parameter trick to make
* `NgxVestSuite<SpecificModel>` assignable to `NgxVestSuite<unknown>` in
* Angular templates without requiring `$any()` casts. This preserves template
* ergonomics while keeping implementation sites strictly typed.
*
* **Safety:**
* The bivariant callback is only used for the suite's parameter type. The return
* type remains fully typed. The model parameter remains strictly typed.
*
* @template T The model type that the validation suite operates on (default: unknown)
*
* @example
* ```typescript
* import { NgxDeepPartial, NgxVestSuite } from 'ngx-vest-forms';
* import { staticSuite, test, enforce, only } from 'vest';
*
* type UserModel = NgxDeepPartial<{
* name: string;
* email: string;
* age: number;
* }>;
*
* /// Create validation suite
* export const userValidation: NgxVestSuite<UserModel> = staticSuite((model, field?) => {
* only(field); // Always call unconditionally
*
* test('name', 'Name is required', () => {
* enforce(model.name).isNotBlank();
* });
*
* test('email', 'Valid email is required', () => {
* enforce(model.email).isNotBlank().isEmail();
* });
*
* test('age', 'Must be 18 or older', () => {
* enforce(model.age).greaterThanOrEquals(18);
* });
* });
*
* /// Use in component
* @Component({...})
* class UserFormComponent {
* protected readonly suite = userValidation;
* protected readonly formValue = signal<UserModel>({});
* }
* ```
*
* @example
* ```typescript
* /// For dynamic/untyped scenarios, use unknown
* const dynamicSuite: NgxVestSuite = create((model, field) => {
* only(field);
* /// Validation logic for any model structure
* });
* ```
*
* **Type design notes:**
* - Default generic is `unknown` (safer than `any`) for opt-in typing
* - Uses bivariant method parameter trick for better template assignability
* - Field parameter accepts any string for nested/dynamic paths
* - Return type is fully typed (void) to catch errors at call sites
*
* @see {@link https://vestjs.dev/docs/writing_your_suite | Vest Documentation}
* @see {@link NgxFieldKey} For typed field name hints with arbitrary string support
*/
/** @internal Do not use outside ngx-vest-forms. The `any` is architecturally required. */
type NgxSuiteCallback<T> = {
bivarianceHack(model: T, field?: any): void;
}['bivarianceHack'];
type NgxVestSuite<T = unknown> = StaticSuite<string, string, NgxSuiteCallback<T>>;
/**
* Type-safe validation suite with autocomplete for field paths.
* Use this when defining validation suites to get IDE autocomplete for field names.
*
* **Recommended Pattern:**
* Always define validation suites with `NgxTypedVestSuite<T>` and let TypeScript infer the type in components:
*
* ```typescript
* import { NgxTypedVestSuite, FormFieldName } from 'ngx-vest-forms';
*
* /// ✅ RECOMMENDED: Define with NgxTypedVestSuite for autocomplete
* export const userSuite: NgxTypedVestSuite<UserModel> = staticSuite(
* (model: UserModel, field?: FormFieldName<UserModel>) => {
* only(field);
* /// ✅ IDE autocomplete for: 'email' | 'password' | 'profile.age' | typeof ROOT_FORM
* test('email', 'Required', () => enforce(model.email).isNotBlank());
* }
* );
*
* /// ✅ In component: Use type inference (no explicit type)
* @Component({...})
* class MyFormComponent {
* protected readonly suite = userSuite; // ✅ Type inferred automatically
* protected readonly formValue = signal<UserModel>({});
* }
* ```
*
* **Why this pattern?**
* - **Strong typing at definition**: `FormFieldName<T>` gives autocomplete for all field paths
* - **Type inference in components**: No need for explicit types, avoids compatibility issues
* - **Best of both worlds**: Type safety where you write validation logic, convenience in components
*
* @template T The model type that the validation suite operates on
*
* @see {@link NgxVestSuite} For the base suite type (accepts any string field)
* @see {@link FormFieldName} For the field name type with autocomplete
*/
/** @internal Do not use outside ngx-vest-forms. */
type NgxTypedSuiteCallback<T> = {
bivarianceHack(model: T, field?: FormFieldName<T>): void;
}['bivarianceHack'];
type NgxTypedVestSuite<T> = StaticSuite<string, string, NgxTypedSuiteCallback<T>>;
/**
* Type for validation configuration that accepts both the typed and untyped versions.
* This ensures backward compatibility while supporting the new typed API.
*/
type NgxValidationConfig<T = unknown> = Record<string, string[]> | ValidationConfigMap<T> | null;
/**
* Payload emitted when a named control inside the form loses focus.
*
* This is intentionally low-level so app code can build workflows such as
* draft auto-save, analytics, or blur-driven side effects without the form
* library taking ownership of persistence behavior.
*
* It is not intended as a blur-time workaround for dependent field validation;
* for that pattern, prefer `validationConfig` plus each target field's own
* `errorDisplayMode`.
*
* The emitted `field` is the full dotted control path (e.g.
* `passwords.confirm`, `businessHours.values.0.from`) regardless of whether
* the surrounding groups use static `ngModelGroup="key"` or dynamic
* `[ngModelGroup]="expr"` bindings, because the path is read from the live
* `NgModel` directive registered with the form.
*
* @publicApi
*/
type NgxFieldBlurEvent<T = unknown> = {
field: string;
value: unknown;
formValue: T | null;
dirty: boolean;
touched: boolean;
valid: boolean;
pending: boolean;
};
/**
* Main form directive for ngx-vest-forms that bridges Angular template-driven forms with Vest.js validation.
*
* This directive provides:
* - **Unidirectional data flow**: Use `[ngModel]` (not `[(ngModel)]`) with `(formValueChange)` for predictable state updates
* - **Vest.js integration**: Automatic async validators from Vest suites with field-level optimization
* - **Validation dependencies**: Configure cross-field validation triggers via `validationConfig`
* - **Form state**: Access validity, errors, and values through the `formState` signal
*
* @usageNotes
*
* ### Basic Usage
* ```html
* <form ngxVestForm [suite]="validationSuite" (formValueChange)="formValue.set($event)">
* <input name="email" [ngModel]="formValue().email" />
* </form>
* ```
*
* ### With Validation Dependencies
* ```html
* <form ngxVestForm [suite]="suite" [validationConfig]="validationConfig">
* <input name="password" [ngModel]="formValue().password" />
* <input name="confirmPassword" [ngModel]="formValue().confirmPassword" />
* </form>
* ```
* ```typescript
* validationConfig = { 'password': ['confirmPassword'] };
* ```
*
* ### Accessing Form State
* ```typescript
* vestForm = viewChild.required('vestForm', { read: FormDirective });
* isValid = computed(() => this.vestForm().formState().valid);
* ```
*
* @see {@link https://github.com/ngx-vest-forms/ngx-vest-forms} for full documentation
* @publicApi
*/
declare class FormDirective<T extends Record<string, unknown>> {
#private;
readonly ngForm: NgForm;
private readonly elementRef;
private readonly destroyRef;
private readonly cdr;
private readonly configDebounceTime;
/**
* Public signal storing field warnings keyed by field path.
* This allows warnings to be stored and displayed without affecting field validity.
* Angular's control.errors !== null marks a field as invalid, so we store warnings
* separately when they exist without errors.
*/
readonly fieldWarnings: _angular_core.WritableSignal<Map<string, readonly string[]>>;
/**
* Computed signal that returns field paths for all touched (or submitted) leaf controls.
* Updates reactively when controls are touched (blur) or when form status changes.
*
* This enables consumers to determine which fields the user has interacted with,
* useful for filtering errors/warnings to match the form's visible validation state.
*
* @publicApi
*/
readonly touchedFieldPaths: _angular_core.Signal<string[]>;
/**
* Computed signal for form state with validity and errors.
* Used by templates and tests as vestForm.formState().valid/errors
*
* Uses custom equality function to prevent unnecessary recalculations
* when form status changes but actual values/errors remain the same.
*/
readonly formState: _angular_core.Signal<NgxFormState<T>>;
/**
* The value of the form, this is needed for the validation part.
* Using input() here because two-way binding is provided via formValueChange output.
* In the minimal core directive (form-core.directive.ts), this would be model() instead.
*/
readonly formValue: InputSignal<T | null>;
/**
* Static vest suite that will be used to feed our angular validators.
* Accepts both NgxVestSuite and NgxTypedVestSuite through compatible type signatures.
* NgxTypedVestSuite<T> is assignable to NgxVestSuite<T> due to bivariance and
* FormFieldName<T> (string literal union) being assignable to string.
*/
readonly suite: InputSignal<NgxVestSuite<T> | NgxTypedVestSuite<T> | null>;
/**
* The shape of our form model. This is a deep required version of the form model
* The goal is to add default values to the shape so when the template-driven form
* contains values that shouldn't be there (typo's) that the developer gets run-time
* errors in dev mode
*/
readonly formShape: InputSignal<ngx_vest_forms.NgxDeepRequired<T> | null>;
/**
* Updates the validation config which is a dynamic object that will be used to
* trigger validations on the dependant fields
* Eg: ```typescript
* validationConfig = {
* 'passwords.password': ['passwords.confirmPassword']
* }
* ```
*
* This will trigger the updateValueAndValidity on passwords.confirmPassword every time the passwords.password gets a new value
*
* @param v
*/
readonly validationConfig: InputSignal<NgxValidationConfig<T>>;
/**
* Emits whenever validation feedback may have changed, even if the aggregate
* root form status string stays the same.
*/
private readonly validationFeedback$;
private readonly pending$;
/**
* Emits every time the form status changes in a state
* that is not PENDING
* We need this to assure that the form is in 'idle' state
*/
readonly idle$: Observable<"VALID" | "INVALID" | "DISABLED">;
/**
* Triggered as soon as the form value changes
* It also contains the disabled values (raw values)
*
* Cleanup is handled automatically by the directive when it's destroyed.
*/
readonly formValueChange: _angular_core.OutputRef<T>;
/**
* Emits an object with all the errors of the form
* every time a form control or form groups changes its status to valid or invalid
*
* For submit events, waits for async validation (including ROOT_FORM) to complete
* before emitting errors. This ensures ROOT_FORM errors are included in the output.
*
* Cleanup is handled automatically by the directive when it's destroyed.
*/
readonly errorsChange: _angular_core.OutputRef<Record<string, string[]>>;
/**
* Triggered as soon as the form becomes dirty
*
* Cleanup is handled automatically by the directive when it's destroyed.
*/
readonly dirtyChange: _angular_core.OutputRef<boolean>;
/**
* Fired when the status of the root form changes.
*/
private readonly statusChanges$;
/**
* Triggered When the form becomes valid but waits until the form is idle
*
* Cleanup is handled automatically by the directive when it's destroyed.
*/
readonly validChange: _angular_core.OutputRef<boolean>;
/**
* Emits when a named control inside the form loses focus.
*
* Useful for application-level workflows such as draft auto-save on blur.
*/
readonly fieldBlur: _angular_core.OutputEmitterRef<NgxFieldBlurEvent<T>>;
/**
* Track validation in progress to prevent circular triggering (Issue #19)
*/
private readonly validationInProgress;
constructor();
/**
* Manually trigger form validation update.