Skip to content

Commit 06d00ed

Browse files
committed
refactor(forms): add debug name to form and FormField
Add a debug name to `form` and `FormField` that can be used for the clustering of all internal signal nodes in Angular DevTools. Note: Due to limitations related to the reactive nature of `FormField`, each form field gets a default index-based debug name.
1 parent 642fd43 commit 06d00ed

23 files changed

Lines changed: 806 additions & 433 deletions

packages/forms/signals/compat/src/compat_field_adapter.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {CompatFieldNode} from './compat_field_node';
2323
import {CompatNodeState} from './compat_node_state';
2424
import {CompatChildFieldNodeOptions, CompatStructure} from './compat_structure';
2525
import {CompatValidationState} from './compat_validation_state';
26+
import {formDebugObj} from '../../src/util/debug';
2627

2728
/**
2829
* This is a tree-shakable Field adapter that can create a compat node
@@ -120,9 +121,10 @@ export function createCompatNode(options: FieldNodeOptions) {
120121
const control = (
121122
options.kind === 'root'
122123
? options.value
123-
: computed(() => {
124-
return options.parent.value()[options.initialKeyInParent];
125-
})
124+
: computed(
125+
() => options.parent.value()[options.initialKeyInParent],
126+
ngDevMode ? formDebugObj(options.debugName, 'control') : undefined,
127+
)
126128
) as Signal<AbstractControl>;
127129

128130
return new CompatFieldNode({

packages/forms/signals/compat/src/compat_field_node.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {map, takeUntil} from 'rxjs/operators';
1414
import {FieldNode} from '../../src/field/node';
1515
import {getInjectorFromOptions} from '../../src/field/util';
1616
import type {CompatFieldNodeOptions} from './compat_structure';
17+
import {formDebugObj} from '../../src/util/debug';
1718

1819
/**
1920
* Field node with additional control property.
@@ -72,11 +73,15 @@ export function extractControlPropToSignal<T, R = T>(
7273
return runInInjectionContext(injector, () => makeSignal(control, createDestroySubject()));
7374
});
7475
},
76+
...(ngDevMode ? formDebugObj(options.debugName, 'signalOfControlSignal') : undefined),
7577
});
7678

7779
// We have to have computed, because we need to react to both:
7880
// linked signal changes as well as the inner signal changes.
79-
return computed(() => signalOfControlSignal()());
81+
return computed(
82+
() => signalOfControlSignal()(),
83+
ngDevMode ? formDebugObj(options.debugName, 'controlPropToSignal') : undefined,
84+
);
8085
}
8186

8287
/**
@@ -99,6 +104,7 @@ export const getControlStatusSignal = <T>(
99104
),
100105
{
101106
initialValue: getValue(c),
107+
...(ngDevMode ? formDebugObj(options.debugName, 'controlStatus') : undefined),
102108
},
103109
),
104110
);
@@ -126,6 +132,7 @@ export const getControlEventsSignal = <T>(
126132
),
127133
{
128134
initialValue: getValue(c),
135+
...(ngDevMode ? formDebugObj(options.debugName, 'controlEvents') : undefined),
129136
},
130137
),
131138
);

packages/forms/signals/compat/src/compat_node_state.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {AbstractControl} from '@angular/forms';
1111
import {FieldNodeState} from '../../src/field/state';
1212
import {CompatFieldNode, getControlEventsSignal, getControlStatusSignal} from './compat_field_node';
1313
import {CompatFieldNodeOptions} from './compat_structure';
14+
import {formDebugObj} from '../../src/util/debug';
1415

1516
/**
1617
* A FieldNodeState class wrapping a FormControl and proxying it's state.
@@ -31,9 +32,12 @@ export class CompatNodeState extends FieldNodeState {
3132
this.dirty = getControlEventsSignal(options, (c) => c.dirty);
3233
const controlDisabled = getControlStatusSignal(options, (c) => c.disabled);
3334

34-
this.disabled = computed(() => {
35-
return controlDisabled() || this.disabledReasons().length > 0;
36-
});
35+
this.disabled = computed(
36+
() => {
37+
return controlDisabled() || this.disabledReasons().length > 0;
38+
},
39+
ngDevMode ? formDebugObj(options.debugName, 'disabled') : undefined,
40+
);
3741
}
3842

3943
override markAsDirty() {

packages/forms/signals/compat/src/compat_structure.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {FormFieldManager} from '../../src/field/manager';
1818
import {FieldNode, ParentFieldNode} from '../../src/field/node';
1919
import {
2020
ChildFieldNodeOptions,
21+
ChildrenData,
2122
FieldNodeOptions,
2223
FieldNodeStructure,
2324
RootFieldNodeOptions,
@@ -27,6 +28,7 @@ import {toSignal} from '@angular/core/rxjs-interop';
2728
import {AbstractControl} from '@angular/forms';
2829
import {map, takeUntil} from 'rxjs/operators';
2930
import {extractControlPropToSignal} from './compat_field_node';
31+
import {formDebugObj} from '../../src/util/debug';
3032

3133
/**
3234
* Child Field Node options also exposing control property.
@@ -86,6 +88,7 @@ function getControlValueSignal<T>(options: CompatFieldNodeOptions) {
8688
),
8789
{
8890
initialValue: control.getRawValue(),
91+
...(ngDevMode ? formDebugObj(options.debugName, 'value') : undefined),
8992
},
9093
);
9194
}) as WritableSignal<T>;
@@ -111,8 +114,8 @@ export class CompatStructure extends FieldNodeStructure {
111114
override keyInParent: Signal<string>;
112115
override root: FieldNode;
113116
override pathKeys: Signal<readonly string[]>;
114-
override readonly children = signal([]);
115-
override readonly childrenMap = computed(() => undefined);
117+
override readonly children: WritableSignal<FieldNode[]>;
118+
override readonly childrenMap: Signal<ChildrenData | undefined>;
116119
override readonly parent: ParentFieldNode | undefined;
117120
override readonly fieldManager: FormFieldManager;
118121
override readonly isOrphaned: Signal<boolean>;
@@ -140,8 +143,16 @@ export class CompatStructure extends FieldNodeStructure {
140143
this.keyInParent = signals.keyInParent;
141144
this.isOrphaned = signals.isOrphaned;
142145

143-
this.pathKeys = computed(() =>
144-
this.parent ? [...this.parent.structure.pathKeys(), this.keyInParent()] : [],
146+
this.pathKeys = computed(
147+
() => (this.parent ? [...this.parent.structure.pathKeys(), this.keyInParent()] : []),
148+
ngDevMode ? formDebugObj(this.node.debugName, 'pathKeys') : undefined,
149+
);
150+
151+
this.children = signal([], ngDevMode ? formDebugObj(node.debugName, 'children') : undefined);
152+
153+
this.childrenMap = computed(
154+
() => undefined,
155+
ngDevMode ? formDebugObj(node.debugName, 'childrenMap') : undefined,
145156
);
146157
}
147158

packages/forms/signals/compat/src/compat_validation_state.ts

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '../../src/compat/validation_errors';
1717
import {CompatFieldNode, getControlStatusSignal} from './compat_field_node';
1818
import {CompatFieldNodeOptions} from './compat_structure';
19+
import {formDebugObj} from '../../src/util/debug';
1920

2021
// Readonly signal containing an empty array, used for optimization.
2122
const EMPTY_ARRAY_SIGNAL = computed(() => []);
@@ -33,7 +34,25 @@ export class CompatValidationState implements ValidationState {
3334
readonly invalid: Signal<boolean>;
3435
readonly valid: Signal<boolean>;
3536

36-
readonly parseErrors: Signal<ValidationError.WithFormField[]> = computed(() => []);
37+
readonly parseErrors: Signal<ValidationError.WithFormField[]>;
38+
39+
// Compat fields can't have validation rules applied to them; however, there are other
40+
// features that depend on this property, such as `markAsTouched()`.
41+
readonly shouldSkipValidation: Signal<boolean>;
42+
43+
/**
44+
* Computes status based on whether the field is valid/invalid/pending.
45+
*/
46+
readonly status: Signal<'valid' | 'invalid' | 'unknown'>;
47+
48+
readonly rawSyncTreeErrors: Signal<ValidationError.WithFieldTree[]>;
49+
readonly syncErrors: Signal<ValidationError.WithFieldTree[]>;
50+
readonly rawAsyncErrors: Signal<(ValidationError.WithFieldTree | 'pending')[]>;
51+
52+
asyncErrors: Signal<(ValidationError.WithFieldTree | 'pending')[]>;
53+
errorSummary: Signal<ValidationError.WithFieldTree[]>;
54+
55+
private readonly emptyArraySignal: Signal<never[]>;
3756

