From 0c23608da836d82eea4b8b00a7d557fe5f0d8f50 Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Sat, 9 May 2026 22:31:50 +0000 Subject: [PATCH] fix(forms): update Signal Forms nullable field guidance Clarify nullable guidance for Signal Forms by distinguishing between required and optional fields. Optional numeric fields may validly use null to represent an unset or empty value. --- .../references/signal-forms.md | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/skills/dev-skills/angular-developer/references/signal-forms.md b/skills/dev-skills/angular-developer/references/signal-forms.md index 992fab10cd48..b6150286cabf 100644 --- a/skills/dev-skills/angular-developer/references/signal-forms.md +++ b/skills/dev-skills/angular-developer/references/signal-forms.md @@ -2,7 +2,11 @@ Signal Forms are the recommended approach for handling forms in modern Angular applications (v21+). They provide a reactive, type-safe, and model-driven way to manage form state using Angular Signals. -**CRITICAL**: You MUST use Angular's new Signal Forms API for all form-related functionality. Do NOT use null as a value or type of any fields. +**CRITICAL**: You MUST use Angular's new Signal Forms API for all form-related functionality. + +Avoid using `null` for required fields. + +For optional fields (including numeric fields), `null` can be used to represent an empty or unset value. ## Imports @@ -48,7 +52,7 @@ export class Example { userModel = signal({ name: '', // CRITICAL: NEVER use null or undefined as initial values email: '', - age: 0, // Use 0 for numbers, NOT null + age: 0, // Use 0 for required numeric fields; use null for optional numeric fields address: { street: '', city: '', @@ -56,10 +60,10 @@ export class Example { hobbies: [] as string[], // Use [] for arrays, NOT null }); - // WRONG - DO NOT DO THIS: + // WRONG - DO NOT DO THIS for required fields: // badModel = signal({ - // name: null, // ERROR: use '' instead - // age: null, // ERROR: use 0 instead + // name: null, // ERROR: use '' for required string fields + // age: null, // VALID only if the field is optional // items: null // ERROR: use [] instead // }); @@ -188,7 +192,7 @@ Do NOT do this: `` or `US - + ``` @@ -506,32 +510,32 @@ form( ## Common Pitfalls (DO NOT DO THESE) -| Error Scenario | WRONG (Common Mistake) | RIGHT (Correct Way) | -| :--------------------- | :-------------------------------------------- | :---------------------------------------------------------- | -| **Accessing Flags** | `form.field.valid()` | `form.field().valid()` | -| **Accessing value** | `form.field.value()` | `form.field().value()` | -| **Setting value** | `form.field.set(x)` | Update model signal: `this.model.update(...)` | -| **Form root flags** | `form.invalid()` | `form().invalid()` | -| **Double-calling** | `form.field()()` | `form.field().value()` | -| **Rules Context** | `({ touched }) => touched()` | `({ state }) => state.touched()` | -| **Calling Paths** | `applyWhen(p.foo, () => p.foo() === 'x')` | `applyWhen(p.foo, ({ valueOf }) => valueOf(p.foo) === 'x')` | -| **applyWhen args** | `applyWhen(condition, () => {...})` | `applyWhen(path, condition, schemaFn)` - needs 3 args | -| **Array length** | `form.items().length` | `form.items.length` (structural) | -| **Multi-select array** | `` | Use `readonly()` rule in schema | -| **min/max attributes** | `` | Use `min()` and `max()` rules in schema | -| **value binding** | `` | Do NOT use `[value]` with `[formField]` | -| **when option** | `pattern(p.x, /.../, {when: ...})` | `when` only works with `required()` | -| **Submit callback** | `submit(form, () => { ... })` | `submit(form, async () => { ... })` | -| **Async params** | `params: s.field` | `params: ({ value }) => value()` | -| **Async onError** | Omitting `onError` | `onError` is REQUIRED in `validateAsync` | -| **resource() API** | `request: signal` | `params: signal` | -| **applyEach args** | `applyEach(s.items, (item, index) => ...)` | `applyEach(s.items, (item) => ...)` | -| **Nested @for** | `$parent.$index` | Use `let outerIndex = $index` | -| **FormState import** | `import { FormState }` | `FormState` does not exist, use `FieldState` | -| **Null in model** | `signal({ name: null })` | `signal({ name: '' })` or `signal({ age: 0 })` | -| **Validate syntax** | `validate(s.field, { value } => ...)` | `validate(s.field, ({ value }) => ...)` | -| **Checkbox Array** | `[formField]="form.tags"` (string[]) | Checkboxes ONLY bind to `boolean` | +| Error Scenario | WRONG (Common Mistake) | RIGHT (Correct Way) | +| :--------------------- | :-------------------------------------------- | :------------------------------------------------------------------------ | ------ | +| **Accessing Flags** | `form.field.valid()` | `form.field().valid()` | +| **Accessing value** | `form.field.value()` | `form.field().value()` | +| **Setting value** | `form.field.set(x)` | Update model signal: `this.model.update(...)` | +| **Form root flags** | `form.invalid()` | `form().invalid()` | +| **Double-calling** | `form.field()()` | `form.field().value()` | +| **Rules Context** | `({ touched }) => touched()` | `({ state }) => state.touched()` | +| **Calling Paths** | `applyWhen(p.foo, () => p.foo() === 'x')` | `applyWhen(p.foo, ({ valueOf }) => valueOf(p.foo) === 'x')` | +| **applyWhen args** | `applyWhen(condition, () => {...})` | `applyWhen(path, condition, schemaFn)` - needs 3 args | +| **Array length** | `form.items().length` | `form.items.length` (structural) | +| **Multi-select array** | `` | Use `readonly()` rule in schema | +| **min/max attributes** | `` | Use `min()` and `max()` rules in schema | +| **value binding** | `` | Do NOT use `[value]` with `[formField]` | +| **when option** | `pattern(p.x, /.../, {when: ...})` | `when` only works with `required()` | +| **Submit callback** | `submit(form, () => { ... })` | `submit(form, async () => { ... })` | +| **Async params** | `params: s.field` | `params: ({ value }) => value()` | +| **Async onError** | Omitting `onError` | `onError` is REQUIRED in `validateAsync` | +| **resource() API** | `request: signal` | `params: signal` | +| **applyEach args** | `applyEach(s.items, (item, index) => ...)` | `applyEach(s.items, (item) => ...)` | +| **Nested @for** | `$parent.$index` | Use `let outerIndex = $index` | +| **FormState import** | `import { FormState }` | `FormState` does not exist, use `FieldState` | +| **Null in model** | `signal({ name: null })` | Use `''` for required fields, or `null` for optional fields (e.g. `number | null`) | +| **Validate syntax** | `validate(s.field, { value } => ...)` | `validate(s.field, ({ value }) => ...)` | +| **Checkbox Array** | `[formField]="form.tags"` (string[]) | Checkboxes ONLY bind to `boolean` | ## Big Form Example