diff --git a/adev/src/content/guide/forms/signals/validation.md b/adev/src/content/guide/forms/signals/validation.md
index 5f3b293eb81..522bd0b746d 100644
--- a/adev/src/content/guide/forms/signals/validation.md
+++ b/adev/src/content/guide/forms/signals/validation.md
@@ -110,12 +110,22 @@ export class RegistrationComponent {
}
```
-A field is considered "empty" when:
+A field is considered "empty" when its value is one of the following, and non-empty for every other
+value — including `0` and the empty array `[]`:
-| Condition | Example |
-| ------------------------ | ------- |
-| Value is `null` | `null`, |
-| Value is an empty string | `''` |
+| Condition | Example |
+| ------------------------ | ----------- |
+| Value is `null` | `null` |
+| Value is `undefined` | `undefined` |
+| Value is an empty string | `''` |
+| Value is `false` | `false` |
+| Value is `NaN` | `NaN` |
+
+The last two are worth calling out:
+
+- `false` is empty to follow the native semantics of `required` on ``, where
+ an unchecked box fails validation.
+- `NaN` is empty because it is usually the result of a parsing error, and is not a valid number.
For conditional requirements, use the `when` option:
@@ -130,7 +140,7 @@ registrationForm = form(this.registrationModel, (schemaPath) => {
The validation rule only runs when the `when` function returns `true`.
-Note: `required` treats an empty array as present (valid), so use [`minLength()`](#minlength-and-maxlength) to enforce a minimum number of array items; it treats `false` as missing (invalid), matching ``.
+Note: `required` treats an empty array as present (valid), so use [`minLength()`](#minlength-and-maxlength) to enforce a minimum number of array items.
### email()
@@ -505,9 +515,7 @@ interface User {
lastName: string;
}
-@Component({
- /* ... */
-})
+@Component({/* ... */})
export class UserFormComponent {
readonly userModel = model({
firstName: '',
@@ -750,9 +758,7 @@ import {Component, computed, signal} from '@angular/core';
import {form, FormField, validateStandardSchema} from '@angular/forms/signals';
import z from 'zod';
-@Component({
- /* ... */
-})
+@Component({/* ... */})
export class DynamicSchema {
model = signal({document: '', type: 'dni'});
diff --git a/packages/forms/signals/src/api/rules/validation/required.ts b/packages/forms/signals/src/api/rules/validation/required.ts
index 69132d5fa39..9478fb86320 100644
--- a/packages/forms/signals/src/api/rules/validation/required.ts
+++ b/packages/forms/signals/src/api/rules/validation/required.ts
@@ -17,6 +17,14 @@ import {requiredError} from './validation_errors';
* This function can only be called on any type of path.
* In addition to binding a validator, this function adds `REQUIRED` property to the field.
*
+ * A value is considered empty when it is `null`, `undefined`, the empty string `''`, `false`, or
+ * `NaN`. Every other value is considered non-empty, including `0` and the empty array `[]` — use
+ * [`minLength()`](api/forms/signals/minLength) to require a minimum number of items in an array.
+ *
+ * `false` is empty to follow the native semantics of `required` on ``, where
+ * an unchecked box fails validation. `NaN` is empty because it is usually the result of a parsing
+ * error, and is not a valid number.
+ *
* @param path Path of the field to validate
* @param config Optional, allows providing any of the following options:
* - `message`: A user-facing message for the error.
diff --git a/packages/forms/signals/test/node/api/validators/required.spec.ts b/packages/forms/signals/test/node/api/validators/required.spec.ts
index fe7b5f2b6f0..587782c99c4 100644
--- a/packages/forms/signals/test/node/api/validators/required.spec.ts
+++ b/packages/forms/signals/test/node/api/validators/required.spec.ts
@@ -12,6 +12,58 @@ import {form, required} from '../../../../public_api';
import {requiredError} from '../../../../src/api/rules/validation/validation_errors';
describe('required validator', () => {
+ // Documented on `required()` and in guide/forms/signals/validation#required. `false` follows the
+ // native semantics of `required` on ``; `NaN` is not a valid number.
+ describe('emptiness', () => {
+ it('treats null, empty string, false and NaN as empty', () => {
+ const model = signal<{
+ nullable: string | null;
+ text: string;
+ checkbox: boolean;
+ num: number;
+ }>({nullable: null, text: '', checkbox: false, num: Number.NaN});
+ const f = form(
+ model,
+ (p) => {
+ required(p.nullable);
+ required(p.text);
+ required(p.checkbox);
+ required(p.num);
+ },
+ {injector: TestBed.inject(Injector)},
+ );
+
+ expect(f.nullable().errors()).toEqual([requiredError({fieldTree: f.nullable})]);
+ expect(f.text().errors()).toEqual([requiredError({fieldTree: f.text})]);
+ expect(f.checkbox().errors()).toEqual([requiredError({fieldTree: f.checkbox})]);
+ expect(f.num().errors()).toEqual([requiredError({fieldTree: f.num})]);
+ });
+
+ it('treats 0, an empty array and other filled values as non-empty', () => {
+ const model = signal<{
+ zero: number;
+ list: string[];
+ checkbox: boolean;
+ text: string;
+ }>({zero: 0, list: [], checkbox: true, text: 'a'});
+ const f = form(
+ model,
+ (p) => {
+ required(p.zero);
+ required(p.list);
+ required(p.checkbox);
+ required(p.text);
+ },
+ {injector: TestBed.inject(Injector)},
+ );
+
+ expect(f.zero().errors()).toEqual([]);
+ expect(f.list().errors()).toEqual([]);
+ expect(f.checkbox().errors()).toEqual([]);
+ expect(f.text().errors()).toEqual([]);
+ });
+ });
+
it('returns required Error when the value is not present', () => {
const cat = signal({name: ''});
const f = form(