Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 36 additions & 32 deletions skills/dev-skills/angular-developer/references/signal-forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -48,18 +52,18 @@ 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: '',
},
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
// });

Expand Down Expand Up @@ -188,7 +192,7 @@ Do NOT do this: `<input min="1" [formField]>` or `<input [value]="val" [formFiel
<option value="us">US</option>
</select>

<!-- userForm.name can NOT be nullable, because input does not accept null-->
<!-- Required text inputs should not be nullable -->
<input [formField]="userForm.name" />
```

Expand Down Expand Up @@ -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** | `<select [formField]="form.tags">` (string[]) | Use checkboxes for array fields |
| **readonly attribute** | `<input readonly [formField]>` | Use `readonly()` rule in schema |
| **min/max attributes** | `<input min="1" max="10">` | Use `min()` and `max()` rules in schema |
| **value binding** | `<input [value]="val">` | 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** | `<select [formField]="form.tags">` (string[]) | Use checkboxes for array fields |
| **readonly attribute** | `<input readonly [formField]>` | Use `readonly()` rule in schema |
| **min/max attributes** | `<input min="1" max="10">` | Use `min()` and `max()` rules in schema |
| **value binding** | `<input [value]="val">` | 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

Expand Down
Loading