| 35 | * `FieldContext` implementation, backed by a `FieldNode`. |
| 36 | */ |
| 37 | export class FieldNodeContext implements FieldContext<unknown> { |
| 38 | /** |
| 39 | * Cache of paths that have been resolved for this context. |
| 40 | * |
| 41 | * For each resolved path we keep track of a signal of field that it maps to rather than a static |
| 42 | * field, since it theoretically could change. In practice for the current system it should not |
| 43 | * actually change, as they only place we currently track fields moving within the parent |
| 44 | * structure is for arrays, and paths do not currently support array indexing. |
| 45 | */ |
| 46 | private readonly cache = new WeakMap< |
| 47 | SchemaPath<unknown, SchemaPathRules>, |
| 48 | Signal<ReadonlyFieldTree<unknown>> |
| 49 | >(); |
| 50 | |
| 51 | constructor( |
| 52 | /** The field node this context corresponds to. */ |
| 53 | private readonly node: FieldNode, |
| 54 | ) { |
| 55 | // These methods are explicitly bound to the context instance so that they |
| 56 | // safely retain their `this` reference if destructured by consumers |
| 57 | // (e.g., during validation when `stateOf` or `fieldTreeOf` are extracted). |
| 58 | this.fieldTreeOf = this.fieldTreeOf.bind(this); |
| 59 | this.stateOf = this.stateOf.bind(this); |
| 60 | } |
| 61 | |
| 62 | /** |
| 63 | * Resolves a target path relative to this context. |
| 64 | * @param target The path to resolve |
| 65 | * @returns The field corresponding to the target path. |
| 66 | */ |
| 67 | private resolve<U>(target: SchemaPath<U, SchemaPathRules>): ReadonlyFieldTree<U> { |
| 68 | if (!this.cache.has(target)) { |
| 69 | const resolver = computed<ReadonlyFieldTree<unknown>>(() => { |
| 70 | const targetPathNode = FieldPathNode.unwrapFieldPath(target); |
| 71 | |
| 72 | // First, find the field where the root our target path was merged in. |
| 73 | // We determine this by walking up the field tree from the current field and looking for |
| 74 | // the place where the LogicNodeBuilder from the target path's root was merged in. |
| 75 | // We always make sure to walk up at least as far as the depth of the path we were bound to. |
| 76 | // This ensures that we do not accidentally match on the wrong application of a recursively |
| 77 | // applied schema. |
| 78 | let field: FieldNode | undefined = this.node; |
| 79 | let stepsRemaining = getBoundPathDepth(); |
| 80 | while (stepsRemaining > 0 || !field.structure.logic.hasLogic(targetPathNode.root.builder)) { |
| 81 | stepsRemaining--; |
| 82 | field = field.structure.parent; |
| 83 | if (field === undefined) { |
| 84 | throw new RuntimeError( |
| 85 | RuntimeErrorCode.PATH_NOT_IN_FIELD_TREE, |
| 86 | ngDevMode && 'Path is not part of this field tree.', |
| 87 | ); |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // Now, we can navigate to the target field using the relative path in the target path node |
| 92 | // to traverse down from the field we just found. |
| 93 | for (let key of targetPathNode.keys) { |
| 94 | field = field.structure.getChild(key); |