/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/forms/signals/src/schema/logic_node.ts
555 строк
20 KB
Alex Rickabaugh
perf(forms): lazily instantiate signal form fields
21 мар 2026, 01:09
21 мар 2026, 01:09
98c5afd
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {ɵRuntimeError as RuntimeError} from '@angular/core'; import {RuntimeErrorCode} from '../errors'; import type {MetadataKey, ValidationError} from '../api/rules'; import type {AsyncValidationResult, DisabledReason, LogicFn, ValidationResult} from '../api/types'; import {setBoundPathDepthForResolution} from '../field/resolution'; import {type BoundPredicate, DYNAMIC, LogicContainer, type Predicate} from './logic'; /** * Abstract base class for building a `LogicNode`. * This class defines the interface for adding various logic rules (e.g., hidden, disabled) * and data factories to a node in the logic tree. * LogicNodeBuilders are 1:1 with nodes in the Schema tree. */ export abstract class AbstractLogicNodeBuilder { constructor( /** The depth of this node in the schema tree. */ protected readonly depth: number, ) {} /** Adds a rule to determine if a field should be hidden. */ abstract addHiddenRule(logic: LogicFn<any, boolean>): void; /** Adds a rule to determine if a field should be disabled, and for what reason. */ abstract addDisabledReasonRule(logic: LogicFn<any, DisabledReason | undefined>): void; /** Adds a rule to determine if a field should be read-only. */ abstract addReadonlyRule(logic: LogicFn<any, boolean>): void; /** Adds a rule for synchronous validation errors for a field. */ abstract addSyncErrorRule(logic: LogicFn<any, ValidationResult>): void; /** Adds a rule for synchronous validation errors that apply to a subtree. */ abstract addSyncTreeErrorRule(logic: LogicFn<any, ValidationResult>): void; /** Adds a rule for asynchronous validation errors for a field. */ abstract addAsyncErrorRule(logic: LogicFn<any, AsyncValidationResult>): void; /** Adds a rule to compute metadata for a field. */ abstract addMetadataRule<M>(key: MetadataKey<unknown, M, unknown>, logic: LogicFn<any, M>): void; /** * Gets a builder for a child node associated with the given property key. * @param key The property key of the child. * @returns A `LogicNodeBuilder` for the child. */ abstract getChild(key: PropertyKey): LogicNodeBuilder; /** * Checks whether a particular `AbstractLogicNodeBuilder` has been merged into this one. * @param builder The builder to check for. * @returns True if the builder has been merged, false otherwise. */ abstract hasLogic(builder: AbstractLogicNodeBuilder): boolean; /** * Checks whether this builder or any of its children have any logic rules defined. * @returns True if rules exist, false otherwise. */ abstract hasRules(): boolean; /** * Checks whether any of the children of this builder have any logic rules defined. * @returns True if rules exist on any child, false otherwise. */ abstract anyChildHasLogic(): boolean; /** * Builds the `LogicNode` from the accumulated rules and child builders. * @returns The constructed `LogicNode`. */ build(): LogicNode { return new LeafLogicNode(this, [], 0); } } /** * A builder for `LogicNode`. Used to add logic to the final `LogicNode` tree. * This builder supports merging multiple sources of logic, potentially with predicates, * preserving the order of rule application. */ export class LogicNodeBuilder extends AbstractLogicNodeBuilder { constructor(depth: number) { super(depth); } /** * The current `NonMergeableLogicNodeBuilder` being used to add rules directly to this * `LogicNodeBuilder`. Do not use this directly, call `getCurrent()` which will create a current * builder if there is none. */ private current: NonMergeableLogicNodeBuilder | undefined; /** * Stores all builders that contribute to this node, along with any predicates * that gate their application. */ readonly all: {builder: AbstractLogicNodeBuilder; predicate?: Predicate}[] = []; override addHiddenRule(logic: LogicFn<any, boolean>): void { this.getCurrent().addHiddenRule(logic); } override addDisabledReasonRule(logic: LogicFn<any, DisabledReason | undefined>): void { this.getCurrent().addDisabledReasonRule(logic); } override addReadonlyRule(logic: LogicFn<any, boolean>): void { this.getCurrent().addReadonlyRule(logic); } override addSyncErrorRule( logic: LogicFn<any, ValidationResult<ValidationError.WithFieldTree>>, ): void { this.getCurrent().addSyncErrorRule(logic); } override addSyncTreeErrorRule( logic: LogicFn<any, ValidationResult<ValidationError.WithFieldTree>>, ): void { this.getCurrent().addSyncTreeErrorRule(logic); } override addAsyncErrorRule( logic: LogicFn<any, AsyncValidationResult<ValidationError.WithFieldTree>>, ): void { this.getCurrent().addAsyncErrorRule(logic); } override addMetadataRule<T>(key: MetadataKey<unknown, T, any>, logic: LogicFn<any, T>): void { this.getCurrent().addMetadataRule(key, logic); } override getChild(key: PropertyKey): LogicNodeBuilder { // Close off the current builder if the key is DYNAMIC and the current builder already has logic // for some non-DYNAMIC key. This guarantees that all of the DYNAMIC logic always comes before // all of the specific-key logic for any given instance of `NonMergeableLogicNodeBuilder`. // We rely on this fact in `getAllChildBuilder` to know that we can return the DYNAMIC logic, // followed by the property-specific logic, in that order. if (key === DYNAMIC) { const children = this.getCurrent().children; // Use the children size to determine if there is logic registered for a property other than // the DYNAMIC property. // - If the children map doesn't have DYNAMIC logic, but the size is still >0 then we know it // has logic for some other property. // - If the children map does have DYNAMIC logic then its size is going to be at least 1, // because it has the DYNAMIC key. However if it is >1, then we again know it contains other // keys. if (children.size > (children.has(DYNAMIC) ? 1 : 0)) { this.current = undefined; } } return this.getCurrent().getChild(key); } override hasLogic(builder: AbstractLogicNodeBuilder): boolean { if (this === builder) { return true; } return this.all.some(({builder: subBuilder}) => subBuilder.hasLogic(builder)); } override hasRules(): boolean { return this.all.length > 0; } override anyChildHasLogic(): boolean { return this.all.some(({builder}) => builder.anyChildHasLogic()); } /** * Merges logic from another `LogicNodeBuilder` into this one. * If a `predicate` is provided, all logic from the `other` builder will only apply * when the predicate evaluates to true. * @param other The `LogicNodeBuilder` to merge in. * @param predicate An optional predicate to gate the merged logic. */ mergeIn(other: LogicNodeBuilder, predicate?: Predicate): void { // Add the other builder to our collection, we'll defer the actual merging of the logic until // the logic node is requested to be created. In order to preserve the original ordering of the // rules, we close off the current builder to any further edits. If additional logic is added, // a new current builder will be created to capture it. if (predicate) { this.all.push({ builder: other, predicate: { fn: setBoundPathDepthForResolution(predicate.fn, this.depth), path: predicate.path, }, }); } else { this.all.push({builder: other}); } this.current = undefined; } /** * Gets the current `NonMergeableLogicNodeBuilder` for adding rules directly to this * `LogicNodeBuilder`. If no current builder exists, a new one is created. * The current builder is cleared whenever `mergeIn` is called to preserve the order * of rules when merging separate builder trees. * @returns The current `NonMergeableLogicNodeBuilder`. */ private getCurrent(): NonMergeableLogicNodeBuilder { if (this.current === undefined) { this.current = new NonMergeableLogicNodeBuilder(this.depth); this.all.push({builder: this.current}); } return this.current; } /** * Creates a new root `LogicNodeBuilder`. * @returns A new instance of `LogicNodeBuilder`. */ static newRoot(): LogicNodeBuilder { return new LogicNodeBuilder(0); } } /** * A type of `AbstractLogicNodeBuilder` used internally by the `LogicNodeBuilder` to record "pure" * chunks of logic that do not require merging in other builders. */ class NonMergeableLogicNodeBuilder extends AbstractLogicNodeBuilder { /** The collection of logic rules directly added to this builder. */ readonly logic = new LogicContainer([]); /** * A map of child property keys to their corresponding `LogicNodeBuilder` instances. * This allows for building a tree of logic. */ readonly children = new Map<PropertyKey, LogicNodeBuilder>(); constructor(depth: number) { super(depth); } override addHiddenRule(logic: LogicFn<any, boolean>): void { this.logic.hidden.push(setBoundPathDepthForResolution(logic, this.depth)); } override addDisabledReasonRule(logic: LogicFn<any, DisabledReason | undefined>): void { this.logic.disabledReasons.push(setBoundPathDepthForResolution(logic, this.depth)); } override addReadonlyRule(logic: LogicFn<any, boolean>): void { this.logic.readonly.push(setBoundPathDepthForResolution(logic, this.depth)); } override addSyncErrorRule( logic: LogicFn<any, ValidationResult<ValidationError.WithFieldTree>>, ): void { this.logic.syncErrors.push(setBoundPathDepthForResolution(logic, this.depth)); } override addSyncTreeErrorRule( logic: LogicFn<any, ValidationResult<ValidationError.WithFieldTree>>, ): void { this.logic.syncTreeErrors.push(setBoundPathDepthForResolution(logic, this.depth)); } override addAsyncErrorRule( logic: LogicFn<any, AsyncValidationResult<ValidationError.WithFieldTree>>, ): void { this.logic.asyncErrors.push(setBoundPathDepthForResolution(logic, this.depth)); } override addMetadataRule<T>(key: MetadataKey<unknown, T, unknown>, logic: LogicFn<any, T>): void { this.logic.getMetadata(key).push(setBoundPathDepthForResolution(logic, this.depth)); } override getChild(key: PropertyKey): LogicNodeBuilder { if (!this.children.has(key)) { this.children.set(key, new LogicNodeBuilder(this.depth + 1)); } return this.children.get(key)!; } override hasLogic(builder: AbstractLogicNodeBuilder): boolean { return this === builder; } override hasRules(): boolean { return this.logic.hasAnyLogic() || this.children.size > 0; } override anyChildHasLogic(): boolean { for (const child of this.children.values()) { if (child.hasRules()) { return true; } } return false; } } /** * Represents a node in the logic tree, containing all logic applicable * to a specific field or path in the form structure. * LogicNodes are 1:1 with nodes in the Field tree. */ export interface LogicNode { /** The collection of logic rules (hidden, disabled, errors, etc.) for this node. */ readonly logic: LogicContainer; /** * Retrieves the `LogicNode` for a child identified by the given property key. * @param key The property key of the child. * @returns The `LogicNode` for the specified child. */ getChild(key: PropertyKey): LogicNode; /** * Checks whether the logic from a particular `AbstractLogicNodeBuilder` has been merged into this * node. * @param builder The builder to check for. * @returns True if the builder has been merged, false otherwise. */ hasLogic(builder: AbstractLogicNodeBuilder): boolean; /** * Checks whether this node or any of its children have any logic rules defined. * @returns True if rules exist, false otherwise. */ hasRules(): boolean; /** * Checks whether any of the children of this node have any logic rules defined. * @returns True if rules exist on any child, false otherwise. */ anyChildHasLogic(): boolean; } /** * A tree structure of `Logic` corresponding to a tree of fields. * This implementation represents a leaf in the sense that its logic is derived * from a single builder. */ class LeafLogicNode implements LogicNode { /** The computed logic for this node. */ readonly logic: LogicContainer; /** * Constructs a `LeafLogicNode`. * @param builder The `AbstractLogicNodeBuilder` from which to derive the logic. * If undefined, an empty `Logic` instance is created. * @param predicates An array of predicates that gate the logic from the builder. */ constructor( private builder: AbstractLogicNodeBuilder | undefined, private predicates: BoundPredicate[], /** The depth of this node in the field tree. */ private depth: number, ) { this.logic = builder ? createLogic(builder, predicates, depth) : new LogicContainer([]); } // TODO: cache here, or just rely on the user of this API to do caching? /** * Retrieves the `LogicNode` for a child identified by the given property key. * @param key The property key of the child. * @returns The `LogicNode` for the specified child. */ getChild(key: PropertyKey): LogicNode { // The logic for a particular child may be spread across multiple builders. We lazily combine // this logic at the time the child logic node is requested to be created. const childBuilders = this.builder ? getAllChildBuilders(this.builder, key) : []; if (childBuilders.length === 0) { return new LeafLogicNode(undefined, [], this.depth + 1); } else if (childBuilders.length === 1) { const {builder, predicates} = childBuilders[0]; return new LeafLogicNode( builder, [...this.predicates, ...predicates.map((p) => bindLevel(p, this.depth))], this.depth + 1, ); } else { const builtNodes = childBuilders.map( ({builder, predicates}) => new LeafLogicNode( builder, [...this.predicates, ...predicates.map((p) => bindLevel(p, this.depth))], this.depth + 1, ), ); return new CompositeLogicNode(builtNodes); } } hasLogic(builder: AbstractLogicNodeBuilder): boolean { if (!this.builder) { return false; } return this.builder.hasLogic(builder); } hasRules(): boolean { return this.builder ? this.builder.hasRules() : false; } anyChildHasLogic(): boolean { return this.builder ? this.builder.anyChildHasLogic() : false; } } /** * A `LogicNode` that represents the composition of multiple `LogicNode` instances. * This is used when logic for a particular path is contributed by several distinct * builder branches that need to be merged. */ class CompositeLogicNode implements LogicNode { /** The merged logic from all composed nodes. */ readonly logic: LogicContainer; /** * Constructs a `CompositeLogicNode`. * @param all An array of `LogicNode` instances to compose. */ constructor(private all: LogicNode[]) { this.logic = new LogicContainer([]); for (const node of all) { this.logic.mergeIn(node.logic); } } /** * Retrieves the child `LogicNode` by composing the results of `getChild` from all * underlying `LogicNode` instances. * @param key The property key of the child. * @returns A `CompositeLogicNode` representing the composed child. */ getChild(key: PropertyKey): LogicNode { return new CompositeLogicNode(this.all.flatMap((child) => child.getChild(key))); } /** * Checks whether the logic from a particular `AbstractLogicNodeBuilder` has been merged into this * node. * @param builder The builder to check for. * @returns True if the builder has been merged, false otherwise. */ hasLogic(builder: AbstractLogicNodeBuilder): boolean { return this.all.some((node) => node.hasLogic(builder)); } hasRules(): boolean { return this.all.some((node) => node.hasRules()); } anyChildHasLogic(): boolean { return this.all.some((child) => child.anyChildHasLogic()); } } /** * Gets all of the builders that contribute logic to the given child of the parent builder. * This function recursively traverses the builder hierarchy. * @param builder The parent `AbstractLogicNodeBuilder`. * @param key The property key of the child. * @returns An array of objects, each containing a `LogicNodeBuilder` for the child and any associated predicates. */ function getAllChildBuilders( builder: AbstractLogicNodeBuilder, key: PropertyKey, ): {builder: LogicNodeBuilder; predicates: Predicate[]}[] { if (builder instanceof LogicNodeBuilder) { return builder.all.flatMap(({builder, predicate}) => { const children = getAllChildBuilders(builder, key); if (predicate) { return children.map(({builder, predicates}) => ({ builder, predicates: [...predicates, predicate], })); } return children; }); } else if (builder instanceof NonMergeableLogicNodeBuilder) { return [ // DYNAMIC logic always comes first for any individual `NonMergeableLogicNodeBuilder`. // This assumption is guaranteed by the behavior of `LogicNodeBuilder.getChild`. // Therefore we can return all of the DYNAMIC logic, followed by all of the property-specific // logic. ...(key !== DYNAMIC && builder.children.has(DYNAMIC) ? [{builder: builder.getChild(DYNAMIC), predicates: []}] : []), ...(builder.children.has(key) ? [{builder: builder.getChild(key), predicates: []}] : []), ]; } else { throw new RuntimeError( RuntimeErrorCode.UNKNOWN_BUILDER_TYPE, ngDevMode && 'Unknown LogicNodeBuilder type', ); } } /** * Creates the full `Logic` for a given builder. * This function handles different types of builders (`LogicNodeBuilder`, `NonMergeableLogicNodeBuilder`) * and applies the provided predicates. * @param builder The `AbstractLogicNodeBuilder` to process. * @param predicates Predicates to apply to the logic derived from the builder. * @param depth The depth in the field tree of the field which this logic applies to. * @returns The `Logic` instance. */ function createLogic( builder: AbstractLogicNodeBuilder, predicates: BoundPredicate[], depth: number, ): LogicContainer { const logic = new LogicContainer(predicates); if (builder instanceof LogicNodeBuilder) { const builtNodes = builder.all.map( ({builder, predicate}) => new LeafLogicNode( builder, predicate ? [...predicates, bindLevel(predicate, depth)] : predicates, depth, ), ); for (const node of builtNodes) { logic.mergeIn(node.logic); } } else if (builder instanceof NonMergeableLogicNodeBuilder) { logic.mergeIn(builder.logic); } else { throw new RuntimeError( RuntimeErrorCode.UNKNOWN_BUILDER_TYPE, ngDevMode && 'Unknown LogicNodeBuilder type', ); } return logic; } /** * Create a bound version of the given predicate to a specific depth in the field tree. * This allows us to unambiguously know which `FieldContext` the predicate function should receive. * * This is of particular concern when a schema is applied recursively to itself. Since the schema is * only compiled once, each nested application adds the same predicate instance. We differentiate * these by recording the depth of the field they're bound to. * * @param predicate The unbound predicate * @param depth The depth of the field the predicate is bound to * @returns A bound predicate */ function bindLevel(predicate: Predicate, depth: number): BoundPredicate { return {...predicate, depth: depth}; }