UNPKG

ngx-vest-forms

Version:

Opinionated template-driven forms library for Angular with Vest.js integration

1,192 lines (1,175 loc) 114 kB
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.