3857
constructor(
3958
private readonly node: CompatFieldNode,
@@ -50,26 +69,32 @@ export class CompatValidationState implements ValidationState {
5069
this.invalid = getControlStatusSignal(options, (c) => {
5170
return c.invalid;
5271
});
53-
}
5472

55-
asyncErrors: Signal<(ValidationError.WithFieldTree | 'pending')[]> = EMPTY_ARRAY_SIGNAL;
56-
errorSummary: Signal<ValidationError.WithFieldTree[]> = EMPTY_ARRAY_SIGNAL;
73+
this.parseErrors = computed(
74+
() => [],
75+
ngDevMode ? formDebugObj(node.debugName, 'parseErrors') : undefined,
76+
);
5777

58-
// Those are irrelevant for compat mode, as it has no children
59-
rawSyncTreeErrors = EMPTY_ARRAY_SIGNAL;
60-
syncErrors = EMPTY_ARRAY_SIGNAL;
61-
rawAsyncErrors = EMPTY_ARRAY_SIGNAL;
78+
this.shouldSkipValidation = computed(
79+
() => this.node.hidden() || this.node.disabled() || this.node.readonly(),
80+
ngDevMode ? formDebugObj(node.debugName, 'shouldSkipValidation') : undefined,
81+
);
6282

63-
// Compat fields can't have validation rules applied to them; however, there are other
64-
// features that depend on this property, such as `markAsTouched()`.
65-
readonly shouldSkipValidation = computed(
66-
() => this.node.hidden() || this.node.disabled() || this.node.readonly(),
67-
);
83+
this.status = computed(
84+
() => calculateValidationSelfStatus(this),
85+
ngDevMode ? formDebugObj(node.debugName, 'status') : undefined,
86+
);
6887

69-
/**
70-
* Computes status based on whether the field is valid/invalid/pending.
71-
*/
72-
readonly status: Signal<'valid' | 'invalid' | 'unknown'> = computed(() => {
73-
return calculateValidationSelfStatus(this);
74-
});
88+
this.emptyArraySignal = ngDevMode
89+
? computed(() => [], formDebugObj(node.debugName, 'EMPTY_ARRAY_SIGNAL'))
90+
: EMPTY_ARRAY_SIGNAL;
91+
92+
this.asyncErrors = this.emptyArraySignal;
93+
this.errorSummary = this.emptyArraySignal;
94+
95+
// Those are irrelevant for compat mode, as it has no children
96+
this.rawSyncTreeErrors = this.emptyArraySignal;
97+
this.syncErrors = this.emptyArraySignal;
98+
this.rawAsyncErrors = this.emptyArraySignal;
99+
}
75100
}

