docs(forms): document what required() considers empty

The `required()` validator treats `null`, `undefined`, `''`, `false` and `NaN` as empty, but the API
docs never defined "empty" at all and the validation guide listed only `null` and `''`.

(cherry picked from commit 467b37b4b4)
This commit is contained in:
Shuaib Hasan Akib
2026-09-12 04:16:17 +06:00
committed by Andrew Scott
parent 4b5571c324
commit 376b71ea1c
3 changed files with 78 additions and 12 deletions
@@ -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 `<input type="checkbox">`, 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 `<input type="checkbox" required>`.
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<User>({
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'});
@@ -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 `<input type="checkbox">`, 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.
@@ -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 `<input type="checkbox">`; `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(