ngx-vest-forms
Version:
Opinionated template-driven forms library for Angular with Vest.js integration
1 lines • 330 kB
Source Map (JSON)
{"version":3,"file":"ngx-vest-forms.mjs","sources":["../../../projects/ngx-vest-forms/src/lib/directives/error-display-mode.token.ts","../../../projects/ngx-vest-forms/src/lib/errors/error-catalog.ts","../../../projects/ngx-vest-forms/src/lib/tokens/debounce.token.ts","../../../projects/ngx-vest-forms/src/lib/utils/equality.ts","../../../projects/ngx-vest-forms/src/lib/tokens/equality.token.ts","../../../projects/ngx-vest-forms/src/lib/utils/destroy-scheduler.ts","../../../projects/ngx-vest-forms/src/lib/utils/field-path.utils.ts","../../../projects/ngx-vest-forms/src/lib/utils/first-invalid.utils.ts","../../../projects/ngx-vest-forms/src/lib/constants.ts","../../../projects/ngx-vest-forms/src/lib/utils/form-utils.ts","../../../projects/ngx-vest-forms/src/lib/utils/shape-validation.ts","../../../projects/ngx-vest-forms/src/lib/directives/form-submitted-state.ts","../../../projects/ngx-vest-forms/src/lib/directives/form.directive.ts","../../../projects/ngx-vest-forms/src/lib/directives/form-control-state.directive.ts","../../../projects/ngx-vest-forms/src/lib/directives/form-error-display.directive.ts","../../../projects/ngx-vest-forms/src/lib/utils/aria-association.utils.ts","../../../projects/ngx-vest-forms/src/lib/utils/pending-state.utils.ts","../../../projects/ngx-vest-forms/src/lib/components/control-wrapper/control-wrapper.component.ts","../../../projects/ngx-vest-forms/src/lib/components/control-wrapper/control-wrapper.component.html","../../../projects/ngx-vest-forms/src/lib/components/form-group-wrapper/form-group-wrapper.component.ts","../../../projects/ngx-vest-forms/src/lib/components/form-group-wrapper/form-group-wrapper.component.html","../../../projects/ngx-vest-forms/src/lib/directives/form-error-control.directive.ts","../../../projects/ngx-vest-forms/src/lib/directives/async-validator-bridge.ts","../../../projects/ngx-vest-forms/src/lib/directives/form-model-group.directive.ts","../../../projects/ngx-vest-forms/src/lib/directives/form-model.directive.ts","../../../projects/ngx-vest-forms/src/lib/directives/validate-root-form.directive.ts","../../../projects/ngx-vest-forms/src/lib/exports.ts","../../../projects/ngx-vest-forms/src/lib/utils/field-path-types.ts","../../../projects/ngx-vest-forms/src/lib/utils/form-state.utils.ts","../../../projects/ngx-vest-forms/src/lib/utils/validation-config-builder.ts","../../../projects/ngx-vest-forms/src/lib/utils/array-to-object.ts","../../../projects/ngx-vest-forms/src/lib/utils/field-clearing.ts","../../../projects/ngx-vest-forms/src/public-api.ts","../../../projects/ngx-vest-forms/src/ngx-vest-forms.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport {\n NgxWarningDisplayMode,\n ScErrorDisplayMode,\n} from './form-error-display.directive';\n\n/**\n * @deprecated Use NGX_ERROR_DISPLAY_MODE_TOKEN instead\n */\nexport const SC_ERROR_DISPLAY_MODE_TOKEN =\n new InjectionToken<ScErrorDisplayMode>('SC_ERROR_DISPLAY_MODE_TOKEN', {\n providedIn: 'root',\n factory: () => 'on-blur-or-submit',\n });\n\n/**\n * Injection token for configuring the default error display mode.\n * Values:\n * - 'on-blur': Show errors after field is touched/blurred\n * - 'on-submit': Show errors after form submission\n * - 'on-blur-or-submit': Show errors after blur or form submission (default)\n * - 'on-dirty': Show errors as soon as the field value changes\n * - 'always': Show errors immediately, even on pristine fields\n */\nexport const NGX_ERROR_DISPLAY_MODE_TOKEN =\n new InjectionToken<ScErrorDisplayMode>('NGX_ERROR_DISPLAY_MODE_TOKEN', {\n providedIn: 'root',\n factory: () => 'on-blur-or-submit',\n });\n\n/**\n * Injection token for configuring the default warning display mode.\n * Values:\n * - 'on-touch': Show warnings after field is touched/blurred\n * - 'on-validated-or-touch': Show warnings after validation runs or field is touched (default)\n * - 'on-dirty': Show warnings as soon as the field value changes\n * - 'always': Show warnings immediately, even on pristine fields\n */\nexport const NGX_WARNING_DISPLAY_MODE_TOKEN =\n new InjectionToken<NgxWarningDisplayMode>('NGX_WARNING_DISPLAY_MODE_TOKEN', {\n providedIn: 'root',\n factory: () => 'on-validated-or-touch',\n });\n","export const NGX_VEST_FORMS_ERRORS = {\n EXTRA_PROPERTY: {\n code: 'NGX-001',\n message: (path: string) =>\n `Shape mismatch: Property '${path}' is present in the form value but not defined in the form shape.`,\n },\n TYPE_MISMATCH: {\n code: 'NGX-002',\n message: (path: string, expected: string, actual: string) =>\n `Type mismatch at '${path}': Expected '${expected}' but got '${actual}'.`,\n },\n CONTROL_NOT_FOUND: {\n code: 'NGX-003',\n message: (path: string) =>\n `Control not found: Could not find form control at path '${path}'. Check your [ngModel] name attributes.`,\n },\n} as const;\n\nexport function logWarning<T extends unknown[]>(\n error: { code: string; message: (...args: T) => string },\n ...args: T\n): void {\n console.warn(\n `[${error.code}] ${error.message(...args)}\\nCheck your [formShape] input and the initial [formValue].`\n );\n}\n","import { InjectionToken } from '@angular/core';\n\n/**\n * Injection token for configurable validation config debounce timing.\n *\n * This token allows you to configure the debounce time for validation config\n * dependencies at the application, route, or component level.\n *\n * @example\n * ```typescript\n * /// Global configuration\n * export const appConfig: ApplicationConfig = {\n * providers: [\n * {\n * provide: NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN,\n * useValue: 200\n * }\n * ]\n * };\n *\n * /// Per-route configuration\n * {\n * path: 'checkout',\n * component: CheckoutComponent,\n * providers: [\n * {\n * provide: NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN,\n * useValue: 50\n * }\n * ]\n * }\n *\n * /// Per-component override\n * @Component({\n * providers: [\n * {\n * provide: NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN,\n * useValue: 0 // for testing\n * }\n * ]\n * })\n * export class TestFormComponent {}\n * ```\n *\n * @default 100ms - Maintains backward compatibility with existing behavior\n */\nexport const NGX_VALIDATION_CONFIG_DEBOUNCE_TOKEN = new InjectionToken<number>(\n 'NgxValidationConfigDebounceTime',\n {\n providedIn: 'root',\n factory: () => 100,\n }\n);\n","/**\n * Type guard to check if a value is a primitive type.\n *\n * @param value - The value to check\n * @returns true if the value is a primitive (string, number, boolean, null, undefined, symbol, bigint)\n */\nexport function isPrimitive(\n value: unknown\n): value is string | number | boolean | null | undefined | symbol | bigint {\n return (\n value === null || (typeof value !== 'object' && typeof value !== 'function')\n );\n}\n\ntype VisitedPairs = WeakMap<object, WeakSet<object>>;\n\n/**\n * Records that we've started comparing the pair (a, b). Re-entering the same\n * pair during recursion (cycle, or repeated DAG path) short-circuits to `true`\n * — correct because if the prior descent had returned `false`, the outer call\n * would already have short-circuited before we reach the second visit.\n */\nfunction rememberPair(seen: VisitedPairs, a: object, b: object): void {\n let targets = seen.get(a);\n if (!targets) {\n targets = new WeakSet<object>();\n seen.set(a, targets);\n }\n targets.add(b);\n}\n\n/**\n * @internal\n * Internal utility for shallow equality checks.\n *\n * **Not intended for external use.** This function is used internally by the library\n * for performance-critical operations. Consider using your own comparison logic or\n * a library like lodash if you need shallow equality checks in your application.\n *\n * Optimized shallow equality check for objects.\n *\n * **Why this custom implementation is preferred:**\n * - **Performance**: Direct property comparison is significantly faster than JSON.stringify\n * - **Type Safety**: Handles null/undefined values correctly without serialization issues\n * - **Accuracy**: Doesn't suffer from JSON.stringify limitations (undefined values, functions, symbols)\n * - **Memory Efficient**: No temporary string creation or object serialization overhead\n *\n * **Use Cases:**\n * - Form value change detection where only top-level properties matter\n * - Quick object comparison in performance-critical code paths\n * - Validation triggers where deep comparison is unnecessary\n *\n * **Performance Comparison:**\n * ```typescript\n * /// ❌ Slow: JSON.stringify approach\n * JSON.stringify(obj1) === JSON.stringify(obj2)\n *\n * /// ✅ Fast: Direct property comparison\n * shallowEqual(obj1, obj2)\n * ```\n *\n * @param obj1 - First object to compare\n * @param obj2 - Second object to compare\n * @returns true if objects are shallowly equal (same keys and same values by reference)\n */\nexport function shallowEqual(obj1: unknown, obj2: unknown): boolean {\n if (obj1 === obj2) {\n return true;\n }\n\n if (obj1 == null || obj2 == null) {\n return obj1 === obj2;\n }\n\n if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {\n return obj1 === obj2;\n }\n\n const keys1 = Object.keys(obj1);\n const keys2 = Object.keys(obj2);\n\n if (keys1.length !== keys2.length) {\n return false;\n }\n\n for (const key of keys1) {\n if (\n !Object.hasOwn(obj2 as object, key) ||\n (obj1 as Record<string, unknown>)[key] !==\n (obj2 as Record<string, unknown>)[key]\n ) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * @internal\n * Internal utility for deep equality checks.\n *\n * **Not intended for external use.** This function is used internally by the library\n * for form value comparison and change detection. Consider using your own comparison\n * logic or a library like lodash if you need deep equality checks in your application.\n *\n * Fast deep equality check optimized for form values and Angular applications.\n *\n * **Why this custom implementation is preferred over alternatives:**\n *\n * **vs JSON.stringify():**\n * - **10-100x faster**: Direct comparison without string serialization overhead\n * - **Accurate**: Handles Date objects, RegExp, undefined values, and functions correctly\n * - **Memory efficient**: No temporary string creation or garbage collection pressure\n * - **Preserves semantics**: Maintains type information during comparison\n *\n * **vs structuredClone():**\n * - **Wrong purpose**: structuredClone creates copies, not comparisons\n * - **Performance**: Would require cloning both objects just to compare them\n * - **Memory waste**: Creates unnecessary deep copies, doubling memory usage\n * - **Still incomplete**: Even after cloning, you'd still need a comparison function\n *\n * **vs External libraries (lodash.isEqual, etc.):**\n * - **Bundle size**: Zero dependencies, smaller application bundles\n * - **Form-specific**: Optimized for common Angular form data patterns\n * - **Type safety**: Full TypeScript integration with strict typing\n * - **Performance**: Tailored algorithms for form value comparison use cases\n *\n * **Supported Data Types:**\n * - Primitives (string, number, boolean, null, undefined, symbol, bigint)\n * - Arrays (with recursive deep comparison)\n * - Plain objects (with recursive deep comparison)\n * - Date objects (by timestamp comparison)\n * - RegExp objects (by source and flags comparison)\n * - Set objects (reference equality only)\n * - Map objects (reference equality only)\n * - Functions (reference equality only — distinct function instances are never equal,\n * even if their source code is identical)\n *\n * **Safety Features:**\n * - **Circular reference handling**: Tracks visited object pairs with `WeakMap<object, WeakSet<object>>`\n * - **Type coercion prevention**: Strict type checking before comparison\n * - **Null safety**: Proper handling of null and undefined values\n *\n * **Performance Characteristics:**\n * ```typescript\n * /// Performance comparison on typical form objects:\n * /// JSON.stringify: ~100ms for complex nested forms\n * /// fastDeepEqual: ~1-5ms for the same objects\n * ///\n * /// Memory usage:\n * /// JSON.stringify: Creates temporary strings (high GC pressure)\n * /// fastDeepEqual: One small WeakMap of visited object pairs is allocated lazily on\n * /// first nested-container descent; primitive-only comparisons allocate nothing.\n * ```\n *\n * **Typical Usage in Forms:**\n * ```typescript\n * /// Detect when form values actually change\n * distinctUntilChanged(fastDeepEqual)\n *\n * /// Prevent unnecessary re-renders\n * if (!fastDeepEqual(oldFormValue, newFormValue)) {\n * updateUI();\n * }\n * ```\n *\n * @param obj1 - First object to compare\n * @param obj2 - Second object to compare\n *\n * Cyclic arrays and plain objects are compared structurally by tracking visited object\n * pairs. Distinct cyclic graphs with the same structure compare equal. `Date` and\n * `RegExp` values compare structurally. `Map` and `Set` values compare by reference\n * only, so distinct instances are considered different even if their contents match.\n *\n * @returns true if objects are deeply equal by value\n */\nexport function fastDeepEqual(obj1: unknown, obj2: unknown): boolean {\n return fastDeepEqualInternal(obj1, obj2, undefined);\n}\n\nfunction fastDeepEqualInternal(\n obj1: unknown,\n obj2: unknown,\n seen: VisitedPairs | undefined\n): boolean {\n // Object.is gives correct semantics for NaN and ±0 — important for numeric\n // form values where `fastDeepEqual(NaN, NaN)` should be true.\n if (Object.is(obj1, obj2)) {\n return true;\n }\n\n if (obj1 == null || obj2 == null) {\n return false;\n }\n\n if (typeof obj1 !== typeof obj2) {\n return false;\n }\n\n // Functions use reference-only equality. Object.is at the top already returned\n // true for identical references, so reaching here means the references differ.\n if (typeof obj1 === 'function') {\n return false;\n }\n\n if (isPrimitive(obj1) || isPrimitive(obj2)) {\n return false;\n }\n\n // Handle Date / RegExp first — they have value semantics and can't contain cycles,\n // so they don't need pair tracking.\n if (obj1 instanceof Date || obj2 instanceof Date) {\n return (\n obj1 instanceof Date &&\n obj2 instanceof Date &&\n obj1.getTime() === obj2.getTime()\n );\n }\n\n if (obj1 instanceof RegExp || obj2 instanceof RegExp) {\n return (\n obj1 instanceof RegExp &&\n obj2 instanceof RegExp &&\n obj1.source === obj2.source &&\n obj1.flags === obj2.flags\n );\n }\n\n // Intentional contract: distinct Set / Map instances compare by reference only.\n if (obj1 instanceof Set || obj2 instanceof Set) {\n return false;\n }\n if (obj1 instanceof Map || obj2 instanceof Map) {\n return false;\n }\n\n if (Array.isArray(obj1) !== Array.isArray(obj2)) {\n return false;\n }\n\n // Cycle / repeated-pair guard for traversable containers.\n // Allocates lazily on first nested-object descent.\n const a = obj1 as object;\n const b = obj2 as object;\n if (seen?.get(a)?.has(b)) {\n return true;\n }\n seen ??= new WeakMap<object, WeakSet<object>>();\n rememberPair(seen, a, b);\n\n if (Array.isArray(obj1)) {\n const arr2 = obj2 as unknown[];\n if (obj1.length !== arr2.length) {\n return false;\n }\n for (let i = 0; i < obj1.length; i++) {\n if (!fastDeepEqualInternal(obj1[i], arr2[i], seen)) {\n return false;\n }\n }\n return true;\n }\n\n // Plain objects\n const keys1 = Object.keys(a);\n const o2 = b as Record<string, unknown>;\n if (keys1.length !== Object.keys(o2).length) {\n return false;\n }\n for (const key of keys1) {\n if (!Object.hasOwn(o2, key)) {\n return false;\n }\n if (\n !fastDeepEqualInternal(\n (a as Record<string, unknown>)[key],\n o2[key],\n seen\n )\n ) {\n return false;\n }\n }\n return true;\n}\n","import { InjectionToken } from '@angular/core';\nimport { fastDeepEqual } from '../utils/equality';\n\n/**\n * Signature of a deep-equality comparator. Must return `true` iff `a` and `b`\n * are considered equal for the purpose of change detection.\n */\nexport type NgxEqualityFn = (a: unknown, b: unknown) => boolean;\n\n/**\n * Injection token for the deep-equality function used internally by\n * {@link FormDirective} for change detection — `formValueChange`\n * `distinctUntilChanged`, the form↔model two-way sync effect, and the\n * `formState` signal's structural comparator.\n *\n * The default factory returns {@link fastDeepEqual}. Override this token to\n * plug in a smaller or differently-tuned comparator (e.g. `dequal/lite`,\n * `lodash.isEqual`) without forking the directive.\n *\n * @example Bring-your-own equality at the application level\n * ```ts\n * import { dequal } from 'dequal/lite';\n * import { NGX_EQUALITY_FN } from 'ngx-vest-forms';\n *\n * export const appConfig: ApplicationConfig = {\n * providers: [\n * { provide: NGX_EQUALITY_FN, useValue: dequal },\n * ],\n * };\n * ```\n *\n * @example Per-component override (e.g. for tests)\n * ```ts\n * @Component({\n * providers: [\n * { provide: NGX_EQUALITY_FN, useValue: (a, b) => a === b },\n * ],\n * })\n * export class TestFormComponent {}\n * ```\n *\n * @default {@link fastDeepEqual}\n */\nexport const NGX_EQUALITY_FN = new InjectionToken<NgxEqualityFn>(\n 'NgxEqualityFn',\n {\n providedIn: 'root',\n factory: () => fastDeepEqual,\n }\n);\n","import { DestroyRef } from '@angular/core';\n\n/**\n * Schedules a callback to run after a delay, automatically cancelling it if the\n * provided `DestroyRef` fires before the timer expires.\n *\n * @param callback - Function to invoke after the delay.\n * @param delayMs - Delay in milliseconds (passed to `setTimeout`).\n * @param destroyRef - Angular `DestroyRef` to register auto-cancellation against.\n * @returns A cancel function; calling it before the timer fires prevents the\n * callback from running and unregisters the destroy listener.\n */\nexport function scheduleTimeout(\n callback: () => void,\n delayMs: number,\n destroyRef: DestroyRef\n): () => void {\n let cancelled = false;\n let unregisterCalled = false;\n // eslint-disable-next-line prefer-const -- assigned after onDestroy returns\n let unregisterDestroy: (() => void) | undefined;\n\n // Idempotent unregister wrapper so all paths (timer-fires, explicit-cancel,\n // and onDestroy) can call it safely without double-removal.\n const safeUnregister = () => {\n if (unregisterCalled) return;\n unregisterCalled = true;\n unregisterDestroy?.();\n };\n\n const handle = setTimeout(() => {\n safeUnregister();\n if (!cancelled) {\n callback();\n }\n }, delayMs);\n\n unregisterDestroy = destroyRef.onDestroy(() => {\n cancelled = true;\n clearTimeout(handle);\n safeUnregister();\n });\n\n return () => {\n if (!cancelled) {\n cancelled = true;\n clearTimeout(handle);\n safeUnregister();\n }\n };\n}\n\n/**\n * Schedules a microtask callback, automatically suppressing it if the provided\n * `DestroyRef` fires before the microtask runs.\n *\n * Since `queueMicrotask` has no native cancellation API, this relies on a\n * destroyed flag that is set inside the registered `onDestroy` hook.\n *\n * @param callback - Function to invoke in the next microtask checkpoint.\n * @param destroyRef - Angular `DestroyRef` to register auto-cancellation against.\n * @returns A cancel function; calling it before the microtask runs prevents the\n * callback from executing and unregisters the destroy listener.\n */\nexport function scheduleMicrotask(\n callback: () => void,\n destroyRef: DestroyRef\n): () => void {\n let cancelled = false;\n let unregisterCalled = false;\n\n // Register the onDestroy listener before queuing the microtask so that\n // `unregisterDestroy` is always defined when the microtask fires.\n const unregisterDestroy = destroyRef.onDestroy(() => {\n cancelled = true;\n safeUnregister();\n });\n\n // Idempotent unregister wrapper so all paths (microtask-fires, explicit-cancel,\n // and onDestroy) can call it safely without double-removal.\n function safeUnregister() {\n if (unregisterCalled) return;\n unregisterCalled = true;\n unregisterDestroy();\n }\n\n queueMicrotask(() => {\n // Always clean up the destroy listener when the microtask fires,\n // regardless of whether the callback is suppressed.\n safeUnregister();\n if (!cancelled) {\n callback();\n }\n });\n\n return () => {\n if (!cancelled) {\n cancelled = true;\n safeUnregister();\n }\n };\n}\n","/**\n * Utilities for working with field paths in dot/bracket notation and Standard Schema path arrays.\n *\n * These utilities help convert between:\n * - **Dot/bracket notation**: `'addresses[0].street'` (used in Angular forms and Vest)\n * - **Standard Schema paths**: `['addresses', 0, 'street']` (used in schema validation)\n *\n * @example\n * ```typescript\n * // Parse string path to array\n * parseFieldPath('user.addresses[0].street')\n * // Returns: ['user', 'addresses', 0, 'street']\n *\n * // Stringify array path to string\n * stringifyFieldPath(['user', 'addresses', 0, 'street'])\n * // Returns: 'user.addresses[0].street'\n * ```\n */\n\nconst UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);\nconst LOG_PREFIX = '[ngx-vest-forms] field-path.utils';\n\n/**\n * @internal\n * Returns whether a path segment is unsafe for object writes/merges.\n *\n * Unsafe segments are blocked to prevent prototype pollution vectors.\n */\nexport function isUnsafePathSegment(segment: string | number): boolean {\n return typeof segment === 'string' && UNSAFE_PATH_SEGMENTS.has(segment);\n}\n\n/**\n * @internal\n * Internal utility for parsing field path strings.\n *\n * **Not intended for external use.** While this function can parse field paths,\n * it's primarily used internally. Most users won't need to parse field paths manually.\n * If you do need this functionality, consider using your own implementation tailored\n * to your specific needs.\n *\n * Converts a dot/bracket notation path (e.g. `'addresses[0].street'`)\n * to a Standard Schema path array (e.g. `['addresses', 0, 'street']`).\n *\n * **Use cases:**\n * - Converting Angular form paths to schema-compatible arrays\n * - Parsing Vest field names for array access\n * - Processing validation error paths from different sources\n *\n * **Supported formats:**\n * - Dot notation: `'user.name'` → `['user', 'name']`\n * - Bracket notation: `'addresses[0]'` → `['addresses', 0]`\n * - Mixed: `'user.addresses[0].street'` → `['user', 'addresses', 0, 'street']`\n *\n * @param path - The dot/bracket notation path string\n * @returns Array of path segments (strings for properties, numbers for array indices)\n *\n * @example\n * ```typescript\n * parseFieldPath('email') // ['email']\n * parseFieldPath('user.profile.name') // ['user', 'profile', 'name']\n * parseFieldPath('items[0]') // ['items', 0]\n * parseFieldPath('users[0].addresses[1].street')\n * // ['users', 0, 'addresses', 1, 'street']\n * ```\n */\nexport function parseFieldPath(path: string): Array<string | number> {\n if (!path) return [];\n\n // Normalize bracket notation to dot notation first so malformed inputs like\n // 'a.[0]' (which becomes 'a..0') are caught alongside 'a..b', '.a', 'a.', '.'.\n const startsWithBracket = path.startsWith('[');\n const segments = path\n .replaceAll(/\\[(\\d+)\\]/g, '.$1')\n .split('.');\n\n // Empty segments after normalization signal a malformed path. The single\n // legitimate case is a leading empty produced by a path that originally\n // started with '[' (e.g. '[0].x' → '.0.x' → ['', '0', 'x']).\n for (let i = 0; i < segments.length; i++) {\n if (segments[i] === '' && !(i === 0 && startsWithBracket)) {\n if (typeof ngDevMode !== 'undefined' && ngDevMode) {\n console.warn(\n `${LOG_PREFIX}: Invalid field path '${path}'. Leading dots, trailing dots, consecutive dots, and '.[' separators are not allowed.`\n );\n }\n return [];\n }\n }\n\n return segments\n .filter((part) => part !== '')\n .map((part) => (/^\\d+$/.test(part) ? Number(part) : part));\n}\n\n/**\n * Converts a Standard Schema path array (e.g. `['addresses', 0, 'street']`)\n * to a dot/bracket notation string (e.g. `'addresses[0].street'`).\n *\n * **Use cases:**\n * - Converting schema validation paths to Angular form field names\n * - Generating Vest field names from path arrays\n * - Creating human-readable field identifiers\n *\n * **Format rules:**\n * - String segments joined with dots: `['user', 'name']` → `'user.name'`\n * - Number segments use brackets: `['items', 0]` → `'items[0]'`\n * - Mixed paths combine both: `['users', 0, 'email']` → `'users[0].email'`\n *\n * @param path - Array of path segments (strings for properties, numbers for indices)\n * @returns Dot/bracket notation path string\n *\n * @example\n * ```typescript\n * stringifyFieldPath(['email']) // 'email'\n * stringifyFieldPath(['user', 'profile', 'name']) // 'user.profile.name'\n * stringifyFieldPath(['items', 0]) // 'items[0]'\n * stringifyFieldPath(['users', 0, 'addresses', 1, 'street'])\n * // 'users[0].addresses[1].street'\n * ```\n */\nexport function stringifyFieldPath(path: Array<string | number>): string {\n if (!path || path.length === 0) return '';\n\n let result = '';\n\n for (let i = 0; i < path.length; i++) {\n const segment = path[i];\n\n if (typeof segment === 'number') {\n // Array index - add as bracket notation\n result += `[${segment}]`;\n } else {\n // Property name - add with dot prefix if not first segment\n if (i > 0) result += '.';\n result += segment;\n }\n }\n\n return result;\n}\n","export type NgxFirstInvalidOptions = {\n /** Scroll animation behavior (default: `'smooth'`, or `'auto'` when reduced motion is preferred). */\n behavior?: ScrollBehavior;\n /** Vertical alignment when scrolling (default: `'center'`). */\n block?: ScrollLogicalPosition;\n /** Horizontal alignment when scrolling (default: `'nearest'`). */\n inline?: ScrollLogicalPosition;\n /** Whether to focus after scrolling (default: `true`). */\n focus?: boolean;\n /** Passed to `focus()` when focusing (default: `true`). */\n preventScrollOnFocus?: boolean;\n /** Opens ancestor `<details>` elements before scroll/focus (default: `true`). */\n openCollapsedParents?: boolean;\n /** Selector used to locate the first invalid element. */\n invalidSelector?: string;\n /** Selector used to resolve the best focus target within the invalid element. */\n focusSelector?: string;\n};\n\nexport const DEFAULT_INVALID_SELECTOR = [\n '.ngx-control-wrapper--invalid',\n '.ngx-form-group-wrapper--invalid',\n 'input[aria-invalid=\"true\"]',\n 'textarea[aria-invalid=\"true\"]',\n 'select[aria-invalid=\"true\"]',\n].join(', ');\n\nexport const DEFAULT_FOCUS_SELECTOR = [\n 'input:not([type=\"hidden\"]):not([disabled])',\n 'textarea:not([disabled])',\n 'select:not([disabled])',\n 'button:not([disabled])',\n 'a[href]',\n '[tabindex]:not([tabindex=\"-1\"]):not([disabled])',\n].join(', ');\n\n/**\n * Focus candidates that are already invalid.\n * This ensures invalid group wrappers focus a failing control before any valid sibling.\n */\nconst INVALID_FOCUS_PREFERRED_SELECTOR = [\n 'input[aria-invalid=\"true\"]:not([type=\"hidden\"]):not([disabled])',\n 'textarea[aria-invalid=\"true\"]:not([disabled])',\n 'select[aria-invalid=\"true\"]:not([disabled])',\n '[aria-invalid=\"true\"][tabindex]:not([tabindex=\"-1\"]):not([disabled])',\n].join(', ');\n\nconst REDUCED_MOTION_MEDIA_QUERY = '(prefers-reduced-motion: reduce)';\n\nfunction prefersReducedMotion(): boolean {\n return (\n typeof globalThis.matchMedia === 'function' &&\n globalThis.matchMedia(REDUCED_MOTION_MEDIA_QUERY).matches\n );\n}\n\nexport function resolveFirstInvalidScrollBehavior(\n behavior?: ScrollBehavior\n): ScrollBehavior {\n if (behavior !== undefined) {\n return behavior;\n }\n return prefersReducedMotion() ? 'auto' : 'smooth';\n}\n\nexport function resolveFirstInvalidElement(\n root: HTMLFormElement,\n invalidSelector: string\n): HTMLElement | null {\n try {\n const firstInvalid = root.querySelector(invalidSelector);\n return firstInvalid instanceof HTMLElement ? firstInvalid : null;\n } catch {\n return null;\n }\n}\n\nexport function openCollapsedDetailsAncestors(\n root: HTMLFormElement,\n element: HTMLElement\n): void {\n let current: HTMLElement = element;\n while (current !== root) {\n const parentElement = current.parentElement;\n if (!parentElement) {\n break;\n }\n\n if (parentElement instanceof HTMLDetailsElement) {\n parentElement.open = true;\n }\n\n current = parentElement;\n }\n}\n\nexport function resolveFirstInvalidFocusTarget(\n firstInvalid: HTMLElement,\n focusSelector: string\n): HTMLElement | null {\n const preferredInvalidTarget = firstInvalid.querySelector(\n INVALID_FOCUS_PREFERRED_SELECTOR\n );\n if (preferredInvalidTarget instanceof HTMLElement) {\n return preferredInvalidTarget;\n }\n\n try {\n if (firstInvalid.matches(focusSelector)) {\n return firstInvalid;\n }\n\n const fallbackTarget = firstInvalid.querySelector(focusSelector);\n return fallbackTarget instanceof HTMLElement ? fallbackTarget : null;\n } catch {\n return null;\n }\n}\n","export const ROOT_FORM = 'rootForm';\n","import {\n AbstractControl,\n FormArray,\n FormGroup,\n isFormArray,\n isFormGroup,\n ValidationErrors,\n} from '@angular/forms';\nimport { ROOT_FORM } from '../constants';\nimport {\n isUnsafePathSegment,\n parseFieldPath,\n stringifyFieldPath,\n} from './field-path.utils';\n\ntype UnknownRecord = Record<string, unknown>;\n\ntype ControlWithOptionalName = AbstractControl & {\n name?: unknown;\n};\n\ntype FormContainer = FormGroup | FormArray;\ntype ErrorList = string[] & { warnings?: string[] };\n\nconst ERROR_MESSAGES_KEY = 'errors';\nconst WARNING_MESSAGES_KEY = 'warnings';\n\nfunction isRecord(value: unknown): value is UnknownRecord {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Recursively calculates the path of a form control\n * @param formGroup\n * @param control\n */\nfunction getChildEntries(\n container: FormContainer\n): Array<[string, AbstractControl]> {\n if (isFormArray(container)) {\n return container.controls.map((child, index) => [String(index), child]);\n }\n\n return Object.entries(container.controls);\n}\n\nfunction getControlPath(\n formGroup: FormGroup,\n control: AbstractControl\n): string {\n // First attempt: depth-first traversal from provided root\n for (const [key, child] of getChildEntries(formGroup)) {\n if (child === control) {\n return key;\n }\n if (isFormGroup(child) || isFormArray(child)) {\n const subPath = getControlPath(child as FormGroup, control);\n if (subPath) {\n return `${key}.${subPath}`;\n }\n }\n }\n\n // Fallback: walk up the parent chain from control to root\n let current: AbstractControl | null | undefined = control;\n const segments: string[] = [];\n while (current?.parent) {\n const parent: FormContainer = current.parent;\n for (const [key, controlInParent] of getChildEntries(parent)) {\n if (controlInParent === current) {\n segments.unshift(key);\n break;\n }\n }\n current = parent;\n if (current === formGroup) {\n return segments.join('.');\n }\n }\n\n // Last resort: try control.name if available\n const name = (control as ControlWithOptionalName).name;\n if (typeof name === 'string') {\n return name;\n }\n return '';\n}\n\n/**\n * Recursively calculates the path of a form group\n * @param formGroup\n * @param control\n */\nfunction getGroupPath(formGroup: FormGroup, control: AbstractControl): string {\n for (const [key, ctrl] of getChildEntries(formGroup)) {\n if (ctrl === control) {\n return key;\n }\n if (isFormGroup(ctrl)) {\n const path = getGroupPath(ctrl, control);\n if (path) {\n return `${key}.${path}`;\n }\n }\n }\n return '';\n}\n\n/**\n * @internal\n * Internal utility for calculating form control field paths.\n *\n * **Not intended for external use.** This function is used internally by the library\n * to determine field names for validation. Use the `name` attribute on your form controls\n * instead of relying on this function.\n *\n * Calculates the field name of a form control: Eg: addresses.shippingAddress.street\n * @param rootForm\n * @param control\n */\nexport function getFormControlField(\n rootForm: FormGroup,\n control: AbstractControl\n): string {\n return getControlPath(rootForm, control);\n}\n\n/**\n * @internal\n * Internal utility for calculating form group field paths.\n *\n * **Not intended for external use.** This function is used internally by the library\n * to determine field names for nested form groups.\n *\n * Calcuates the field name of a form group Eg: addresses.shippingAddress\n * @param rootForm\n * @param control\n */\nexport function getFormGroupField(\n rootForm: FormGroup,\n control: AbstractControl\n): string {\n return getGroupPath(rootForm, control);\n}\n\n/**\n * @internal\n * Internal utility for merging form values with disabled field values.\n *\n * **Not intended for external use.** This function is used internally by the library\n * to include disabled field values in form submissions. Use Angular's `getRawValue()`\n * method on your form if you need to access disabled field values.\n *\n * This utility merges the value of the form with the raw value.\n * By doing this we can assure that we don't lose values of disabled form fields\n *\n * Security: Unsafe prototype-related keys (`__proto__`, `prototype`, `constructor`)\n * are skipped during recursive merge.\n * @param form\n */\nexport function mergeValuesAndRawValues<T>(form: FormGroup): T {\n // Deep clone both values to prevent reference sharing.\n // This is necessary because:\n // 1. form.value may contain object references that could be mutated elsewhere\n // 2. form.getRawValue() also returns references to form control values\n // 3. Without cloning, mutations to the returned object would affect the original form state\n // 4. The merge operation itself requires a mutable copy to work with\n // Performance note: For large forms, this may have performance implications. However,\n // reference isolation is critical for maintaining form state integrity.\n const value = structuredClone(form.value);\n const rawValue = structuredClone(form.getRawValue());\n\n // Recursive function to merge rawValue into value\n function mergeRecursive(target: UnknownRecord, source: UnknownRecord): void {\n for (const key of Object.keys(source)) {\n if (isUnsafePathSegment(key)) {\n continue;\n }\n\n const sourceValue = source[key];\n const targetValue = target[key];\n\n if (targetValue === undefined || targetValue === null) {\n // Key missing from target (e.g. disabled field) or set to null —\n // copy source so raw values from disabled controls aren't dropped.\n target[key] = sourceValue;\n } else if (isRecord(sourceValue) && isRecord(targetValue)) {\n // If the value is an object, merge it recursively\n mergeRecursive(targetValue, sourceValue);\n }\n // If the target already has the key with a primitive value, it's left as is to maintain references\n }\n }\n\n mergeRecursive(value as UnknownRecord, rawValue as UnknownRecord);\n return value;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\ntype Primitive = undefined | null | boolean | string | number | Function;\n\nfunction isPrimitive(value: unknown): value is Primitive {\n return (\n value === null || (typeof value !== 'object' && typeof value !== 'function')\n );\n}\n\nfunction getStringArrayError(\n errors: ValidationErrors | null,\n key: string\n): string[] | undefined {\n const value = errors?.[key];\n return Array.isArray(value)\n ? value.filter((v): v is string => typeof v === 'string')\n : undefined;\n}\n\n/**\n * Performs a deep-clone of an object.\n *\n * @deprecated Use the standard {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone structuredClone} instead.\n *\n * `structuredClone` correctly handles `Map`, `Set`, `RegExp`, typed arrays, and\n * cyclic references; this implementation silently drops `Map` / `Set` / `RegExp`\n * data and produces incorrect results on cycles. Scheduled for removal in a\n * future major; see `docs/prd/PRD-bug-sweep.md` (Bundle D) for tracking.\n *\n * Browser Support: `structuredClone` is available in all modern browsers\n * (Chrome 98+, Firefox 94+, Safari 15.4+, Edge 98+) and Node.js 17+.\n */\nlet cloneDeepDeprecationWarned = false;\n\nexport function cloneDeep<T>(object: T): T {\n // NOTE: `typeof ngDevMode !== 'undefined' && ngDevMode` is kept inline\n // (not extracted to a helper) because Angular's build optimizer relies on\n // this exact pattern for tree-shaking dev-only code from production bundles.\n if (\n !cloneDeepDeprecationWarned &&\n typeof ngDevMode !== 'undefined' &&\n ngDevMode\n ) {\n cloneDeepDeprecationWarned = true;\n console.warn(\n '[ngx-vest-forms] cloneDeep is deprecated and silently drops Map/Set/RegExp values. ' +\n 'Use the standard structuredClone() instead.'\n );\n }\n\n // Handle primitives (null, undefined, boolean, string, number, function)\n if (isPrimitive(object)) {\n return object;\n }\n\n // Handle Date\n if (object instanceof Date) {\n return new Date(object) as T;\n }\n\n // Handle Array\n if (Array.isArray(object)) {\n return object.map((item) => cloneDeep(item)) as T;\n }\n\n // Handle Object\n if (object instanceof Object) {\n const clonedObject: UnknownRecord = {};\n for (const key in object) {\n if (Object.prototype.hasOwnProperty.call(object, key)) {\n clonedObject[key] = cloneDeep(\n (object as UnknownRecord)[key] as unknown\n );\n }\n }\n return clonedObject as T;\n }\n\n throw new Error(\"Unable to copy object! Its type isn't supported.\");\n}\n\n/**\n * Sets a value in an object at the provided field path.\n *\n * Supports dot and bracket notation via `parseFieldPath()`.\n * Examples: `user.profile.name`, `addresses[0].street`.\n *\n * Security: If any path segment matches an unsafe prototype-related key\n * (`__proto__`, `prototype`, `constructor`), the write is ignored.\n *\n * @param obj - Target object to mutate.\n * @param path - Dot/bracket field path.\n * @param value - Value to assign at the resolved path.\n */\nexport function setValueAtPath(\n obj: object,\n path: string,\n value: unknown\n): void {\n const keys = parseFieldPath(path);\n if (keys.length === 0) {\n return;\n }\n\n let current: UnknownRecord = obj as UnknownRecord;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const segment = keys[i];\n const nextSegment = keys[i + 1];\n if (segment === undefined) {\n continue;\n }\n if (isUnsafePathSegment(segment)) {\n return;\n }\n\n const key = String(segment);\n\n const next = current[key];\n if (!Array.isArray(next) && !isRecord(next)) {\n const shouldCreateArray =\n typeof nextSegment === 'number' ||\n (typeof nextSegment === 'string' && /^\\d+$/.test(nextSegment));\n current[key] = shouldCreateArray ? [] : {};\n }\n current = current[key] as UnknownRecord;\n }\n\n const lastSegment = keys[keys.length - 1];\n if (lastSegment === undefined || isUnsafePathSegment(lastSegment)) {\n return;\n }\n\n current[String(lastSegment)] = value;\n}\n\n/**\n * @deprecated Use {@link setValueAtPath} instead\n */\nexport function set(obj: object, path: string, value: unknown): void {\n return setValueAtPath(obj, path, value);\n}\n\n/**\n * @internal\n * Internal utility for collecting all form errors by field path.\n *\n * **Not intended for external use.** This function is used internally by the library\n * to generate the form state. Use the `formState()` signal from the `scVestForm` directive\n * to access form errors in your components.\n *\n * Traverses the form and returns the errors by path\n * @param form\n */\nexport function getAllFormErrors(\n form?: AbstractControl\n): Record<string, string[]> {\n const errors: Record<string, ErrorList> = {};\n if (!form) {\n return errors;\n }\n\n // Collect root form errors (from ValidateRootFormDirective) before processing children\n if (form.enabled) {\n const rootErrors = getStringArrayError(form.errors, ERROR_MESSAGES_KEY);\n if (rootErrors) {\n errors[ROOT_FORM] = rootErrors;\n }\n }\n\n function collect(\n control: AbstractControl,\n pathParts: Array<string | number>\n ): void {\n const pathString = stringifyFieldPath(pathParts);\n\n // Skip processing the root form control directly in NgxFormDirective\n if (pathParts.length === 0 && control === form) {\n // Instead, iterate its children if it's a group/array\n if (isFormGroup(control) || isFormArray(control)) {\n for (const [key, childControl] of getChildEntries(control)) {\n const numericKey = Number(key);\n const nextPath = [\n // ...pathParts, // pathParts is empty here\n Number.isNaN(numericKey) ? key : numericKey,\n ];\n collect(childControl, nextPath);\n }\n }\n return; // Stop processing for the root form itself at this level\n }\n\n if (isFormGroup(control) || isFormArray(control)) {\n for (const [key, childControl] of getChildEntries(control)) {\n const numericKey = Number(key);\n const nextPath = [\n ...pathParts,\n Number.isNaN(numericKey) ? key : numericKey,\n ];\n collect(childControl, nextPath);\n }\n }\n\n // Attach control errors (both errors and warnings)\n if (control.enabled) {\n const fieldErrors = getStringArrayError(\n control.errors,\n ERROR_MESSAGES_KEY\n );\n if (fieldErrors) {\n errors[pathString] = fieldErrors;\n }\n // Optionally, add warnings if present\n const fieldWarnings = getStringArrayError(\n control.errors,\n WARNING_MESSAGES_KEY\n );\n if (fieldWarnings) {\n // Attach warnings as a property on the error array (non-enumerable)\n // This is still done here for field-specific warnings, but not for root warnings.\n if (!errors[pathString]) {\n errors[pathString] = []; // Ensure array exists if only warnings are present\n }\n Object.defineProperty(errors[pathString], 'warnings', {\n value: fieldWarnings,\n enumerable: false, // Keep it non-enumerable as per previous behavior for field warnings\n configurable: true,\n writable: true,\n });\n }\n }\n }\n\n collect(form, []);\n\n // Root form errors (form.errors) are no longer processed here.\n // They are handled directly in NgxFormDirective to populate formState.root.\n\n return errors;\n}\n","import { isDevMode } from '@angular/core';\nimport { NGX_VEST_FORMS_ERRORS, logWarning } from '../errors/error-catalog';\n\nconst NUMERIC_PATH_SEGMENT = /^\\d+$/;\ntype TraversableShapeValue = Record<string, unknown> | unknown[];\n\nfunction isOpaqueLeafValue(value: unknown): boolean {\n return (\n value instanceof Date ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof RegExp ||\n (typeof File !== 'undefined' && value instanceof File) ||\n (typeof Blob !== 'undefined' && value instanceof Blob)\n );\n}\n\nfunction isTraversableValue(value: unknown): value is TraversableShapeValue {\n return typeof value === 'object' && value !== null && !isOpaqueLeafValue(value);\n}\n\n/**\n * Validates a form value against a shape to catch typos in `name` or `ngModelGroup` attributes.\n *\n * **What it checks:**\n * - Extra properties: Keys in formValue that don't exist in shape (likely typos)\n * - Type mismatches: When formValue has an object but shape expects a primitive\n *\n * **What it does NOT check:**\n * - Missing properties: Keys in shape that don't exist in formValue\n * (forms build incrementally with `NgxDeepPartial`, and `@if` conditionally renders fields)\n *\n * Only runs in development mode.\n *\n * @param formVal - The current form value\n * @param shape - The expected shape (created with `NgxDeepRequired<T>`)\n */\nexport function validateShape<\n T extends Record<string, unknown>,\n U extends Record<string, unknown>,\n>(formVal: T, shape: U): void {\n if (isDevMode()) {\n validateFormValueAgainstShape(formVal, shape);\n }\n}\n\n/**\n * Recursively validates form value keys against the shape.\n * Reports warnings for extra properties and type mismatches.\n */\nfunction validateFormValueAgainstShape(\n formValue: Record<string, unknown>,\n shape: Record<string, unknown>,\n path = ''\n): void {\n for (const key of Object.keys(formValue)) {\n const value = formValue[key];\n const fieldPath = path ? `${path}.${key}` : key;\n\n // Skip null/undefined values (valid during form initialization)\n if (value == null) {\n continue;\n }\n\n // For array items (numeric keys > 0), compare against the first item in shape\n // since we only define one example item in the shape for arrays\n const isNumericKey = NUMERIC_PATH_SEGMENT.test(key);\n // Array shapes provide one example item at index 0, so every numeric key maps to '0'.\n const shapeKey = isNumericKey && key !== '0' ? '0' : key;\n const shapeValue = shape?.[shapeKey];\n const hasShapeKey = shape != null && shapeKey in shape;\n\n // Skip Date fields receiving empty strings (common in date picker libraries)\n if (shapeValue instanceof Date && value === '') {\n continue;\n }\n\n // Handle object values (recurse into nested objects)\n if (typeof value === 'object') {\n if (!isTraversableValue(value)) {\n if (!isNumericKey && !hasShapeKey) {\n logWarning(NGX_VEST_FORMS_ERRORS.EXTRA_PROPERTY, fieldPath);\n } else if (\n !isNumericKey &&\n hasShapeKey &&\n (shapeValue === null || typeof shapeValue !== 'object')\n ) {\n // Type mismatch: formValue holds an opaque object (Date, Map, etc.)\n // but shape declares a primitive. Recursion is intentionally skipped\n // for opaque leaves, but the user still benefits from a warning.\n logWarning(\n NGX_VEST_FORMS_ERRORS.TYPE_MISMATCH,\n fieldPath,\n 'primitive',\n 'object'\n );\n }\n continue;\n }\n\n // Type mismatch: formValue has object, but shape expects primitive\n if (!isNumericKey && !isTraversableValue(shapeValue)) {\n logWarning(\n NGX_VEST_FORMS_ERRORS.TYPE_MISMATCH,\n fieldPath,\n 'primitive',\n 'object'\n );\n }\n\n // Recurse into nested object\n validateFormValueAgainstShape(\n value as Record<string, unknown>,\n isTraversableValue(shapeValue) ? (shapeValue as Record<string, unknown>) : {},\n fieldPath\n );\n continue;\n }\n\n // Extra property: key exists in formValue but not in shape (likely a typo)\n if (!isNumericKey && !hasShapeKey) {\n logWarning(NGX_VEST_FORMS_ERRORS.EXTRA_PROPERTY, fieldPath);\n }\n }\n}\n","import { signal, WritableSignal } from '@angular/core';\nimport { NgForm } from '@angular/forms';\n\nconst formSubmittedSignals = new WeakMap<NgForm, WritableSignal<boolean>>();\n\ntype AngularSubmittedSignalHost = {\n submittedReactive?: WritableSignal<boolean>;\n _submittedReactive?: WritableSignal<boolean>;\n};\n\nexport function getFormSubmittedSignal(\n ngForm: NgForm\n): WritableSignal<boolean> {\n let submitted = formSubmittedSignals.get(ngForm);\n if (!submitted) {\n submitted = signal(ngForm.submitted);\n formSubmittedSignals.set(ngForm, submitted);\n }\n return submitted;\n}\n\nexport function setAngularFormSubmittedState(\n ngForm: NgForm,\n submitted: boolean\n): void {\n // Angular 21.x's concrete NgForm stores submitted state on\n // `submittedReactive`, while AbstractFormDirective-backed implementations\n // expose `_submittedReactive` and may also provide a public setter on\n // `submitted`. This helper is verified against Angular 21.x in this\n // repository and falls back to the first writable `submitted` setter it can\n // find if a future Angular version changes the concrete field names.\n //\n // We try the concrete/internal signal first because NgForm overrides the\n // getter-only `submitted` property at runtime in this workspace. If neither\n // signal exists, fall back to the first writable `submitted` setter we can\n // find on the prototype chain. If no setter exists either, callers should\n // still update ngx-vest-forms' shared signal so error display state remains\n // reactive even though Angular's native submitted flag cannot be changed.\n const signalHost = ngForm as unknown as AngularSubmittedSignalHost;\n const angularSignal =\n signalHost.submittedReactive ?? signalHost._submittedReactive;\n\n if (angularSignal) {\n angularSignal.set(