packages/forms/signals/compat/src/signal_form_control/signal_form_control.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {RuntimeErrorCode} from '../../../src/errors';
3737
import {FieldNode} from '../../../src/field/node';
3838
import {normalizeFormArgs} from '../../../src/util/normalize_form_args';
3939
import {compatForm} from '../api/compat_form';
40+
import {formDebugObj} from '../../../src/util/debug';
4041

4142
/** Options used to update the control value. */
4243
export type ValueUpdateOptions = {
@@ -96,7 +97,11 @@ export class SignalFormControl<T> extends AbstractControl {
9697
constructor(value: T, schemaOrOptions?: SchemaFn<T> | FormOptions<T>, options?: FormOptions<T>) {
9798
super(null, null);
9899

99-
const [model, schema, opts] = normalizeFormArgs<T>([signal(value), schemaOrOptions, options]);
100+
const [model, schema, opts] = normalizeFormArgs<T>([
101+
signal(value, ngDevMode ? formDebugObj(options?.debugName, 'value') : undefined),
102+
schemaOrOptions,
103+
options,
104+
]);
100105
this.sourceValue = model;
101106
this.initialValue = value;
102107
const injector = opts?.injector ?? inject(Injector);
@@ -122,7 +127,10 @@ export class SignalFormControl<T> extends AbstractControl {
122127
this.emitControlEvent(new ValueChangeEvent(value, this));
123128
});
124129
},
125-
{injector},
130+
{
131+
injector,
132+
...(ngDevMode ? formDebugObj(options?.debugName, 'valueChanges') : undefined),
133+
},
126134
);
127135

