@angular/forms
Version:
Angular - directives and services for creating forms
1 lines • 75 kB
Source Map (JSON)
{"version":3,"file":"signals-compat.mjs","sources":["../../../../../k8-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/compat/src/compat_field_node.ts","../../../../../k8-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/compat/src/compat_node_state.ts","../../../../../k8-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/compat/src/compat_structure.ts","../../../../../k8-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/compat/src/compat_validation_state.ts","../../../../../k8-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/compat/src/compat_field_adapter.ts","../../../../../k8-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/compat/src/api/compat_form.ts","../../../../../k8-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/compat/src/api/extract.ts","../../../../../k8-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/compat/src/api/di.ts","../../../../../k8-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/compat/src/signal_form_control/signal_form_control.ts"],"sourcesContent":["/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {computed, linkedSignal, runInInjectionContext, Signal, untracked} from '@angular/core';\nimport {toSignal} from '@angular/core/rxjs-interop';\nimport {AbstractControl} from '@angular/forms';\nimport {Observable, ReplaySubject} from 'rxjs';\nimport {map, takeUntil} from 'rxjs/operators';\nimport {FieldNode} from '../../src/field/node';\nimport {getInjectorFromOptions} from '../../src/field/util';\nimport type {CompatFieldNodeOptions} from './compat_structure';\n\n/**\n * Field node with additional control property.\n *\n * Compat node has no children.\n */\nexport class CompatFieldNode extends FieldNode {\n readonly control: Signal<AbstractControl>;\n\n constructor(public readonly options: CompatFieldNodeOptions) {\n super(options);\n this.control = this.options.control;\n }\n}\n\n/**\n * Makes a function which creates a new subject (and unsubscribes/destroys the previous one).\n *\n * This allows us to automatically unsubscribe from status changes of the previous FormControl when we go to subscribe to a new one\n */\nfunction makeCreateDestroySubject() {\n let destroy$ = new ReplaySubject<void>(1);\n return () => {\n destroy$.next();\n destroy$.complete();\n\n return (destroy$ = new ReplaySubject<void>(1));\n };\n}\n\n/**\n * Helper function taking options, and a callback which takes options, and a function\n * converting reactive control to appropriate property using toSignal from rxjs compat.\n *\n * This helper keeps all complexity in one place by doing the following things:\n * - Running the callback in injection context\n * - Not tracking the callback, as it creates a new signal.\n * - Reacting to control changes, allowing to swap control dynamically.\n *\n * @param options\n * @param makeSignal\n */\nexport function extractControlPropToSignal<T, R = T>(\n options: CompatFieldNodeOptions,\n makeSignal: (c: AbstractControl<unknown, T>, destroy$: Observable<void>) => Signal<R>,\n): Signal<R> {\n const injector = getInjectorFromOptions(options);\n\n // Creates a subject that could be used in takeUntil.\n const createDestroySubject = makeCreateDestroySubject();\n\n const signalOfControlSignal = linkedSignal({\n source: options.control,\n computation: (control) => {\n return untracked(() => {\n return runInInjectionContext(injector, () => makeSignal(control, createDestroySubject()));\n });\n },\n });\n\n // We have to have computed, because we need to react to both:\n // linked signal changes as well as the inner signal changes.\n return computed(() => signalOfControlSignal()());\n}\n\n/**\n * A helper function, simplifying getting reactive control properties after status changes.\n *\n * Used to extract errors and statuses such as valid, pending.\n *\n * @param options\n * @param getValue\n */\nexport const getControlStatusSignal = <T>(\n options: CompatFieldNodeOptions,\n getValue: (c: AbstractControl<unknown>) => T,\n) => {\n return extractControlPropToSignal<unknown, T>(options, (c, destroy$) =>\n toSignal(\n c.statusChanges.pipe(\n map(() => getValue(c)),\n takeUntil(destroy$),\n ),\n {\n initialValue: getValue(c),\n },\n ),\n );\n};\n\n/**\n * A helper function, simplifying converting convert events to signals.\n *\n * Used to get dirty and touched signals from control.\n *\n * @param options\n * @param getValue A function which takes control and returns required value.\n */\nexport const getControlEventsSignal = <T>(\n options: CompatFieldNodeOptions,\n getValue: (c: AbstractControl) => T,\n) => {\n return extractControlPropToSignal<unknown, T>(options, (c, destroy$) =>\n toSignal(\n c.events.pipe(\n map(() => {\n return getValue(c);\n }),\n takeUntil(destroy$),\n ),\n {\n initialValue: getValue(c),\n },\n ),\n );\n};\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {computed, Signal} from '@angular/core';\nimport {AbstractControl} from '@angular/forms';\nimport {FieldNodeState} from '../../src/field/state';\nimport {CompatFieldNode, getControlEventsSignal, getControlStatusSignal} from './compat_field_node';\nimport {CompatFieldNodeOptions} from './compat_structure';\n\n/**\n * A FieldNodeState class wrapping a FormControl and proxying it's state.\n */\nexport class CompatNodeState extends FieldNodeState {\n override readonly touched: Signal<boolean>;\n override readonly dirty: Signal<boolean>;\n override readonly disabled: Signal<boolean>;\n private readonly control: Signal<AbstractControl>;\n\n constructor(\n readonly compatNode: CompatFieldNode,\n options: CompatFieldNodeOptions,\n ) {\n super(compatNode);\n this.control = options.control;\n this.touched = getControlEventsSignal(options, (c) => c.touched);\n this.dirty = getControlEventsSignal(options, (c) => c.dirty);\n const controlDisabled = getControlStatusSignal(options, (c) => c.disabled);\n\n this.disabled = computed(() => {\n return controlDisabled() || this.disabledReasons().length > 0;\n });\n }\n\n override markAsDirty() {\n this.control().markAsDirty();\n }\n\n override markAsTouched() {\n this.control().markAsTouched();\n }\n\n override markAsPristine() {\n this.control().markAsPristine();\n }\n\n override markAsUntouched() {\n this.control().markAsUntouched();\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {\n computed,\n Signal,\n signal,\n WritableSignal,\n ɵRuntimeError as RuntimeError,\n} from '@angular/core';\nimport {RuntimeErrorCode} from '../../src/errors';\nimport {FormFieldManager} from '../../src/field/manager';\nimport {FieldNode, ParentFieldNode} from '../../src/field/node';\nimport {\n ChildFieldNodeOptions,\n FieldNodeOptions,\n FieldNodeStructure,\n RootFieldNodeOptions,\n} from '../../src/field/structure';\n\nimport {toSignal} from '@angular/core/rxjs-interop';\nimport {AbstractControl} from '@angular/forms';\nimport {map, takeUntil} from 'rxjs/operators';\nimport {extractControlPropToSignal} from './compat_field_node';\n\n/**\n * Child Field Node options also exposing control property.\n */\nexport interface CompatChildFieldNodeOptions extends ChildFieldNodeOptions {\n control: Signal<AbstractControl>;\n}\n\n/**\n * Root Field Node options also exposing control property.\n */\nexport interface CompatRootFieldNodeOptions extends RootFieldNodeOptions {\n control: Signal<AbstractControl>;\n}\n\n/**\n * Field Node options also exposing control property.\n */\nexport type CompatFieldNodeOptions = CompatRootFieldNodeOptions | CompatChildFieldNodeOptions;\n\n/**\n * A helper function allowing to get parent if it exists.\n */\nfunction getParentFromOptions(options: FieldNodeOptions) {\n if (options.kind === 'root') {\n return undefined;\n }\n\n return options.parent;\n}\n\n/**\n * A helper function allowing to get fieldManager regardless of the option type.\n */\nfunction getFieldManagerFromOptions(options: FieldNodeOptions) {\n if (options.kind === 'root') {\n return options.fieldManager;\n }\n\n return options.parent.structure.root.structure.fieldManager;\n}\n\n/**\n * A helper function that takes CompatFieldNodeOptions, and produce a writable signal synced to the\n * value of contained AbstractControl.\n *\n * This uses toSignal, which requires an injector.\n *\n * @param options\n */\nfunction getControlValueSignal<T>(options: CompatFieldNodeOptions) {\n const value = extractControlPropToSignal<T>(options, (control, destroy$) => {\n return toSignal(\n control.valueChanges.pipe(\n map(() => control.getRawValue()),\n takeUntil(destroy$),\n ),\n {\n initialValue: control.getRawValue(),\n },\n );\n }) as WritableSignal<T>;\n\n value.set = (value: T) => {\n options.control().setValue(value);\n };\n\n value.update = (fn: (current: T) => T) => {\n value.set(fn(value()));\n };\n\n return value;\n}\n\n/**\n * Compat version of FieldNodeStructure,\n * - It has no children\n * - It wraps FormControl and proxies its value.\n */\nexport class CompatStructure extends FieldNodeStructure {\n override value: WritableSignal<unknown>;\n override keyInParent: Signal<string>;\n override root: FieldNode;\n override pathKeys: Signal<readonly string[]>;\n override readonly children = signal([]);\n override readonly childrenMap = computed(() => undefined);\n override readonly parent: ParentFieldNode | undefined;\n override readonly fieldManager: FormFieldManager;\n override readonly isOrphaned: Signal<boolean>;\n\n constructor(node: FieldNode, options: CompatFieldNodeOptions) {\n super(options.logic, node, () => {\n throw new RuntimeError(\n RuntimeErrorCode.COMPAT_NO_CHILDREN,\n ngDevMode && `Compat nodes don't have children.`,\n );\n });\n this.value = getControlValueSignal(options);\n this.parent = getParentFromOptions(options);\n this.root = this.parent?.structure.root ?? node;\n this.fieldManager = getFieldManagerFromOptions(options);\n\n const identityInParent = options.kind === 'child' ? options.identityInParent : undefined;\n const initialKeyInParent = options.kind === 'child' ? options.initialKeyInParent : undefined;\n\n const signals = this.createKeyOrOrphanSignals(\n options.kind,\n identityInParent,\n initialKeyInParent,\n );\n this.keyInParent = signals.keyInParent;\n this.isOrphaned = signals.isOrphaned;\n\n this.pathKeys = computed(() =>\n this.parent ? [...this.parent.structure.pathKeys(), this.keyInParent()] : [],\n );\n }\n\n override getChild(): FieldNode | undefined {\n return undefined;\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {computed, Signal} from '@angular/core';\nimport {AbstractControl} from '@angular/forms';\nimport {ValidationError} from '../../src/api/rules';\nimport {calculateValidationSelfStatus, ValidationState} from '../../src/field/validation';\nimport {\n extractNestedReactiveErrors,\n type CompatValidationError,\n} from '../../src/compat/validation_errors';\nimport {CompatFieldNode, getControlStatusSignal} from './compat_field_node';\nimport {CompatFieldNodeOptions} from './compat_structure';\n\n// Readonly signal containing an empty array, used for optimization.\nconst EMPTY_ARRAY_SIGNAL = computed(() => []);\n\n/**\n * Compat version of a validation state that wraps a FormControl, and proxies it's validation state.\n */\nexport class CompatValidationState implements ValidationState {\n readonly syncValid: Signal<boolean>;\n /**\n * All validation errors for this field.\n */\n readonly errors: Signal<CompatValidationError[]>;\n readonly pending: Signal<boolean>;\n readonly invalid: Signal<boolean>;\n readonly valid: Signal<boolean>;\n\n readonly parseErrors: Signal<ValidationError.WithFormField[]> = computed(() => []);\n\n constructor(\n private readonly node: CompatFieldNode,\n options: CompatFieldNodeOptions,\n ) {\n this.syncValid = getControlStatusSignal(options, (c: AbstractControl) => c.status === 'VALID');\n this.errors = getControlStatusSignal(options, extractNestedReactiveErrors);\n this.pending = getControlStatusSignal(options, (c) => c.pending);\n\n this.valid = getControlStatusSignal(options, (c) => {\n return c.valid;\n });\n\n this.invalid = getControlStatusSignal(options, (c) => {\n return c.invalid;\n });\n }\n\n asyncErrors: Signal<(ValidationError.WithFieldTree | 'pending')[]> = EMPTY_ARRAY_SIGNAL;\n errorSummary: Signal<ValidationError.WithFieldTree[]> = EMPTY_ARRAY_SIGNAL;\n\n // Those are irrelevant for compat mode, as it has no children\n rawSyncTreeErrors = EMPTY_ARRAY_SIGNAL;\n syncErrors = EMPTY_ARRAY_SIGNAL;\n rawAsyncErrors = EMPTY_ARRAY_SIGNAL;\n\n // Compat fields can't have validation rules applied to them; however, there are other\n // features that depend on this property, such as `markAsTouched()`.\n readonly shouldSkipValidation = computed(\n () => this.node.hidden() || this.node.disabled() || this.node.readonly(),\n );\n\n /**\n * Computes status based on whether the field is valid/invalid/pending.\n */\n readonly status: Signal<'valid' | 'invalid' | 'unknown'> = computed(() => {\n return calculateValidationSelfStatus(this);\n });\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {computed, Signal, WritableSignal} from '@angular/core';\nimport {AbstractControl} from '@angular/forms';\nimport {BasicFieldAdapter, FieldAdapter} from '../../src/field/field_adapter';\nimport {FormFieldManager} from '../../src/field/manager';\nimport {FieldNode} from '../../src/field/node';\nimport {FieldNodeState} from '../../src/field/state';\nimport {\n ChildFieldNodeOptions,\n FieldNodeOptions,\n FieldNodeStructure,\n} from '../../src/field/structure';\nimport {ValidationState} from '../../src/field/validation';\nimport {FieldPathNode} from '../../src/schema/path_node';\nimport {CompatFieldNode} from './compat_field_node';\nimport {CompatNodeState} from './compat_node_state';\nimport {CompatChildFieldNodeOptions, CompatStructure} from './compat_structure';\nimport {CompatValidationState} from './compat_validation_state';\n\n/**\n * This is a tree-shakable Field adapter that can create a compat node\n * that proxies FormControl state and value to a field.\n */\nexport class CompatFieldAdapter implements FieldAdapter {\n readonly basicAdapter = new BasicFieldAdapter();\n\n /**\n * Creates a regular or compat root node state based on whether the control is present.\n * @param fieldManager\n * @param value\n * @param pathNode\n * @param adapter\n */\n newRoot<TModel>(\n fieldManager: FormFieldManager,\n value: WritableSignal<TModel>,\n pathNode: FieldPathNode,\n adapter: FieldAdapter,\n ): FieldNode {\n if (value() instanceof AbstractControl) {\n return createCompatNode({\n kind: 'root',\n fieldManager,\n value,\n pathNode,\n logic: pathNode.builder.build(),\n fieldAdapter: adapter,\n });\n }\n\n return this.basicAdapter.newRoot<TModel>(fieldManager, value, pathNode, adapter);\n }\n\n /**\n * Creates a regular or compat node state based on whether the control is present.\n * @param node\n * @param options\n */\n createNodeState(node: CompatFieldNode, options: CompatChildFieldNodeOptions): FieldNodeState {\n if (!options.control) {\n return this.basicAdapter.createNodeState(node);\n }\n return new CompatNodeState(node, options);\n }\n\n /**\n * Creates a regular or compat structure based on whether the control is present.\n * @param node\n * @param options\n */\n createStructure(node: CompatFieldNode, options: CompatChildFieldNodeOptions): FieldNodeStructure {\n if (!options.control) {\n return this.basicAdapter.createStructure(node, options);\n }\n return new CompatStructure(node, options);\n }\n\n /**\n * Creates a regular or compat validation state based on whether the control is present.\n * @param node\n * @param options\n */\n createValidationState(\n node: CompatFieldNode,\n options: CompatChildFieldNodeOptions,\n ): ValidationState {\n if (!options.control) {\n return this.basicAdapter.createValidationState(node);\n }\n return new CompatValidationState(node, options);\n }\n\n /**\n * Creates a regular or compat node based on whether the control is present.\n * @param options\n */\n newChild(options: ChildFieldNodeOptions): FieldNode {\n const value = options.parent.value()[options.initialKeyInParent];\n\n if (value instanceof AbstractControl) {\n return createCompatNode(options);\n }\n\n return new FieldNode(options);\n }\n}\n\n/**\n * Creates a CompatFieldNode from options.\n * @param options\n */\nexport function createCompatNode(options: FieldNodeOptions) {\n const control = (\n options.kind === 'root'\n ? options.value\n : computed(() => {\n return options.parent.value()[options.initialKeyInParent];\n })\n ) as Signal<AbstractControl>;\n\n return new CompatFieldNode({\n ...options,\n control,\n });\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {WritableSignal} from '@angular/core';\nimport {form, FormOptions} from '../../../public_api';\nimport {FieldTree, PathKind, SchemaOrSchemaFn} from '../../../src/api/types';\nimport {normalizeFormArgs} from '../../../src/util/normalize_form_args';\nimport {CompatFieldAdapter} from '../compat_field_adapter';\n\n/**\n * Options that may be specified when creating a compat form.\n *\n * @category interop\n * @publicApi 22.0\n */\nexport type CompatFormOptions<TModel> = Omit<FormOptions<TModel>, 'adapter'>;\n\n/**\n * Creates a compatibility form wrapped around the given model data.\n *\n * `compatForm` is a version of the `form` function that is designed for backwards\n * compatibility with Reactive forms by accepting Reactive controls as a part of the data.\n *\n * @example\n * ```ts\n * const lastName = new FormControl('lastName');\n *\n * const nameModel = signal({\n * first: '',\n * last: lastName\n * });\n *\n * const nameForm = compatForm(nameModel, (name) => {\n * required(name.first);\n * });\n *\n * nameForm.last().value(); // lastName, not FormControl\n * ```\n *\n * @param model A writable signal that contains the model data for the form. The resulting field\n * structure will match the shape of the model and any changes to the form data will be written to\n * the model.\n *\n * @category interop\n * @publicApi 22.0\n */\nexport function compatForm<TModel>(model: WritableSignal<TModel>): FieldTree<TModel>;\n\n/**\n * Creates a compatibility form wrapped around the given model data.\n *\n * `compatForm` is a version of the `form` function that is designed for backwards\n * compatibility with Reactive forms by accepting Reactive controls as a part of the data.\n *\n * @example\n * ```ts\n * const lastName = new FormControl('lastName');\n *\n * const nameModel = signal({\n * first: '',\n * last: lastName\n * });\n *\n * const nameForm = compatForm(nameModel, (name) => {\n * required(name.first);\n * });\n *\n * nameForm.last().value(); // lastName, not FormControl\n *\n * @param model A writable signal that contains the model data for the form. The resulting field\n * structure will match the shape of the model and any changes to the form data will be written to\n * the model.\n * @param schemaOrOptions The second argument can be either\n * 1. A schema or a function used to specify logic for the form (e.g. validation, disabled fields, etc.).\n * When passing a schema, the form options can be passed as a third argument if needed.\n * 2. The form options (excluding adapter, since it's provided).\n *\n * @category interop\n * @publicApi 22.0\n */\nexport function compatForm<TModel>(\n model: WritableSignal<TModel>,\n schemaOrOptions: SchemaOrSchemaFn<TModel> | CompatFormOptions<TModel>,\n): FieldTree<TModel>;\n\n/**\n * Creates a compatibility form wrapped around the given model data.\n *\n * `compatForm` is a version of the `form` function that is designed for backwards\n * compatibility with Reactive forms by accepting Reactive controls as a part of the data.\n *\n * @example\n * ```ts\n * const lastName = new FormControl('lastName');\n *\n * const nameModel = signal({\n * first: '',\n * last: lastName\n * });\n *\n * const nameForm = compatForm(nameModel, (name) => {\n * required(name.first);\n * });\n *\n * nameForm.last().value(); // lastName, not FormControl\n *\n * @param model A writable signal that contains the model data for the form. The resulting field\n * structure will match the shape of the model and any changes to the form data will be written to\n * the model.\n * @param schemaOrOptions A schema or a function used to specify logic for the form (e.g. validation, disabled fields, etc.).\n * When passing a schema, the form options can be passed as a third argument if needed.\n * @param options The form options (excluding adapter, since it's provided).\n *\n * @category interop\n * @publicApi 22.0\n */\nexport function compatForm<TModel>(\n model: WritableSignal<TModel>,\n schema: SchemaOrSchemaFn<TModel>,\n options: CompatFormOptions<TModel>,\n): FieldTree<TModel>;\n\nexport function compatForm<TModel>(...args: any[]): FieldTree<TModel> {\n const [model, maybeSchema, maybeOptions] = normalizeFormArgs<TModel>(args);\n\n const options = {...maybeOptions, adapter: new CompatFieldAdapter()};\n const schema = maybeSchema || ((() => {}) as SchemaOrSchemaFn<TModel, PathKind>);\n return form(model, schema, options);\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {untracked} from '@angular/core';\nimport {AbstractControl} from '@angular/forms';\nimport {FieldState, FieldTree} from '../../../src/api/types';\nimport {isArray, isObject} from '../../../src/util/type_guards';\n\n/**\n * Type utility that recursively unwraps the value type of a `FieldTree`.\n *\n * If the value type contains `AbstractControl` instances (common in compat mode),\n * they are replaced with their underlying value types.\n */\nexport type RawValue<T> =\n T extends AbstractControl<infer TValue, any>\n ? TValue\n : T extends (infer U)[]\n ? RawValue<U>[]\n : T extends object\n ? {[K in keyof T]: RawValue<T[K]>}\n : T;\n\n/**\n * A type that recursively makes all properties of T optional.\n * Used for the result of `extractValue` when filtering is applied.\n * @publicApi 22.0\n */\nexport type DeepPartial<T> =\n | (T extends (infer U)[]\n ? DeepPartial<U>[]\n : T extends object\n ? {[K in keyof T]?: DeepPartial<T[K]>}\n : T)\n | undefined;\n\n/**\n * Criteria that determine whether a field should be included in the extraction.\n *\n * Each property is optional; when provided, the field must match the specified state.\n *\n * @category interop\n * @publicApi 22.0\n */\nexport interface ExtractFilter {\n readonly dirty?: boolean;\n readonly touched?: boolean;\n readonly enabled?: boolean;\n}\n\n/**\n * Utility to unwrap a {@link FieldTree} into its underlying raw value.\n *\n * This function is recursive, so if the field tree represents an object or an array,\n * the result will be an object or an array of the raw values of its children.\n *\n * @param field The field tree to extract the value from.\n * @returns The raw value of the field tree.\n *\n * @category interop\n * @publicApi 22.0\n */\nexport function extractValue<T>(field: FieldTree<T>): RawValue<T>;\n/**\n * Utility to unwrap a {@link FieldTree} into its underlying raw value.\n *\n * This function is recursive, so if the field tree represents an object or an array,\n * the result will be an object or an array of the raw values of its children.\n *\n * @param field The field tree to extract the value from.\n * @param filter Criteria to include only fields matching certain state (dirty, touched, enabled).\n * @returns A partial value containing only the fields matching the filter, or `undefined` if none match.\n *\n * @category interop\n * @publicApi 22.0\n */\nexport function extractValue<T>(\n field: FieldTree<T>,\n filter: ExtractFilter,\n): DeepPartial<RawValue<T>>;\nexport function extractValue<T>(\n field: FieldTree<T>,\n filter?: ExtractFilter,\n): RawValue<T> | DeepPartial<RawValue<T>> {\n return untracked(() => visitFieldTree(field, filter)) as RawValue<T> | DeepPartial<RawValue<T>>;\n}\n\nfunction visitFieldTree(\n field: FieldTree<unknown>,\n filter?: ExtractFilter,\n): RawValue<unknown> | DeepPartial<RawValue<unknown>> {\n const state = field();\n const value = state.value();\n\n const matchingChildren = extractChildren(field, value, filter);\n\n if (matchingChildren !== undefined || isContainerNode(field, value)) {\n return matchingChildren;\n }\n\n if (matchesFilter(state, filter)) {\n return value;\n }\n\n return undefined;\n}\n\nfunction isContainerNode(field: FieldTree<unknown>, value: unknown): boolean {\n return (\n (isArray(value) || isObject(value)) &&\n Object.keys(value).some((k) => isFieldTreeNode(field[k as keyof FieldTree<unknown>]))\n );\n}\n\nfunction extractChildren(\n field: FieldTree<unknown>,\n value: unknown,\n filter?: ExtractFilter,\n): unknown {\n if (isArray(value)) {\n const record = field as unknown as Record<number, FieldTree<unknown>>;\n const arrayValue = value as readonly FieldTree<unknown>[];\n const result: unknown[] = new Array(arrayValue.length);\n let hasMatch = false;\n\n for (let i = 0; i < arrayValue.length; i++) {\n const child = record[i];\n\n const childResult = visitFieldTree(child, filter);\n if (childResult !== undefined) {\n hasMatch = true;\n }\n result[i] = childResult;\n }\n\n return hasMatch ? result : undefined;\n }\n\n if (isObject(value)) {\n const record = field as unknown as Record<string, unknown>;\n const objectValue = value as Record<string, unknown>;\n const entries = Object.keys(objectValue)\n .map<[string, FieldTree<unknown>] | undefined>((key) => {\n const child = record[key];\n return isFieldTreeNode(child) ? [key, child] : undefined;\n })\n .filter(isKeyedChild)\n .map(([key, child]) => {\n const childResult = visitFieldTree(child, filter);\n return childResult !== undefined ? ([key, childResult] as [string, unknown]) : undefined;\n })\n .filter((v) => v !== undefined);\n\n return entries.length ? Object.fromEntries(entries) : undefined;\n }\n\n return undefined;\n}\n\nfunction isFieldTreeNode(value: unknown): value is FieldTree<unknown> {\n return typeof value === 'function';\n}\n\nfunction isKeyedChild(\n value: [string, FieldTree<unknown>] | undefined,\n): value is [string, FieldTree<unknown>] {\n return value !== undefined;\n}\n\nfunction matchesFilter(state: FieldState<unknown>, filter?: ExtractFilter): boolean {\n if (!filter) {\n return true;\n }\n\n if (filter.dirty !== undefined && state.dirty() !== filter.dirty) {\n return false;\n }\n\n if (filter.touched !== undefined && state.touched() !== filter.touched) {\n return false;\n }\n\n if (filter.enabled !== undefined) {\n const enabled = !state.disabled();\n if (enabled !== filter.enabled) {\n return false;\n }\n }\n\n return true;\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport type {SignalFormsConfig} from '../../../src/api/di';\n\n/**\n * A value that can be used for `SignalFormsConfig.classes` to automatically add\n * the `ng-*` status classes from reactive forms.\n *\n * @publicApi 22.0\n */\nexport const NG_STATUS_CLASSES: SignalFormsConfig['classes'] = {\n 'ng-touched': ({state}) => state().touched(),\n 'ng-untouched': ({state}) => !state().touched(),\n 'ng-dirty': ({state}) => state().dirty(),\n 'ng-pristine': ({state}) => !state().dirty(),\n 'ng-valid': ({state}) => state().valid(),\n 'ng-invalid': ({state}) => state().invalid(),\n 'ng-pending': ({state}) => state().pending(),\n};\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {\n effect,\n EventEmitter,\n inject,\n Injector,\n ɵRuntimeError as RuntimeError,\n signal,\n untracked,\n WritableSignal,\n} from '@angular/core';\nimport {\n AbstractControl,\n ControlEvent,\n FormArray,\n FormControlState,\n FormControlStatus,\n FormGroup,\n FormResetEvent,\n PristineChangeEvent,\n StatusChangeEvent,\n TouchedChangeEvent,\n ValueChangeEvent,\n} from '@angular/forms';\n\nimport {FormOptions} from '../../../src/api/structure';\nimport {FieldState, FieldTree, SchemaFn} from '../../../src/api/types';\nimport {signalErrorsToValidationErrors} from '../../../src/compat/validation_errors';\nimport {RuntimeErrorCode} from '../../../src/errors';\nimport {FieldNode} from '../../../src/field/node';\nimport {normalizeFormArgs} from '../../../src/util/normalize_form_args';\nimport {compatForm} from '../api/compat_form';\n\n/** Options used to update the control value. */\nexport type ValueUpdateOptions = {\n onlySelf?: boolean;\n emitEvent?: boolean;\n emitModelToViewChange?: boolean;\n emitViewToModelChange?: boolean;\n};\n\n/**\n * A `FormControl` that is backed by signal forms rules.\n *\n * This class provides a bridge between Signal Forms and Reactive Forms, allowing\n * signal-based controls to be used within a standard `FormGroup` or `FormArray`.\n *\n * A control could be created using signal forms, and integrated with an existing FormGroup\n * propagating all the statuses and validity.\n *\n * @usageNotes\n *\n * ### Basic usage\n *\n * ```angular-ts\n * const form = new FormGroup({\n * // You can create SignalFormControl with signal form rules, and add it to a FormGroup.\n * name: new SignalFormControl('Alice', p => {\n * required(p);\n * }),\n * age: new FormControl(25),\n * });\n * ```\n * In the template you can get the underlying `fieldTree` and bind it:\n *\n * ```angular-html\n * <form [formGroup]=\"form\">\n * <input [formField]=\"nameControl.fieldTree\" />\n * <input formControlName=\"age\" />\n * </form>\n * ```\n *\n * @publicApi 22.0\n */\nexport class SignalFormControl<T> extends AbstractControl {\n /** Source FieldTree. */\n public readonly fieldTree: FieldTree<T>;\n /** The raw signal driving the control value. */\n public readonly sourceValue: WritableSignal<T>;\n\n private readonly fieldState: FieldState<T>;\n private readonly initialValue: T;\n private pendingParentNotifications = 0;\n private readonly onChangeCallbacks: Array<(value?: any, emitModelEvent?: boolean) => void> = [];\n private readonly onDisabledChangeCallbacks: Array<(isDisabled: boolean) => void> = [];\n override readonly valueChanges = new EventEmitter<T>();\n override readonly statusChanges = new EventEmitter<FormControlStatus>();\n\n constructor(value: T, schemaOrOptions?: SchemaFn<T> | FormOptions<T>, options?: FormOptions<T>) {\n super(null, null);\n\n const [model, schema, opts] = normalizeFormArgs<T>([signal(value), schemaOrOptions, options]);\n this.sourceValue = model;\n this.initialValue = value;\n const injector = opts?.injector ?? inject(Injector);\n\n const rawTree = schema\n ? compatForm(this.sourceValue, schema, {injector})\n : compatForm(this.sourceValue, {injector});\n\n this.fieldTree = wrapFieldTreeForSyncUpdates(rawTree, () =>\n this.parent?.updateValueAndValidity({sourceControl: this} as any),\n );\n this.fieldState = this.fieldTree();\n\n this.defineCompatProperties();\n\n // Value changes effect\n effect(\n () => {\n const value = this.sourceValue();\n untracked(() => {\n this.notifyParentUnlessPending();\n this.valueChanges.emit(value);\n this.emitControlEvent(new ValueChangeEvent(value, this));\n });\n },\n {injector},\n );\n\n // Status changes effect\n effect(\n () => {\n const status = this.status;\n untracked(() => {\n this.statusChanges.emit(status);\n });\n this.emitControlEvent(new StatusChangeEvent(status, this));\n },\n {injector},\n );\n\n // Disabled changes effect\n effect(\n () => {\n const isDisabled = this.disabled;\n untracked(() => {\n for (const fn of this.onDisabledChangeCallbacks) {\n fn(isDisabled);\n }\n });\n },\n {injector},\n );\n\n // Touched changes effect\n effect(\n () => {\n const isTouched = this.fieldState.touched();\n this.emitControlEvent(new TouchedChangeEvent(isTouched, this));\n const parent = this.parent;\n if (!parent) {\n return;\n }\n if (!isTouched) {\n parent.markAsUntouched();\n } else {\n parent.markAsTouched();\n }\n },\n {injector},\n );\n\n // Dirty changes effect\n effect(\n () => {\n const isDirty = this.fieldState.dirty();\n this.emitControlEvent(new PristineChangeEvent(!isDirty, this));\n const parent = this.parent;\n if (!parent) {\n return;\n }\n if (isDirty) {\n parent.markAsDirty();\n } else {\n parent.markAsPristine();\n }\n },\n {injector},\n );\n }\n\n /**\n * Defines properties using closure-safe names to prevent issues with property renaming optimizations.\n *\n * AbstractControl have `value` and `errors` as readonly prop, which doesn't allow getters.\n **/\n private defineCompatProperties(): void {\n const valueProp = getClosureSafeProperty({value: getClosureSafeProperty});\n Object.defineProperty(this, valueProp, {\n get: () => this.sourceValue(),\n });\n const errorsProp = getClosureSafeProperty({errors: getClosureSafeProperty});\n Object.defineProperty(this, errorsProp, {\n get: () => signalErrorsToValidationErrors(this.fieldState.errors()),\n });\n }\n\n private emitControlEvent(event: ControlEvent): void {\n untracked(() => {\n (this as any)._events.next(event);\n });\n }\n\n override setValue(value: any, options?: ValueUpdateOptions): void {\n this.updateValue(value, options);\n }\n\n override patchValue(value: any, options?: ValueUpdateOptions): void {\n this.updateValue(value, options);\n }\n\n private updateValue(value: any, options?: ValueUpdateOptions): void {\n const parent = this.scheduleParentUpdate(options);\n this.sourceValue.set(value);\n if (parent) {\n this.updateParentValueAndValidity(parent, options?.emitEvent);\n }\n if (options?.emitModelToViewChange !== false) {\n for (const fn of this.onChangeCallbacks) {\n fn(value, true);\n }\n }\n }\n\n registerOnChange(fn: (value?: any, emitModelEvent?: boolean) => void): void {\n this.onChangeCallbacks.push(fn);\n }\n\n /** @internal */\n _unregisterOnChange(fn: (value?: any, emitModelEvent?: boolean) => void): void {\n removeListItem(this.onChangeCallbacks, fn);\n }\n\n registerOnDisabledChange(fn: (isDisabled: boolean) => void): void {\n this.onDisabledChangeCallbacks.push(fn);\n }\n\n /** @internal */\n _unregisterOnDisabledChange(fn: (isDisabled: boolean) => void): void {\n removeListItem(this.onDisabledChangeCallbacks, fn);\n }\n\n override getRawValue(): T {\n return this.value;\n }\n\n override reset(value?: T | FormControlState<T>, options?: ValueUpdateOptions): void {\n if (isFormControlState(value)) {\n throw unsupportedDisableEnableError();\n }\n\n const resetValue = value ?? this.initialValue;\n this.fieldState.reset(resetValue);\n\n if (value !== undefined) {\n this.updateValue(value, options);\n } else if (!options?.onlySelf) {\n const parent = this.parent;\n if (parent) {\n this.updateParentValueAndValidity(parent, options?.emitEvent);\n }\n }\n\n if (options?.emitEvent !== false) {\n this.emitControlEvent(new FormResetEvent(this));\n }\n }\n\n private scheduleParentUpdate(options?: ValueUpdateOptions): FormGroup | FormArray | null {\n const parent = options?.onlySelf ? null : this.parent;\n if (options?.onlySelf || parent) {\n this.pendingParentNotifications++;\n }\n return parent;\n }\n\n private notifyParentUnlessPending(): void {\n if (this.pendingParentNotifications > 0) {\n this.pendingParentNotifications--;\n return;\n }\n const parent = this.parent;\n if (parent) {\n this.updateParentValueAndValidity(parent);\n }\n }\n\n private updateParentValueAndValidity(parent: AbstractControl, emitEvent?: boolean): void {\n parent.updateValueAndValidity({emitEvent, sourceControl: this} as any);\n }\n\n private propagateToParent(\n opts: {onlySelf?: boolean} | undefined,\n fn: (parent: AbstractControl) => void,\n ) {\n const parent = this.parent;\n if (parent && !opts?.onlySelf) {\n fn(parent);\n }\n }\n\n override get status(): FormControlStatus {\n if (this.fieldState.disabled()) {\n return 'DISABLED';\n }\n if (this.fieldState.valid()) {\n return 'VALID';\n }\n if (this.fieldState.invalid()) {\n return 'INVALID';\n }\n return 'PENDING';\n }\n\n override get valid(): boolean {\n return this.fieldState.valid();\n }\n\n override get invalid(): boolean {\n return this.fieldState.invalid();\n }\n\n override get pending(): boolean {\n return this.fieldState.pending();\n }\n\n override get disabled(): boolean {\n return this.fieldState.disabled();\n }\n\n override get enabled(): boolean {\n return !this.disabled;\n }\n\n override get dirty(): boolean {\n return this.fieldState.dirty();\n }\n\n override set dirty(_: boolean) {\n throw unsupportedFeatureError(\n ngDevMode && 'Setting dirty directly is not supported. Instead use markAsDirty().',\n );\n }\n\n override get pristine(): boolean {\n return !this.dirty;\n }\n\n override set pristine(_: boolean) {\n throw unsupportedFeatureError(\n ngDevMode && 'Setting pristine directly is not supported. Instead use reset().',\n );\n }\n\n override get touched(): boolean {\n return this.fieldState.touched();\n }\n\n override set touched(_: boolean) {\n throw unsupportedFeatureError(\n ngDevMode &&\n 'Setting touched directly is not supported. Instead use markAsTouched() or reset().',\n );\n }\n\n override get untouched(): boolean {\n return !this.touched;\n }\n\n override set untouched(_: boolean) {\n throw unsupportedFeatureError(\n ngDevMode && 'Setting untouched directly is not supported. Instead use reset().',\n );\n }\n\n override markAsTouched(opts?: {onlySelf?: boolean}): void {\n this.fieldState.markAsTouched();\n this.propagateToParent(opts, (parent) => parent.markAsTouched(opts));\n }\n\n override markAsDirty(opts?: {onlySelf?: boolean}): void {\n this.fieldState.markAsDirty();\n this.propagateToParent(opts, (parent) => parent.markAsDirty(opts));\n }\n\n override markAsPristine(opts?: {onlySelf?: boolean}): void {\n (this.fieldState as FieldNode).markAsPristine();\n this.propagateToParent(opts, (parent) => parent.markAsPristine(opts));\n }\n\n override markAsUntouched(opts?: {onlySelf?: boolean}): void {\n (this.fieldState as FieldNode).markAsUntouched();\n this.propagateToParent(opts, (parent) => parent.markAsUntouched(opts));\n }\n\n override updateValueAndValidity(_opts?: Object): void {}\n\n /** @internal */\n // @ts-ignore\n override _updateValue(): void {}\n\n /** @internal */\n // @ts-ignore\n override _forEachChild(_cb: (c: AbstractControl) => void): void {}\n\n /** @internal */\n // @ts-ignore\n override _anyControls(_condition: (c: AbstractControl) => boolean): boolean {\n return false;\n }\n\n /** @internal */\n // @ts-ignore\n override _allControlsDisabled(): boolean {\n return this.disabled;\n }\n\n /** @internal */\n // @ts-ignore\n override _syncPendingControls(): boolean {\n return false;\n }\n\n override disable(_opts?: {onlySelf?: boolean; emitEvent?: boolean}): void {\n throw unsupportedDisableEnableError();\n }\n\n override enable(_opts?: {onlySelf?: boolean; emitEvent?: boolean}): void {\n throw unsupportedDisableEnableError();\n }\n\n override setValidators(_validators: any): void {\n throw unsupportedValidatorsError();\n }\n\n override setAsyncValidators(_validators: any): void {\n throw unsupportedValidatorsError();\n }\n\n override addValidators(_validators: any): void {\n throw unsupportedValidatorsError();\n }\n\n override addAsyncValidators(_validators: any): void {\n throw unsupportedValidatorsError();\n }\n\n override removeValidators(_validators: any): void {\n throw unsupportedValidatorsError();\n }\n\n override removeAsyncValidators(_validators: any): void {\n throw unsupportedValidatorsError();\n }\n\n override clearValidators(): void {\n throw unsupportedValidatorsError();\n }\n\n override clearAsyncValidators(): void {\n throw unsupportedValidatorsError();\n }\n\n override setErrors(_errors: any, _opts?: {emitEvent?: boolean}): void {\n throw unsupportedFeatureError(\n ngDevMode &&\n 'Imperatively setting errors is not supported in signal forms. Errors are derived from validation rules.',\n );\n }\n\n override markAsPending(_opts?: {onlySelf?: boolean; emitEvent?: boolean}): void {\n throw unsupportedFeatureError(\n ngDevMode &&\n 'Imperatively marking as pending is not supported in signal forms. Pending state is derived from async validation status.',\n );\n }\n}\n\nclass CachingWeakMap<K extends object, V> {\n private readonly map = new WeakMap<K, V>();\n\n getOrCreate(key: K, create: () => V): V {\n const cached = this.map.get(key);\n if (cached) {\n return cached;\n }\n const value = create();\n this.map.set(key, value);\n return value;\n }\n}\n\n/**\n * A FieldTree proxy that patches setters to immediately react on value changes.\n * @param tree\n * @param onUpdate\n */\nfunction wrapFieldTreeForSyncUpdates<T>(tree: FieldTree<T>, onUpdate: () => void): FieldTree<T> {\n const treeCache = new CachingWeakMap<FieldTree<unknown>, FieldTree<unknown>>();\n const stateCache = new CachingWeakMap<FieldState<unknown>, FieldState<unknown>>();\n\n // Takes a FieldState and wraps a value to instantly call onUpdate.\n const wrapState = (state: FieldState<unknown>): FieldState<unknown> => {\n const {value} = state;\n const wrappedValue = Object.assign((...a: unknown[]) => (value as Function)(...a), {\n set: (v: unknown) => {\n value.set(v);\n onUpdate();\n },\n update: (fn: (v: unknown) => unknown) => {\n value.update(fn);\n onUpdate();\n },\n }) as WritableSignal<unknown>;\n return Object.create(state, {value: {get: () => wrappedValue}});\n };\n // Takes a FieldTree and wraps it's state's value to instantly call onUpdate.\n const wrapTree = (t: FieldTree<unknown>): FieldTree<unknown> => {\n return treeCache.getOrCreate(t, () => {\n return new Proxy(t, {\n // When getting a prop, wrap FieldTree if it's a function\n get(target, prop, receiver) {\n const val = Reflect.get(target, prop, receiver);\n // Some of FieldTree children are not function, e.g. length.\n if (typeof val === 'function' && typeof prop === 'string') {\n return wrapTree(val);\n }\n return val;\n },\n // When calling the tree, wrap the returned state\n apply(target, _, args) {\n const state: FieldState<unknown> = (target as Function)(...args);\n return stateCache.getOrCreate(state, () => wrapState(state));\n },\n }) as FieldTree<unknown>;\n });\n };\n\n return wrapTree(tree) as FieldTree<T>;\n}\n\nfunction isFormControlState(formState: unknown): formState is FormControlState<unknown> {\n return (\n typeof formState === 'object' &&\n formState !== null &&\n Object.keys(formState).length === 2 &&\n 'value' in formState &&\n 'disabled' in formState\n );\n}\n\nfunction unsupportedFeatureError(message: string | null): Error {\n return new RuntimeError(RuntimeErrorCode.UNSUPPORTED_FEATURE, message ?? false);\n}\n\nfunction unsupportedDisableEnableError(): Error {\n return unsupportedFeatureError(\n ngDevMode &&\n 'Imperatively changing enabled/disabled status in form control is not supported in signal forms. Instead use a \"disabled\" rule to derive the disabled status from a signal.',\n );\n}\n\nfunction unsupportedValidatorsError(): Error {\n return unsupportedFeatureError(\n ngDevMode &&\n 'Dynamically adding and removing validators is not supported in signal forms. Instead use the \"applyWhen\" rule to conditionally apply validators based on a signal.',\n );\n}\n\nfunction removeListItem<T>(list: T[], el: T): void {\n const index = list.indexOf(el);\n if (index > -1) list.splice(index, 1);\n}\n\nfunction getClosureSafeProperty<T>(objWithPropertyToExtract: T): string {\n for (let key in objWithPropertyToExtract) {\n if (objWithPropertyToExtract[key] === (getClosureSafeProperty as any)) {\n return key;\n }\n }\n throw Error(\n typeof ngDevMode === 'undefined' || ngDevMode\n ? 'Could not find renamed property on target object.'\n : '',\n );\n}\n"],"names":["CompatFieldNode","FieldNode","options","control","constructor","makeCreateDestroySubject","destroy$","ReplaySubject","next","complete","extractControlPropToSignal","makeSignal","injector","getInjectorFromOptions","createDestroySubject","signalOfControlSignal","linkedSignal","ngDevMode","debugName","source","computation","untracked","runInInjectionContext","computed","getControlStatusSignal","getValue","c","toSignal","statusChanges","pipe","map","takeUntil","initialValue","getControlEventsSignal","events","CompatNodeState","FieldNodeState","compatNode","touched","dirty","disabled","controlDisabled","disabledReasons","length","markAsDirty","markAsTouched","markAsPristine","markAsUntouched","getParentFromOptions","kind","undefined","parent","getFieldManagerFromOptions","fieldManager","structure","root","getControlValueSignal","value","valueChanges","getRawValue","set","setValue","update","fn","CompatStructure","FieldNodeStructure","keyInParent","pathKeys","children","signal","childrenMap","isOrphaned","node","logic","RuntimeError","identityInParent","initialKeyInParent","signals","createKeyOrOrphanSignals","getChild","EMPTY_ARRAY_SIGNAL","CompatValidationState","syncValid","errors","pending","invalid","valid","parseErrors","status","extractNestedReactiveErrors","asyncErrors","errorSummary","rawSyncTreeErrors","syncErrors","rawAsyncErrors","shouldSkipValidation","hidden","readonly","calculateValidationSelfStatus","CompatFieldAdapter","basicAdapter","BasicFieldAdapter","newRoot","pathNode","adapter","AbstractControl","createCompatNode","builder","build","fieldAdapter","createNodeState","createStructure","createValidationState","newChild","compatForm","args","model","maybeSchema","maybeOptions","normalizeFormArgs","schema","form","extractValue","field","filter","visitFieldTree","state","matchingChildren","extractChildren","isContainerNode","matchesFilter","isArray","isObject","Object","keys","some","k","isFieldTreeNode","record","arrayValue","result","Array","hasMatch","i","child","childResult","objectValue","entries","key","isKeyedChild","v","fromEntries","enabled","NG_STATUS_CLASSES","ng-touched","ng-untouched","ng-dirty","ng-pristine","ng-valid","ng-invalid","ng-pending","SignalFormControl","fieldTree","sourceValue","fieldState","pendingParentNotifications","onChangeCallbacks","onDisabledChangeCallbacks","EventEmitter","schemaOrOptions","opts","inject","Injector","rawTree","wrapFieldTreeForSyncUpdates","updateValueAndValidity","sourceControl","defineCompatProperties","effect","notifyParentUnlessPending","emit","emitControlEvent","ValueChangeEvent","StatusChangeEvent","isDisabled","isTouched","TouchedChangeEvent","isDirty","PristineChangeEvent","valueProp","getClosureSafeProperty","defineProperty","get","errorsPr