Skip to content

Commit e715503

Browse files
committed
refactor(forms): add fieldTree property to FieldState
The `fieldTree` property of `FieldState` returns its associated `FieldTree`. Note that the round trip from `FieldTree` to `FieldState` and back will lose type information. This is because `FieldState` intentionally does not know whether it came from a pure Signal Forms field tree, or a Reactive Forms compatible field tree: ```ts // Pure Signal Forms: const x: FieldTree<string>; x(); // FieldState<string>; x().fieldTree; // FieldTree<unknown> // Reactive Forms compatibility: const y: FieldTree<FormControl<string>>; y(); // FieldState<string>; y().fieldTree; // FieldTree<unknown>; ```
1 parent 460dbbb commit e715503

8 files changed

Lines changed: 37 additions & 7 deletions

File tree

goldens/public-api/forms/signals/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export interface FieldState<TValue, TKey extends string | number = string | numb
134134
// (undocumented)
135135
readonly errors: Signal<ValidationError.WithFieldTree[]>;
136136
readonly errorSummary: Signal<ValidationError.WithFieldTree[]>;
137+
readonly fieldTree: FieldTree<unknown, TKey>;
137138
focusBoundControl(options?: FocusOptions): void;
138139
readonly formFieldBindings: Signal<readonly FormField<unknown>[]>;
139140
readonly hidden: Signal<boolean>;

packages/forms/signals/src/api/structure.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export function form<TModel>(...args: any[]): FieldTree<TModel> {
222222
const fieldRoot = FieldNode.newRoot(fieldManager, model, pathNode, adapter);
223223
fieldManager.createFieldManagementEffect(fieldRoot.structure);
224224

225-
return fieldRoot.fieldProxy as FieldTree<TModel>;
225+
return fieldRoot.fieldTree as FieldTree<TModel>;
226226
}
227227

228228
/**
@@ -475,7 +475,7 @@ function setSubmissionErrors(
475475
}
476476
const errorsByField = new Map<FieldNode, ValidationError.WithFieldTree[]>();
477477
for (const error of errors) {
478-
const errorWithField = addDefaultField(error, submittedField.fieldProxy);
478+
const errorWithField = addDefaultField(error, submittedField.fieldTree);
479479
const field = errorWithField.fieldTree() as FieldNode;
480480
let fieldErrors = errorsByField.get(field);
481481
if (!fieldErrors) {

packages/forms/signals/src/api/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ export type MaybeFieldTree<TModel, TKey extends string | number = string | numbe
218218
* @experimental 21.0.0
219219
*/
220220
export interface FieldState<TValue, TKey extends string | number = string | number> {
221+
/**
222+
* The {@link FieldTree} associated with this field state.
223+
*/
224+
readonly fieldTree: FieldTree<unknown, TKey>;
225+
221226
/**
222227
* A writable signal containing the value for this field.
223228
*

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export class FieldNodeContext implements FieldContext<unknown> {
9595
}
9696
}
9797

98-
return field.fieldProxy;
98+
return field.fieldTree;
9999
});
100100

101101
this.cache.set(target, resolver);

packages/forms/signals/src/field/node.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ export class FieldNode implements FieldState<unknown> {
130130
},
131131
});
132132

133+
get fieldTree(): FieldTree<unknown> {
134+
return this.fieldProxy;
135+
}
136+
133137
get logicNode(): LogicNode {
134138
return this.structure.logic;
135139
}

packages/forms/signals/src/field/proxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const FIELD_PROXY_HANDLER: ProxyHandler<() => FieldNode> = {
2222
if (child !== undefined) {
2323
// If so, return the child node's `FieldTree` proxy, allowing the developer to continue
2424
// navigating the form structure.
25-
return child.fieldProxy;
25+
return child.fieldTree;
2626
}
2727

2828
// Otherwise, we need to consider whether the properties they're accessing are related to array
@@ -49,7 +49,7 @@ export const FIELD_PROXY_HANDLER: ProxyHandler<() => FieldNode> = {
4949
//
5050
// Instead, side-effectfully read the value here to ensure iterator creation is reactive.
5151
tgt.value();
52-
return Array.prototype[Symbol.iterator].apply(tgt.fieldProxy);
52+
return Array.prototype[Symbol.iterator].apply(tgt.fieldTree);
5353
};
5454
}
5555
// Note: We can consider supporting additional array methods if we want in the future,

packages/forms/signals/src/field/validation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export class FieldValidationState implements ValidationState {
205205
* rather than a descendant.
206206
*/
207207
readonly syncTreeErrors: Signal<ValidationError.WithFieldTree[]> = computed(() =>
208-
this.rawSyncTreeErrors().filter((err) => err.fieldTree === this.node.fieldProxy),
208+
this.rawSyncTreeErrors().filter((err) => err.fieldTree === this.node.fieldTree),
209209
);
210210

211211
/**
@@ -237,7 +237,7 @@ export class FieldValidationState implements ValidationState {
237237
return [];
238238
}
239239
return this.rawAsyncErrors().filter(
240-
(err) => err === 'pending' || err.fieldTree === this.node.fieldProxy,
240+
(err) => err === 'pending' || err.fieldTree === this.node.fieldTree,
241241
);
242242
});
243243

packages/forms/signals/test/node/field_node.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,26 @@ describe('FieldNode', () => {
159159
});
160160
});
161161

162+
describe('fieldTree', () => {
163+
it('should return the associated field tree from state produced by the root field tree', () => {
164+
const f = form(signal(''), {injector: TestBed.inject(Injector)});
165+
166+
expect(f().fieldTree).toBe(f);
167+
});
168+
169+
it('should return the associated field tree from state produced by a child field tree', () => {
170+
const f = form(signal({a: 1, b: 2}), {injector: TestBed.inject(Injector)});
171+
172+
expect(f.a().fieldTree).toBe(f.a);
173+
});
174+
175+
it('should return the associated field tree from state produced by an array item field tree', () => {
176+
const f = form(signal([1, 2, 3]), {injector: TestBed.inject(Injector)});
177+
178+
expect(f[0]().fieldTree).toBe(f[0]);
179+
});
180+
});
181+
162182
describe('dirty', () => {
163183
it('is not dirty initially', () => {
164184
const f = form(

0 commit comments

Comments
 (0)