Skip to content

Commit d75046b

Browse files
authored
fix(forms): warn when showing hidden field state
In signal forms, it is up to the user to guard hidden fields from being rendered in the template. To help catch instances where it is accidentally not guarded, this commit introduces a warning in dev mode.
1 parent 17f1927 commit d75046b

File tree

3 files changed

+71
-2
lines changed

3 files changed

+71
-2
lines changed

packages/forms/signals/src/directive/form_field_directive.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Directive,
1515
effect,
1616
ElementRef,
17+
ɵformatRuntimeError as formatRuntimeError,
1718
inject,
1819
InjectionToken,
1920
Injector,
@@ -240,7 +241,9 @@ export class FormField<T> {
240241
if (this.isFieldBinding) {
241242
throw new RuntimeError(
242243
RuntimeErrorCode.BINDING_ALREADY_REGISTERED,
243-
ngDevMode && 'FormField already registered as a binding',
244+
typeof ngDevMode !== 'undefined' &&
245+
ngDevMode &&
246+
'FormField already registered as a binding',
244247
);
245248
}
246249
this.isFieldBinding = true;
@@ -269,6 +272,25 @@ export class FormField<T> {
269272
},
270273
{injector: this.injector},
271274
);
275+
276+
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
277+
effect(
278+
() => {
279+
const fieldNode = this.state() as unknown as FieldNode;
280+
if (fieldNode.hidden()) {
281+
const path = fieldNode.structure.pathKeys().join('.') || '<root>';
282+
console.warn(
283+
formatRuntimeError(
284+
RuntimeErrorCode.RENDERED_HIDDEN_FIELD,
285+
`Field '${path}' is hidden but is being rendered. ` +
286+
`Hidden fields should be removed from the DOM using @if.`,
287+
),
288+
);
289+
}
290+
},
291+
{injector: this.injector},
292+
);
293+
}
272294
}
273295

274296
/**
@@ -301,7 +323,8 @@ export class FormField<T> {
301323
} else {
302324
throw new RuntimeError(
303325
RuntimeErrorCode.INVALID_FIELD_DIRECTIVE_HOST,
304-
ngDevMode &&
326+
typeof ngDevMode !== 'undefined' &&
327+
ngDevMode &&
305328
`${host.descriptor} is an invalid [formField] directive host. The host must be a native form control ` +
306329
`(such as <input>', '<select>', or '<textarea>') or a custom form control with a 'value' or ` +
307330
`'checked' model.`,

packages/forms/signals/src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ export const enum RuntimeErrorCode {
2828
BINDING_ALREADY_REGISTERED = 1913,
2929
INVALID_FIELD_DIRECTIVE_HOST = 1914,
3030
MISSING_SUBMIT_ACTION = 1915,
31+
RENDERED_HIDDEN_FIELD = 1916,
3132
UNSUPPORTED_FEATURE = 1920,
3233
}

packages/forms/signals/test/web/form_field_directive.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,51 @@ describe('field directive', () => {
779779
act(() => component.field.set(component.f.y));
780780
expect(component.customControl().hidden()).toBe(false);
781781
});
782+
783+
it('should warn when a hidden field is rendered', () => {
784+
const warnSpy = spyOn(console, 'warn');
785+
@Component({
786+
imports: [FormField],
787+
template: `<input [formField]="f" />`,
788+
})
789+
class TestCmp {
790+
readonly f = form(signal(''), (p) => {
791+
hidden(p, () => true);
792+
});
793+
}
794+
795+
act(() => TestBed.createComponent(TestCmp));
796+
expect(warnSpy).toHaveBeenCalledWith(
797+
jasmine.stringMatching(/Field '.*' is hidden but is being rendered/),
798+
);
799+
});
800+
801+
it('should not warn when a hidden field is guarded by @if', () => {
802+
const isHidden = signal(false);
803+
const warnSpy = spyOn(console, 'warn');
804+
@Component({
805+
imports: [FormField],
806+
template: `
807+
@if (!f().hidden()) {
808+
<input [formField]="f" />
809+
}
810+
`,
811+
})
812+
class TestCmp {
813+
readonly f = form(signal(''), (p) => {
814+
hidden(p, isHidden);
815+
});
816+
}
817+
818+
act(() => TestBed.createComponent(TestCmp));
819+
expect(warnSpy).not.toHaveBeenCalled();
820+
821+
act(() => isHidden.set(true));
822+
expect(warnSpy).not.toHaveBeenCalled();
823+
824+
act(() => isHidden.set(false));
825+
expect(warnSpy).not.toHaveBeenCalled();
826+
});
782827
});
783828

784829
describe('invalid', () => {

0 commit comments

Comments
 (0)