UNPKG

@angular/forms

Version:

Angular - directives and services for creating forms

1 lines • 208 kB
{"version":3,"file":"_structure-chunk.mjs","sources":["../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/resolution.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/util.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/util/type_guards.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/schema/logic.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/schema/logic_node.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/schema/path_node.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/schema/schema.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/api/rules/metadata.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/validation.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/debounce.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/context.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/metadata.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/proxy.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/util/deep_signal.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/structure.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/submit.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/node.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/state.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/field_adapter.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/field/manager.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/util/normalize_form_args.ts","../../../../../darwin_arm64-fastbuild-ST-fdfa778d11ba/bin/packages/forms/signals/src/api/structure.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\nlet boundPathDepth = 0;\n\n/**\n * The depth of the current path when evaluating a logic function.\n * Do not set this directly, it is a context variable managed by `setBoundPathDepthForResolution`.\n */\nexport function getBoundPathDepth() {\n return boundPathDepth;\n}\n\n/**\n * Sets the bound path depth for the duration of the given logic function.\n * This is used to ensure that the field resolution algorithm walks far enough up the field tree to\n * reach the point where the root of the path we're bound to is applied. This normally isn't a big\n * concern, but matters when we're dealing with recursive structures.\n *\n * Consider this example:\n *\n * ```\n * const s = schema(p => {\n * disabled(p.next, ({valueOf}) => valueOf(p.data));\n * apply(p.next, s);\n * });\n * ```\n *\n * Here we need to know that the `disabled` logic was bound to a path of depth 1. Otherwise we'd\n * attempt to resolve `p.data` in the context of the field corresponding to `p.next`.\n * The resolution algorithm would start with the field for `p.next` and see that it *does* contain\n * the logic for `s` (due to the fact that its recursively applied.) It would then decide not to\n * walk up the field tree at all, and to immediately start walking down the keys for the target path\n * `p.data`, leading it to grab the field corresponding to `p.next.data`.\n *\n * We avoid the problem described above by keeping track of the depth (relative to Schema root) of\n * the path we were bound to. We then require the resolution algorithm to walk at least that far up\n * the tree before finding a node that contains the logic for `s`.\n *\n * @param fn A logic function that is bound to a particular path\n * @param depth The depth in the field tree of the field the logic is bound to\n * @returns A version of the logic function that is aware of its depth.\n */\nexport function setBoundPathDepthForResolution<A extends any[], R>(\n fn: (...args: A) => R,\n depth: number,\n): (...args: A) => R {\n return (...args: A) => {\n try {\n boundPathDepth = depth;\n return fn(...args);\n } finally {\n boundPathDepth = 0;\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 type {FieldNodeOptions} from './structure';\n\n/** A shortCircuit function for reduceChildren that short-circuits if the value is false. */\nexport function shortCircuitFalse(value: boolean): boolean {\n return !value;\n}\n\n/** A shortCircuit function for reduceChildren that short-circuits if the value is true. */\nexport function shortCircuitTrue(value: boolean): boolean {\n return value;\n}\n\n/** Recasts the given value as a new type. */\nexport function cast<T>(value: unknown): asserts value is T {}\n\n/**\n * A helper method allowing to get injector regardless of the options type.\n * @param options\n */\nexport function getInjectorFromOptions(options: FieldNodeOptions) {\n if (options.kind === 'root') {\n return options.fieldManager.injector;\n }\n\n return options.parent.structure.root.structure.injector;\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\n/**\n * A version of `Array.isArray` that handles narrowing of readonly arrays properly.\n */\nexport function isArray(value: unknown): value is any[] | readonly any[] {\n return Array.isArray(value);\n}\n\n/**\n * Checks if a value is an object.\n */\nexport function isObject(value: unknown): value is Record<PropertyKey, unknown> {\n return (typeof value === 'object' || typeof value === 'function') && value != null;\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 type {MetadataKey} from '../api/rules/metadata';\nimport type {ValidationError} from '../api/rules/validation/validation_errors';\nimport {DisabledReason, type FieldContext, type LogicFn, type SchemaPath} from '../api/types';\nimport type {FieldNode} from '../field/node';\nimport {cast} from '../field/util';\nimport {isArray} from '../util/type_guards';\n\n/**\n * Special key which is used to represent a dynamic logic property in a `FieldPathNode` path.\n * This property is used to represent logic that applies to every element of some dynamic form data\n * (i.e. an array).\n *\n * For example, a rule like `applyEach(p.myArray, () => { ... })` will add logic to the `DYNAMIC`\n * property of `p.myArray`.\n */\nexport const DYNAMIC: unique symbol = Symbol();\n\n/** Represents a result that should be ignored because its predicate indicates it is not active. */\nconst IGNORED = Symbol();\n\n/**\n * A predicate that indicates whether an `AbstractLogic` instance is currently active, or should be\n * ignored.\n */\nexport interface Predicate {\n /** A boolean logic function that returns true if the logic is considered active. */\n readonly fn: LogicFn<any, boolean>;\n /**\n * The path which this predicate was created for. This is used to determine the correct\n * `FieldContext` to pass to the predicate function.\n */\n readonly path: SchemaPath<any>;\n}\n\n/**\n * Represents a predicate that is bound to a particular depth in the field tree. This is needed for\n * recursively applied logic to ensure that the predicate is evaluated against the correct\n * application of that logic.\n *\n * Consider the following example:\n *\n * ```\n * const s = schema(p => {\n * disabled(p.data);\n * applyWhen(p.next, ({valueOf}) => valueOf(p.data) === 1, s);\n * });\n *\n * const f = form(signal({data: 0, next: {data: 1, next: {data: 2, next: undefined}}}), s);\n *\n * const isDisabled = f.next.next.data().disabled();\n * ```\n *\n * In order to determine `isDisabled` we need to evaluate the predicate from `applyWhen` *twice*.\n * Once to see if the schema should be applied to `f.next` and again to see if it should be applied\n * to `f.next.next`. The `depth` tells us which field we should be evaluating against each time.\n */\nexport interface BoundPredicate extends Predicate {\n /** The depth in the field tree at which this predicate is bound. */\n readonly depth: number;\n}\n\n/**\n * Base class for all logic. It is responsible for combining the results from multiple individual\n * logic functions registered in the schema, and using them to derive the value for some associated\n * piece of field state.\n */\nexport abstract class AbstractLogic<TReturn, TValue = TReturn> {\n /** The set of logic functions that contribute to the value of the associated state. */\n protected readonly fns: Array<LogicFn<any, TValue | typeof IGNORED>> = [];\n\n constructor(\n /**\n * A list of predicates that conditionally enable all logic in this logic instance.\n * The logic is only enabled when *all* of the predicates evaluate to true.\n */\n private predicates: ReadonlyArray<BoundPredicate>,\n ) {}\n\n /**\n * Computes the value of the associated field state based on the logic functions and predicates\n * registered with this logic instance.\n */\n abstract compute(arg: FieldContext<any>): TReturn;\n\n /**\n * The default value that the associated field state should assume if there are no logic functions\n * registered by the schema (or if the logic is disabled by a predicate).\n */\n abstract get defaultValue(): TReturn;\n\n /** Registers a logic function with this logic instance. */\n push(logicFn: LogicFn<any, TValue>) {\n this.fns.push(wrapWithPredicates(this.predicates, logicFn));\n }\n\n /**\n * Merges in the logic from another logic instance, subject to the predicates of both the other\n * instance and this instance.\n */\n mergeIn(other: AbstractLogic<TReturn, TValue>) {\n const fns = this.predicates\n ? other.fns.map((fn) => wrapWithPredicates(this.predicates, fn))\n : other.fns;\n this.fns.push(...fns);\n }\n}\n\n/** Logic that combines its individual logic function results with logical OR. */\nexport class BooleanOrLogic extends AbstractLogic<boolean> {\n override get defaultValue() {\n return false;\n }\n\n override compute(arg: FieldContext<any>): boolean {\n return this.fns.some((f) => {\n const result = f(arg);\n return result && result !== IGNORED;\n });\n }\n}\n\n/**\n * Logic that combines its individual logic function results by aggregating them in an array.\n * Depending on its `ignore` function it may ignore certain values, omitting them from the array.\n */\nexport class ArrayMergeIgnoreLogic<TElement, TIgnore = never> extends AbstractLogic<\n readonly TElement[],\n TElement | readonly (TElement | TIgnore)[] | TIgnore | undefined | void\n> {\n /** Creates an instance of this class that ignores `null` values. */\n static ignoreNull<TElement>(predicates: ReadonlyArray<BoundPredicate>) {\n return new ArrayMergeIgnoreLogic<TElement, null>(predicates, (e: unknown) => e === null);\n }\n\n constructor(\n predicates: ReadonlyArray<BoundPredicate>,\n private ignore: undefined | ((e: TElement | undefined | TIgnore) => e is TIgnore),\n ) {\n super(predicates);\n }\n\n override get defaultValue() {\n return [];\n }\n\n override compute(arg: FieldContext<any>): readonly TElement[] {\n return this.fns.reduce((prev, f) => {\n const value = f(arg);\n\n if (value === undefined || value === IGNORED) {\n return prev;\n } else if (isArray(value)) {\n return [...prev, ...(this.ignore ? value.filter((e) => !this.ignore!(e)) : value)];\n } else {\n if (this.ignore && this.ignore(value as TElement | TIgnore | undefined)) {\n return prev;\n }\n return [...prev, value];\n }\n }, [] as TElement[]);\n }\n}\n\n/** Logic that combines its individual logic function results by aggregating them in an array. */\nexport class ArrayMergeLogic<TElement> extends ArrayMergeIgnoreLogic<TElement, never> {\n constructor(predicates: ReadonlyArray<BoundPredicate>) {\n super(predicates, undefined);\n }\n}\n\n/** Logic that combines metadata according to the keys's reduce function. */\nexport class MetadataMergeLogic<TAcc, TItem> extends AbstractLogic<TAcc, TItem> {\n override get defaultValue() {\n return this.key.reducer.getInitial();\n }\n\n constructor(\n predicates: ReadonlyArray<BoundPredicate>,\n private key: MetadataKey<any, TItem, TAcc>,\n ) {\n super(predicates);\n }\n\n override compute(ctx: FieldContext<any>): TAcc {\n if (this.fns.length === 0) {\n return this.key.reducer.getInitial();\n }\n let acc: TAcc = this.key.reducer.getInitial();\n for (let i = 0; i < this.fns.length; i++) {\n const item = this.fns[i](ctx);\n if (item !== IGNORED) {\n acc = this.key.reducer.reduce(acc, item);\n }\n }\n return acc;\n }\n}\n\n/**\n * Wraps a logic function such that it returns the special `IGNORED` sentinel value if any of the\n * given predicates evaluates to false.\n *\n * @param predicates A list of bound predicates to apply to the logic function\n * @param logicFn The logic function to wrap\n * @returns A wrapped version of the logic function that may return `IGNORED`.\n */\nfunction wrapWithPredicates<TValue, TReturn>(\n predicates: ReadonlyArray<BoundPredicate>,\n logicFn: LogicFn<TValue, TReturn>,\n): LogicFn<TValue, TReturn | typeof IGNORED> {\n if (predicates.length === 0) {\n return logicFn;\n }\n return (arg: FieldContext<any>): TReturn | typeof IGNORED => {\n for (const predicate of predicates) {\n let predicateField = arg.stateOf(predicate.path) as FieldNode;\n // Check the depth of the current field vs the depth this predicate is supposed to be\n // evaluated at. If necessary, walk up the field tree to grab the correct context field.\n // We can check the pathKeys as an untracked read since we know the structure of our fields\n // doesn't change.\n const depthDiff = untracked(predicateField.structure.pathKeys).length - predicate.depth;\n for (let i = 0; i < depthDiff; i++) {\n predicateField = predicateField.structure.parent!;\n }\n // If any of the predicates don't match, don't actually run the logic function, just return\n // the default value.\n if (!predicate.fn(predicateField.context)) {\n return IGNORED;\n }\n }\n return logicFn(arg);\n };\n}\n\n/**\n * Container for all the different types of logic that can be applied to a field\n * (disabled, hidden, errors, etc.)\n */\n\nexport class LogicContainer {\n /** Logic that determines if the field is hidden. */\n readonly hidden: BooleanOrLogic;\n /** Logic that determines reasons for the field being disabled. */\n readonly disabledReasons: ArrayMergeLogic<DisabledReason>;\n /** Logic that determines if the field is read-only. */\n readonly readonly: BooleanOrLogic;\n /** Logic that produces synchronous validation errors for the field. */\n readonly syncErrors: ArrayMergeIgnoreLogic<ValidationError.WithField, null>;\n /** Logic that produces synchronous validation errors for the field's subtree. */\n readonly syncTreeErrors: ArrayMergeIgnoreLogic<ValidationError.WithField, null>;\n /** Logic that produces asynchronous validation results (errors or 'pending'). */\n readonly asyncErrors: ArrayMergeIgnoreLogic<ValidationError.WithField | 'pending', null>;\n /** A map of metadata keys to the `AbstractLogic` instances that compute their values. */\n private readonly metadata = new Map<\n MetadataKey<unknown, unknown, unknown>,\n AbstractLogic<unknown>\n >();\n\n /**\n * Constructs a new `Logic` container.\n * @param predicates An array of predicates that must all be true for the logic\n * functions within this container to be active.\n */\n constructor(private predicates: ReadonlyArray<BoundPredicate>) {\n this.hidden = new BooleanOrLogic(predicates);\n this.disabledReasons = new ArrayMergeLogic(predicates);\n this.readonly = new BooleanOrLogic(predicates);\n this.syncErrors = ArrayMergeIgnoreLogic.ignoreNull<ValidationError.WithField>(predicates);\n this.syncTreeErrors = ArrayMergeIgnoreLogic.ignoreNull<ValidationError.WithField>(predicates);\n this.asyncErrors = ArrayMergeIgnoreLogic.ignoreNull<ValidationError.WithField | 'pending'>(\n predicates,\n );\n }\n\n /** Checks whether there is logic for the given metadata key. */\n hasMetadata(key: MetadataKey<any, any, any>) {\n return this.metadata.has(key);\n }\n\n /**\n * Gets an iterable of [metadata key, logic function] pairs.\n * @returns An iterable of metadata keys.\n */\n getMetadataKeys() {\n return this.metadata.keys();\n }\n\n /**\n * Retrieves or creates the `AbstractLogic` for a given metadata key.\n * @param key The `MetadataKey` for which to get the logic.\n * @returns The `AbstractLogic` associated with the key.\n */\n getMetadata<T>(key: MetadataKey<any, T, any>): AbstractLogic<T> {\n cast<MetadataKey<unknown, unknown, unknown>>(key);\n if (!this.metadata.has(key)) {\n this.metadata.set(key, new MetadataMergeLogic(this.predicates, key));\n }\n return this.metadata.get(key)! as AbstractLogic<T>;\n }\n\n /**\n * Merges logic from another `Logic` instance into this one.\n * @param other The `Logic` instance to merge from.\n */\n mergeIn(other: LogicContainer) {\n this.hidden.mergeIn(other.hidden);\n this.disabledReasons.mergeIn(other.disabledReasons);\n this.readonly.mergeIn(other.readonly);\n this.syncErrors.mergeIn(other.syncErrors);\n this.syncTreeErrors.mergeIn(other.syncTreeErrors);\n this.asyncErrors.mergeIn(other.asyncErrors);\n for (const key of other.getMetadataKeys()) {\n const metadataLogic = other.metadata.get(key)!;\n this.getMetadata(key).mergeIn(metadataLogic);\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 type {MetadataKey} from '../api/rules/metadata';\nimport type {ValidationError} from '../api/rules/validation/validation_errors';\nimport type {AsyncValidationResult, DisabledReason, LogicFn, ValidationResult} from '../api/types';\nimport {setBoundPathDepthForResolution} from '../field/resolution';\nimport {type BoundPredicate, DYNAMIC, LogicContainer, type Predicate} from './logic';\n\n/**\n * Abstract base class for building a `LogicNode`.\n * This class defines the interface for adding various logic rules (e.g., hidden, disabled)\n * and data factories to a node in the logic tree.\n * LogicNodeBuilders are 1:1 with nodes in the Schema tree.\n */\nexport abstract class AbstractLogicNodeBuilder {\n constructor(\n /** The depth of this node in the schema tree. */\n protected readonly depth: number,\n ) {}\n\n /** Adds a rule to determine if a field should be hidden. */\n abstract addHiddenRule(logic: LogicFn<any, boolean>): void;\n\n /** Adds a rule to determine if a field should be disabled, and for what reason. */\n abstract addDisabledReasonRule(logic: LogicFn<any, DisabledReason | undefined>): void;\n\n /** Adds a rule to determine if a field should be read-only. */\n abstract addReadonlyRule(logic: LogicFn<any, boolean>): void;\n\n /** Adds a rule for synchronous validation errors for a field. */\n abstract addSyncErrorRule(logic: LogicFn<any, ValidationResult>): void;\n\n /** Adds a rule for synchronous validation errors that apply to a subtree. */\n abstract addSyncTreeErrorRule(logic: LogicFn<any, ValidationResult>): void;\n\n /** Adds a rule for asynchronous validation errors for a field. */\n abstract addAsyncErrorRule(logic: LogicFn<any, AsyncValidationResult>): void;\n\n /** Adds a rule to compute metadata for a field. */\n abstract addMetadataRule<M>(key: MetadataKey<unknown, M, unknown>, logic: LogicFn<any, M>): void;\n\n /**\n * Gets a builder for a child node associated with the given property key.\n * @param key The property key of the child.\n * @returns A `LogicNodeBuilder` for the child.\n */\n abstract getChild(key: PropertyKey): LogicNodeBuilder;\n\n /**\n * Checks whether a particular `AbstractLogicNodeBuilder` has been merged into this one.\n * @param builder The builder to check for.\n * @returns True if the builder has been merged, false otherwise.\n */\n abstract hasLogic(builder: AbstractLogicNodeBuilder): boolean;\n\n /**\n * Builds the `LogicNode` from the accumulated rules and child builders.\n * @returns The constructed `LogicNode`.\n */\n build(): LogicNode {\n return new LeafLogicNode(this, [], 0);\n }\n}\n\n/**\n * A builder for `LogicNode`. Used to add logic to the final `LogicNode` tree.\n * This builder supports merging multiple sources of logic, potentially with predicates,\n * preserving the order of rule application.\n */\nexport class LogicNodeBuilder extends AbstractLogicNodeBuilder {\n constructor(depth: number) {\n super(depth);\n }\n\n /**\n * The current `NonMergeableLogicNodeBuilder` being used to add rules directly to this\n * `LogicNodeBuilder`. Do not use this directly, call `getCurrent()` which will create a current\n * builder if there is none.\n */\n private current: NonMergeableLogicNodeBuilder | undefined;\n /**\n * Stores all builders that contribute to this node, along with any predicates\n * that gate their application.\n */\n readonly all: {builder: AbstractLogicNodeBuilder; predicate?: Predicate}[] = [];\n\n override addHiddenRule(logic: LogicFn<any, boolean>): void {\n this.getCurrent().addHiddenRule(logic);\n }\n\n override addDisabledReasonRule(logic: LogicFn<any, DisabledReason | undefined>): void {\n this.getCurrent().addDisabledReasonRule(logic);\n }\n\n override addReadonlyRule(logic: LogicFn<any, boolean>): void {\n this.getCurrent().addReadonlyRule(logic);\n }\n\n override addSyncErrorRule(\n logic: LogicFn<any, ValidationResult<ValidationError.WithField>>,\n ): void {\n this.getCurrent().addSyncErrorRule(logic);\n }\n\n override addSyncTreeErrorRule(\n logic: LogicFn<any, ValidationResult<ValidationError.WithField>>,\n ): void {\n this.getCurrent().addSyncTreeErrorRule(logic);\n }\n\n override addAsyncErrorRule(\n logic: LogicFn<any, AsyncValidationResult<ValidationError.WithField>>,\n ): void {\n this.getCurrent().addAsyncErrorRule(logic);\n }\n\n override addMetadataRule<T>(key: MetadataKey<unknown, T, any>, logic: LogicFn<any, T>): void {\n this.getCurrent().addMetadataRule(key, logic);\n }\n\n override getChild(key: PropertyKey): LogicNodeBuilder {\n // Close off the current builder if the key is DYNAMIC and the current builder already has logic\n // for some non-DYNAMIC key. This guarantees that all of the DYNAMIC logic always comes before\n // all of the specific-key logic for any given instance of `NonMergeableLogicNodeBuilder`.\n // We rely on this fact in `getAllChildBuilder` to know that we can return the DYNAMIC logic,\n // followed by the property-specific logic, in that order.\n if (key === DYNAMIC) {\n const children = this.getCurrent().children;\n // Use the children size to determine if there is logic registered for a property other than\n // the DYNAMIC property.\n // - If the children map doesn't have DYNAMIC logic, but the size is still >0 then we know it\n // has logic for some other property.\n // - If the children map does have DYNAMIC logic then its size is going to be at least 1,\n // because it has the DYNAMIC key. However if it is >1, then we again know it contains other\n // keys.\n if (children.size > (children.has(DYNAMIC) ? 1 : 0)) {\n this.current = undefined;\n }\n }\n return this.getCurrent().getChild(key);\n }\n\n override hasLogic(builder: AbstractLogicNodeBuilder): boolean {\n if (this === builder) {\n return true;\n }\n return this.all.some(({builder: subBuilder}) => subBuilder.hasLogic(builder));\n }\n\n /**\n * Merges logic from another `LogicNodeBuilder` into this one.\n * If a `predicate` is provided, all logic from the `other` builder will only apply\n * when the predicate evaluates to true.\n * @param other The `LogicNodeBuilder` to merge in.\n * @param predicate An optional predicate to gate the merged logic.\n */\n mergeIn(other: LogicNodeBuilder, predicate?: Predicate): void {\n // Add the other builder to our collection, we'll defer the actual merging of the logic until\n // the logic node is requested to be created. In order to preserve the original ordering of the\n // rules, we close off the current builder to any further edits. If additional logic is added,\n // a new current builder will be created to capture it.\n if (predicate) {\n this.all.push({\n builder: other,\n predicate: {\n fn: setBoundPathDepthForResolution(predicate.fn, this.depth),\n path: predicate.path,\n },\n });\n } else {\n this.all.push({builder: other});\n }\n this.current = undefined;\n }\n\n /**\n * Gets the current `NonMergeableLogicNodeBuilder` for adding rules directly to this\n * `LogicNodeBuilder`. If no current builder exists, a new one is created.\n * The current builder is cleared whenever `mergeIn` is called to preserve the order\n * of rules when merging separate builder trees.\n * @returns The current `NonMergeableLogicNodeBuilder`.\n */\n private getCurrent(): NonMergeableLogicNodeBuilder {\n if (this.current === undefined) {\n this.current = new NonMergeableLogicNodeBuilder(this.depth);\n this.all.push({builder: this.current});\n }\n return this.current;\n }\n\n /**\n * Creates a new root `LogicNodeBuilder`.\n * @returns A new instance of `LogicNodeBuilder`.\n */\n static newRoot(): LogicNodeBuilder {\n return new LogicNodeBuilder(0);\n }\n}\n\n/**\n * A type of `AbstractLogicNodeBuilder` used internally by the `LogicNodeBuilder` to record \"pure\"\n * chunks of logic that do not require merging in other builders.\n */\nclass NonMergeableLogicNodeBuilder extends AbstractLogicNodeBuilder {\n /** The collection of logic rules directly added to this builder. */\n readonly logic = new LogicContainer([]);\n /**\n * A map of child property keys to their corresponding `LogicNodeBuilder` instances.\n * This allows for building a tree of logic.\n */\n readonly children = new Map<PropertyKey, LogicNodeBuilder>();\n\n constructor(depth: number) {\n super(depth);\n }\n\n override addHiddenRule(logic: LogicFn<any, boolean>): void {\n this.logic.hidden.push(setBoundPathDepthForResolution(logic, this.depth));\n }\n\n override addDisabledReasonRule(logic: LogicFn<any, DisabledReason | undefined>): void {\n this.logic.disabledReasons.push(setBoundPathDepthForResolution(logic, this.depth));\n }\n\n override addReadonlyRule(logic: LogicFn<any, boolean>): void {\n this.logic.readonly.push(setBoundPathDepthForResolution(logic, this.depth));\n }\n\n override addSyncErrorRule(\n logic: LogicFn<any, ValidationResult<ValidationError.WithField>>,\n ): void {\n this.logic.syncErrors.push(setBoundPathDepthForResolution(logic, this.depth));\n }\n\n override addSyncTreeErrorRule(\n logic: LogicFn<any, ValidationResult<ValidationError.WithField>>,\n ): void {\n this.logic.syncTreeErrors.push(setBoundPathDepthForResolution(logic, this.depth));\n }\n\n override addAsyncErrorRule(\n logic: LogicFn<any, AsyncValidationResult<ValidationError.WithField>>,\n ): void {\n this.logic.asyncErrors.push(setBoundPathDepthForResolution(logic, this.depth));\n }\n\n override addMetadataRule<T>(key: MetadataKey<unknown, T, unknown>, logic: LogicFn<any, T>): void {\n this.logic.getMetadata(key).push(setBoundPathDepthForResolution(logic, this.depth));\n }\n\n override getChild(key: PropertyKey): LogicNodeBuilder {\n if (!this.children.has(key)) {\n this.children.set(key, new LogicNodeBuilder(this.depth + 1));\n }\n return this.children.get(key)!;\n }\n\n override hasLogic(builder: AbstractLogicNodeBuilder): boolean {\n return this === builder;\n }\n}\n\n/**\n * Represents a node in the logic tree, containing all logic applicable\n * to a specific field or path in the form structure.\n * LogicNodes are 1:1 with nodes in the Field tree.\n */\nexport interface LogicNode {\n /** The collection of logic rules (hidden, disabled, errors, etc.) for this node. */\n readonly logic: LogicContainer;\n\n /**\n * Retrieves the `LogicNode` for a child identified by the given property key.\n * @param key The property key of the child.\n * @returns The `LogicNode` for the specified child.\n */\n getChild(key: PropertyKey): LogicNode;\n\n /**\n * Checks whether the logic from a particular `AbstractLogicNodeBuilder` has been merged into this\n * node.\n * @param builder The builder to check for.\n * @returns True if the builder has been merged, false otherwise.\n */\n hasLogic(builder: AbstractLogicNodeBuilder): boolean;\n}\n\n/**\n * A tree structure of `Logic` corresponding to a tree of fields.\n * This implementation represents a leaf in the sense that its logic is derived\n * from a single builder.\n */\nclass LeafLogicNode implements LogicNode {\n /** The computed logic for this node. */\n readonly logic: LogicContainer;\n\n /**\n * Constructs a `LeafLogicNode`.\n * @param builder The `AbstractLogicNodeBuilder` from which to derive the logic.\n * If undefined, an empty `Logic` instance is created.\n * @param predicates An array of predicates that gate the logic from the builder.\n */\n constructor(\n private builder: AbstractLogicNodeBuilder | undefined,\n private predicates: BoundPredicate[],\n /** The depth of this node in the field tree. */\n private depth: number,\n ) {\n this.logic = builder ? createLogic(builder, predicates, depth) : new LogicContainer([]);\n }\n\n // TODO: cache here, or just rely on the user of this API to do caching?\n /**\n * Retrieves the `LogicNode` for a child identified by the given property key.\n * @param key The property key of the child.\n * @returns The `LogicNode` for the specified child.\n */\n getChild(key: PropertyKey): LogicNode {\n // The logic for a particular child may be spread across multiple builders. We lazily combine\n // this logic at the time the child logic node is requested to be created.\n const childBuilders = this.builder ? getAllChildBuilders(this.builder, key) : [];\n if (childBuilders.length === 0) {\n return new LeafLogicNode(undefined, [], this.depth + 1);\n } else if (childBuilders.length === 1) {\n const {builder, predicates} = childBuilders[0];\n return new LeafLogicNode(\n builder,\n [...this.predicates, ...predicates.map((p) => bindLevel(p, this.depth))],\n this.depth + 1,\n );\n } else {\n const builtNodes = childBuilders.map(\n ({builder, predicates}) =>\n new LeafLogicNode(\n builder,\n [...this.predicates, ...predicates.map((p) => bindLevel(p, this.depth))],\n this.depth + 1,\n ),\n );\n return new CompositeLogicNode(builtNodes);\n }\n }\n\n /**\n * Checks whether the logic from a particular `AbstractLogicNodeBuilder` has been merged into this\n * node.\n * @param builder The builder to check for.\n * @returns True if the builder has been merged, false otherwise.\n */\n hasLogic(builder: AbstractLogicNodeBuilder): boolean {\n return this.builder?.hasLogic(builder) ?? false;\n }\n}\n\n/**\n * A `LogicNode` that represents the composition of multiple `LogicNode` instances.\n * This is used when logic for a particular path is contributed by several distinct\n * builder branches that need to be merged.\n */\nclass CompositeLogicNode implements LogicNode {\n /** The merged logic from all composed nodes. */\n readonly logic: LogicContainer;\n\n /**\n * Constructs a `CompositeLogicNode`.\n * @param all An array of `LogicNode` instances to compose.\n */\n constructor(private all: LogicNode[]) {\n this.logic = new LogicContainer([]);\n for (const node of all) {\n this.logic.mergeIn(node.logic);\n }\n }\n\n /**\n * Retrieves the child `LogicNode` by composing the results of `getChild` from all\n * underlying `LogicNode` instances.\n * @param key The property key of the child.\n * @returns A `CompositeLogicNode` representing the composed child.\n */\n getChild(key: PropertyKey): LogicNode {\n return new CompositeLogicNode(this.all.flatMap((child) => child.getChild(key)));\n }\n\n /**\n * Checks whether the logic from a particular `AbstractLogicNodeBuilder` has been merged into this\n * node.\n * @param builder The builder to check for.\n * @returns True if the builder has been merged, false otherwise.\n */\n hasLogic(builder: AbstractLogicNodeBuilder): boolean {\n return this.all.some((node) => node.hasLogic(builder));\n }\n}\n\n/**\n * Gets all of the builders that contribute logic to the given child of the parent builder.\n * This function recursively traverses the builder hierarchy.\n * @param builder The parent `AbstractLogicNodeBuilder`.\n * @param key The property key of the child.\n * @returns An array of objects, each containing a `LogicNodeBuilder` for the child and any associated predicates.\n */\nfunction getAllChildBuilders(\n builder: AbstractLogicNodeBuilder,\n key: PropertyKey,\n): {builder: LogicNodeBuilder; predicates: Predicate[]}[] {\n if (builder instanceof LogicNodeBuilder) {\n return builder.all.flatMap(({builder, predicate}) => {\n const children = getAllChildBuilders(builder, key);\n if (predicate) {\n return children.map(({builder, predicates}) => ({\n builder,\n predicates: [...predicates, predicate],\n }));\n }\n return children;\n });\n } else if (builder instanceof NonMergeableLogicNodeBuilder) {\n return [\n // DYNAMIC logic always comes first for any individual `NonMergeableLogicNodeBuilder`.\n // This assumption is guaranteed by the behavior of `LogicNodeBuilder.getChild`.\n // Therefore we can return all of the DYNAMIC logic, followed by all of the property-specific\n // logic.\n ...(key !== DYNAMIC && builder.children.has(DYNAMIC)\n ? [{builder: builder.getChild(DYNAMIC), predicates: []}]\n : []),\n ...(builder.children.has(key) ? [{builder: builder.getChild(key), predicates: []}] : []),\n ];\n } else {\n throw new Error('Unknown LogicNodeBuilder type');\n }\n}\n\n/**\n * Creates the full `Logic` for a given builder.\n * This function handles different types of builders (`LogicNodeBuilder`, `NonMergeableLogicNodeBuilder`)\n * and applies the provided predicates.\n * @param builder The `AbstractLogicNodeBuilder` to process.\n * @param predicates Predicates to apply to the logic derived from the builder.\n * @param depth The depth in the field tree of the field which this logic applies to.\n * @returns The `Logic` instance.\n */\nfunction createLogic(\n builder: AbstractLogicNodeBuilder,\n predicates: BoundPredicate[],\n depth: number,\n): LogicContainer {\n const logic = new LogicContainer(predicates);\n if (builder instanceof LogicNodeBuilder) {\n const builtNodes = builder.all.map(\n ({builder, predicate}) =>\n new LeafLogicNode(\n builder,\n predicate ? [...predicates, bindLevel(predicate, depth)] : predicates,\n depth,\n ),\n );\n for (const node of builtNodes) {\n logic.mergeIn(node.logic);\n }\n } else if (builder instanceof NonMergeableLogicNodeBuilder) {\n logic.mergeIn(builder.logic);\n } else {\n throw new Error('Unknown LogicNodeBuilder type');\n }\n return logic;\n}\n\n/**\n * Create a bound version of the given predicate to a specific depth in the field tree.\n * This allows us to unambiguously know which `FieldContext` the predicate function should receive.\n *\n * This is of particular concern when a schema is applied recursively to itself. Since the schema is\n * only compiled once, each nested application adds the same predicate instance. We differentiate\n * these by recording the depth of the field they're bound to.\n *\n * @param predicate The unbound predicate\n * @param depth The depth of the field the predicate is bound to\n * @returns A bound predicate\n */\nfunction bindLevel(predicate: Predicate, depth: number): BoundPredicate {\n return {...predicate, depth: depth};\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 */\nimport type {SchemaPath, SchemaPathRules} from '../api/types';\nimport type {Predicate} from './logic';\nimport {LogicNodeBuilder} from './logic_node';\nimport type {SchemaImpl} from './schema';\n\n/**\n * Special key which is used to retrieve the `FieldPathNode` instance from its `FieldPath` proxy wrapper.\n */\nconst PATH = Symbol('PATH');\n\n/**\n * A path in the schema on which logic is stored so that it can be added to the corresponding field\n * when the field is created.\n */\nexport class FieldPathNode {\n /** The root path node from which this path node is descended. */\n readonly root: FieldPathNode;\n\n /**\n * A map containing all child path nodes that have been created on this path.\n * Child path nodes are created automatically on first access if they do not exist already.\n */\n private readonly children = new Map<PropertyKey, FieldPathNode>();\n\n /**\n * A proxy that wraps the path node, allowing navigation to its child paths via property access.\n */\n readonly fieldPathProxy: SchemaPath<any> = new Proxy(\n this,\n FIELD_PATH_PROXY_HANDLER,\n ) as unknown as SchemaPath<any>;\n\n /**\n * For a root path node this will contain the root logic builder. For non-root nodes,\n * they determine their logic builder from their parent so this is undefined.\n */\n private readonly logicBuilder: LogicNodeBuilder | undefined;\n\n protected constructor(\n /** The property keys used to navigate from the root path to this path. */\n readonly keys: PropertyKey[],\n root: FieldPathNode | undefined,\n /** The parent of this path node. */\n private readonly parent: FieldPathNode | undefined,\n /** The key of this node in its parent. */\n private readonly keyInParent: PropertyKey | undefined,\n ) {\n this.root = root ?? this;\n if (!parent) {\n this.logicBuilder = LogicNodeBuilder.newRoot();\n }\n }\n\n /** The logic builder used to accumulate logic on this path node. */\n get builder(): LogicNodeBuilder {\n if (this.logicBuilder) {\n return this.logicBuilder;\n }\n return this.parent!.builder.getChild(this.keyInParent!);\n }\n\n /**\n * Gets the path node for the given child property key.\n * Child paths are created automatically on first access if they do not exist already.\n */\n getChild(key: PropertyKey): FieldPathNode {\n if (!this.children.has(key)) {\n this.children.set(key, new FieldPathNode([...this.keys, key], this.root, this, key));\n }\n return this.children.get(key)!;\n }\n\n /**\n * Merges in logic from another schema to this one.\n * @param other The other schema to merge in the logic from\n * @param predicate A predicate indicating when the merged in logic should be active.\n */\n mergeIn(other: SchemaImpl, predicate?: Predicate) {\n const path = other.compile();\n this.builder.mergeIn(path.builder, predicate);\n }\n\n /** Extracts the underlying path node from the given path proxy. */\n static unwrapFieldPath(formPath: SchemaPath<unknown, SchemaPathRules>): FieldPathNode {\n return (formPath as any)[PATH] as FieldPathNode;\n }\n\n /** Creates a new root path node to be passed in to a schema function. */\n static newRoot() {\n return new FieldPathNode([], undefined, undefined, undefined);\n }\n}\n\n/** Proxy handler which implements `FieldPath` on top of a `FieldPathNode`. */\nexport const FIELD_PATH_PROXY_HANDLER: ProxyHandler<FieldPathNode> = {\n get(node: FieldPathNode, property: string | symbol) {\n if (property === PATH) {\n return node;\n }\n\n return node.getChild(property).fieldPathProxy;\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 {SchemaPath, SchemaFn, SchemaOrSchemaFn} from '../api/types';\nimport {FieldPathNode} from './path_node';\n\n/**\n * Keeps track of the path node for the schema function that is currently being compiled. This is\n * used to detect erroneous references to a path node outside of the context of its schema function.\n * Do not set this directly, it is a context variable managed by `SchemaImpl.compile`.\n */\nlet currentCompilingNode: FieldPathNode | undefined = undefined;\n\n/**\n * A cache of all schemas compiled under the current root compilation. This is used to avoid doing\n * extra work when compiling a schema that reuses references to the same sub-schema. For example:\n *\n * ```\n * const sub = schema(p => ...);\n * const s = schema(p => {\n * apply(p.a, sub);\n * apply(p.b, sub);\n * });\n * ```\n *\n * This also ensures that we don't go into an infinite loop when compiling a schema that references\n * itself.\n *\n * Do not directly add or remove entries from this map, it is a context variable managed by\n * `SchemaImpl.compile` and `SchemaImpl.rootCompile`.\n */\nconst compiledSchemas = new Map<SchemaImpl, FieldPathNode>();\n\n/**\n * Implements the `Schema` concept.\n */\nexport class SchemaImpl {\n constructor(private schemaFn: SchemaFn<unknown>) {}\n\n /**\n * Compiles this schema within the current root compilation context. If the schema was previously\n * compiled within this context, we reuse the cached FieldPathNode, otherwise we create a new one\n * and cache it in the compilation context.\n */\n compile(): FieldPathNode {\n if (compiledSchemas.has(this)) {\n return compiledSchemas.get(this)!;\n }\n const path = FieldPathNode.newRoot();\n compiledSchemas.set(this, path);\n let prevCompilingNode = currentCompilingNode;\n try {\n currentCompilingNode = path;\n this.schemaFn(path.fieldPathProxy);\n } finally {\n // Use a try/finally to ensure we restore the previous root upon completion,\n // even if there are errors while compiling the schema.\n currentCompilingNode = prevCompilingNode;\n }\n return path;\n }\n\n /**\n * Creates a SchemaImpl from the given SchemaOrSchemaFn.\n */\n static create(schema: SchemaImpl | SchemaOrSchemaFn<any>) {\n if (schema instanceof SchemaImpl) {\n return schema;\n }\n return new SchemaImpl(schema as SchemaFn<unknown>);\n }\n\n /**\n * Compiles the given schema in a fresh compilation context. This clears the cached results of any\n * previous compilations.\n */\n static rootCompile(schema: SchemaImpl | SchemaOrSchemaFn<any> | undefined): FieldPathNode {\n try {\n compiledSchemas.clear();\n if (schema === undefined) {\n return FieldPathNode.newRoot();\n }\n if (schema instanceof SchemaImpl) {\n return schema.compile();\n }\n return new SchemaImpl(schema as SchemaFn<unknown>).compile();\n } finally {\n // Use a try/finally to ensure we properly reset the compilation context upon completion,\n // even if there are errors while compiling the schema.\n compiledSchemas.clear();\n }\n }\n}\n\n/** Checks if the given value is a schema or schema function. */\nexport function isSchemaOrSchemaFn(value: unknown): value is SchemaOrSchemaFn<unknown> {\n return value instanceof SchemaImpl || typeof value === 'function';\n}\n\n/** Checks that a path node belongs to the schema function currently being compiled. */\nexport function assertPathIsCurrent(path: SchemaPath<unknown>): void {\n if (currentCompilingNode !== FieldPathNode.unwrapFieldPath(path).root) {\n throw new Error(\n `A FieldPath can only be used directly within the Schema that owns it,` +\n ` **not** outside of it or within a sub-schema.`,\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 {type Signal} from '@angular/core';\nimport {FieldPathNode} from '../../schema/path_node';\nimport {assertPathIsCurrent} from '../../schema/schema';\nimport type {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';\n\n/**\n * Sets a value for the {@link MetadataKey} for this field.\n *\n * This value is combined via a reduce operation defined by the particular key,\n * since multiple rules in the schema might set values for it.\n *\n * @param path The target path to set the metadata for.\n * @param key The metadata key\n * @param logic A function that receives the `FieldContext` and returns a value for the metadata.\n * @template TValue The type of value stored in the field the logic is bound to.\n * @template TKey The type of metadata key.\n * @template TPathKind The kind of path the logic is bound to (a root path, child path, or item of an array)\n *\n * @category logic\n * @experimental 21.0.0\n */\nexport function metadata<\n TValue,\n TKey extends MetadataKey<any, any, any>,\n TPathKind extends PathKind = PathKind.Root,\n>(\n path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,\n key: TKey,\n logic: NoInfer<LogicFn<TValue, MetadataSetterType<TKey>, TPathKind>>,\n): TKey {\n assertPathIsCurrent(path);\n\n const pathNode = FieldPathNode.unwrapFieldPath(path);\n pathNode.builder.addMetadataRule(key, logic);\n return key;\n}\n\n/**\n * A reducer that determines the accumulated value for a metadata key by reducing the individual\n * values contributed from `metadata()` rules.\n *\n * @template TAcc The accumulated type of the reduce operation.\n * @template TItem The type of the individual items that are reduced over.\n * @experimental 21.0.2\n */\nexport interface MetadataReducer<TAcc, TItem> {\n /** The reduce function. */\n reduce: (acc: TAcc, item: TItem) => TAcc;\n /** Gets the initial accumulated value. */\n getInitial: () => TAcc;\n}\nexport const MetadataReducer = {\n /** Creates a reducer that accumulates a list of its individual item values. */\n list<TItem>(): MetadataReducer<TItem[], TItem | undefined> {\n return {\n reduce: (acc, item) => (item === undefined ? acc : [...acc, item]),\n getInitial: () => [],\n };\n },\n\n /** Creates a reducer that accumulates the min of its individual item values. */\n min(): MetadataReducer<number | undefined, number | undefined> {\n return {\n reduce: (acc, item) => {\n if (acc === undefined || item === undefined) {\n return acc ?? item;\n }\n return Math.min(acc, item);\n },\n getInitial: () => undefined,\n };\n },\n\n /** Creates a reducer that accumulates a the max of its individual item values. */\n max(): MetadataReducer<number | undefined, number | undefined> {\n return {\n reduce: (prev, next) => {\n if (prev === undefined || next === undefined) {\n return prev ?? next;\n }\n return Math.max(prev, next);\n },\n getInitial: () => undefined,\n };\n },\n\n /** Creates a reducer that logically or's its accumulated value with each individual item value. */\n or(): MetadataReducer<boolean, boolean> {\n return {\n reduce: (prev, next) => prev || next,\n getInitial: () => false,\n };\n },\n\n /** Creates a reducer that logically and's its accumulated value with each individual item value. */\n and(): MetadataReducer<boolean, boolean> {\n return {\n reduce: (prev, next) => prev && next,\n getInitial: () => true,\n };\n },\n\n /** Creates a reducer that always takes the next individual item value as the accumulated value. */\n override,\n} as const;\n\nfunction override<T>(): MetadataReducer<T | undefined, T>;\nfunction override<T>(getInitial: () => T): MetadataReducer<T, T>;\nfunction override<T>(getInitial?: () => T): MetadataReducer<T | undefined, T> {\n return {\n reduce: (_, item) => item,\n getInitial: () => getInitial?.(),\n };\n}\n\n/**\n * Represents metadata that is aggregated from multiple parts according to the key's reducer\n * function. A value can be contributed to the aggregated value for a field using an\n * `metadata` rule