128136
// Status changes effect
@@ -134,7 +142,10 @@ export class SignalFormControl<T> extends AbstractControl {
134142
});
135143
this.emitControlEvent(new StatusChangeEvent(status, this));
136144
},
137-
{injector},
145+
{
146+
injector,
147+
...(ngDevMode ? formDebugObj(options?.debugName, 'statusChanges') : undefined),
148+
},
138149
);
139150

140151
// Disabled changes effect
@@ -147,7 +158,10 @@ export class SignalFormControl<T> extends AbstractControl {
147158
}
148159
});
149160
},
150-
{injector},
161+
{
162+
injector,
163+
...(ngDevMode ? formDebugObj(options?.debugName, 'disabledChanges') : undefined),
164+
},
151165
);
152166

153167
// Touched changes effect
@@ -165,7 +179,10 @@ export class SignalFormControl<T> extends AbstractControl {
165179
parent.markAsTouched();
166180
}
167181
},
168-
{injector},
182+
{
183+
injector,
184+
...(ngDevMode ? formDebugObj(options?.debugName, 'touchedChanges') : undefined),
185+
},
169186
);
170187

171188
// Dirty changes effect
@@ -183,7 +200,10 @@ export class SignalFormControl<T> extends AbstractControl {
183200
parent.markAsPristine();
184201
}
185202
},
186-
{injector},
203+
{
204+
injector,
205+
...(ngDevMode ? formDebugObj(options?.debugName, 'dirtyChanges') : undefined),
206+
},
187207
);
188208
}
189209

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
WritableSignal,
1616
} from '@angular/core';
1717
import {RuntimeErrorCode} from '../errors';
18-
import {BasicFieldAdapter, FieldAdapter} from '../field/field_adapter';
18+
import {BasicFieldAdapter} from '../field/field_adapter';
1919
import {FormFieldManager} from '../field/manager';
2020
import {FieldNode} from '../field/node';
2121
import {addDefaultField} from '../field/validation';
@@ -55,6 +55,8 @@ export interface FormOptions<TModel> {
5555
name?: string;
5656
/** Options that define how to handle form submission. */
5757
submission?: FormSubmitOptions<TModel, unknown>;
58+
/** A debug name for the form. Used in Angular DevTools to identify the node. */
59+
debugName?: string;
5860
}
5961

6062
/**
@@ -194,8 +196,8 @@ export function form<TModel>(...args: any[]): FieldTree<TModel> {
194196
options?.submission as FormSubmitOptions<unknown, unknown> | undefined,
195197
);
196198
const adapter = options?.adapter ?? new BasicFieldAdapter();
197-
const fieldRoot = FieldNode.newRoot(fieldManager, model, pathNode, adapter);
198-
fieldManager.createFieldManagementEffect(fieldRoot.structure);
199+
const fieldRoot = FieldNode.newRoot(fieldManager, model, pathNode, adapter, options?.debugName);
200+
fieldManager.createFieldManagementEffect(fieldRoot.structure, options?.debugName);
199201

200202
return fieldRoot.fieldTree as FieldTree<TModel>;
201203
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {createParser} from '../util/parser';
1717
import type {ValidationError} from './rules';
1818
import type {OneOrMany} from './types';
1919
import {ɵFORM_CONTROL_INTEGRATION as FORM_CONTROL_INTEGRATION} from '@angular/forms';
20+
import {formFieldDebugObj} from '../util/debug';
2021

2122
/**
2223
* Result of parsing a raw value into a model value.
@@ -114,12 +115,16 @@ export interface TransformedValueSignal<TRaw> extends WritableSignal<TRaw> {
114115
export function transformedValue<TValue, TRaw>(
115116
value: ModelSignal<TValue>,
116117
options: TransformedValueOptions<TValue, TRaw>,
118+
debugFormFieldName?: string,
117119
): TransformedValueSignal<TRaw> {
118120
const {parse, format} = options;
119-
const parser = createParser(value, value.set, parse);
121+
const parser = createParser(value, value.set, parse, debugFormFieldName);
120122

121123
// Create the result signal with overridden set/update and a `parseErrors` property.
122-
const rawValue = linkedSignal(() => format(value()));
124+
const rawValue = linkedSignal(
125+
() => format(value()),
126+
ngDevMode ? formFieldDebugObj(debugFormFieldName, 'rawValue') : undefined,
127+
);
123128
const result = rawValue as WritableSignal<TRaw> & {
124129
parseErrors: Signal<readonly ValidationError.WithoutFieldTree[]>;
125130
};

0 commit comments

Comments
 (0)