Skip to content

Commit 642b966

Browse files
committed
fix(forms): handle undefined nodes in resolve()
1 parent eb6c2ec commit 642b966

File tree

1 file changed

+51
-37
lines changed

1 file changed

+51
-37
lines changed

packages/forms/experimental/src/field/context.ts

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {WritableSignal} from '@angular/core';
9+
import {computed, Signal, WritableSignal} from '@angular/core';
1010
import {Field, FieldContext, FieldPath, FieldState} from '../api/types';
1111
import {DYNAMIC} from '../logic_node';
1212
import {FieldPathNode, FieldRootPathNode} from '../path_node';
@@ -18,50 +18,64 @@ import {FieldNode} from './node';
1818
export class FieldNodeContext implements FieldContext<unknown> {
1919
constructor(private readonly node: FieldNode) {}
2020

21-
private readonly cache = new WeakMap<FieldPath<unknown>, Field<unknown>>();
21+
private readonly cache = new WeakMap<FieldPath<unknown>, Signal<Field<unknown>>>();
2222
private resolve<U>(target: FieldPath<U>): Field<U> {
23-
if (this.cache.has(target)) {
24-
return this.cache.get(target) as Field<U>;
25-
}
26-
const currentPathKeys = this.node.structure.pathKeys();
27-
const targetPathNode = FieldPathNode.unwrapFieldPath(target);
23+
if (!this.cache.has(target)) {
24+
const resolver = computed<Field<unknown>>(() => {
25+
const currentPathKeys = this.node.structure.pathKeys();
26+
const targetPathNode = FieldPathNode.unwrapFieldPath(target);
2827

29-
if (!(this.node.structure.root.structure.logicPath instanceof FieldRootPathNode)) {
30-
throw Error('Expected root of FieldNode tree to have a FieldRootPathNode.');
31-
}
32-
const prefix = this.node.structure.root.structure.logicPath.subroots.get(targetPathNode.root);
33-
if (!prefix) {
34-
throw Error('Path is not part of this field tree.');
35-
}
28+
if (!(this.node.structure.root.structure.logicPath instanceof FieldRootPathNode)) {
29+
throw Error('Expected root of FieldNode tree to have a FieldRootPathNode.');
30+
}
31+
const prefix = this.node.structure.root.structure.logicPath.subroots.get(
32+
targetPathNode.root,
33+
);
34+
if (!prefix) {
35+
throw Error('Path is not part of this field tree.');
36+
}
3637

37-
const targetPathKeys = [...prefix, ...targetPathNode.keys];
38+
const targetPathKeys = [...prefix, ...targetPathNode.keys];
3839

39-
// Navigate from `currentPath` to `targetPath`. As an example, suppose that:
40-
// currentPath = [A, B, C, D]
41-
// targetPath = [A, B, X, Y, Z]
40+
// Navigate from `currentPath` to `targetPath`. As an example, suppose that:
41+
// currentPath = [A, B, C, D]
42+
// targetPath = [A, B, X, Y, Z]
4243

43-
// Firstly, find the length of the shared prefix between the two paths. In our example, this
44-
// is the prefix [A, B], so we would expect a `sharedPrefixLength` of 2.
45-
const sharedPrefixLength = lengthOfSharedPrefix(currentPathKeys, targetPathNode.keys);
44+
// Firstly, find the length of the shared prefix between the two paths. In our example, this
45+
// is the prefix [A, B], so we would expect a `sharedPrefixLength` of 2.
46+
const sharedPrefixLength = lengthOfSharedPrefix(currentPathKeys, targetPathNode.keys);
4647

47-
// Walk up the graph until we arrive at the common ancestor, which could be the root node if
48-
// there is no shared prefix. In our example, this will require 2 up steps, navigating from
49-
// D to B.
50-
let requiredUpSteps = currentPathKeys.length - sharedPrefixLength;
51-
let field: FieldNode = this.node;
52-
while (requiredUpSteps-- > 0) {
53-
field = field.structure.parent!;
54-
}
48+
// Walk up the graph until we arrive at the common ancestor, which could be the root node if
49+
// there is no shared prefix. In our example, this will require 2 up steps, navigating from
50+
// D to B.
51+
let requiredUpSteps = currentPathKeys.length - sharedPrefixLength;
52+
let field: FieldNode | undefined = this.node;
53+
while (requiredUpSteps-- > 0) {
54+
field = field.structure.parent!;
55+
}
5556

56-
// Now, we can navigate from the closest ancestor to the target, e.g. from B through X, Y,
57-
// and then to Z.
58-
for (let idx = sharedPrefixLength; idx < targetPathKeys.length; idx++) {
59-
const property = targetPathKeys[idx] === DYNAMIC ? currentPathKeys[idx] : targetPathKeys[idx];
60-
field = field.structure.getChild(property)!;
61-
}
57+
// Now, we can navigate from the closest ancestor to the target, e.g. from B through X, Y,
58+
// and then to Z.
59+
for (
60+
let idx = sharedPrefixLength;
61+
field !== undefined && idx < targetPathKeys.length;
62+
idx++
63+
) {
64+
const property =
65+
targetPathKeys[idx] === DYNAMIC ? currentPathKeys[idx] : targetPathKeys[idx];
66+
field = field.structure.getChild(property);
67+
}
68+
69+
if (field === undefined) {
70+
throw new Error(`Resolved field does not exist`);
71+
}
6272

63-
this.cache.set(target, field.fieldProxy);
64-
return field.fieldProxy as Field<U>;
73+
return field.fieldProxy;
74+
});
75+
76+
this.cache.set(target, resolver);
77+
}
78+
return this.cache.get(target)!() as Field<U>;
6579
}
6680

6781
get field(): Field<unknown> {

0 commit comments

Comments
 (0)