Skip to content
Merged
Show file tree
Hide file tree
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
36 changes: 18 additions & 18 deletions blog/2025-10-signal-forms-part1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: "Angular Signal Forms Part 1: Getting Started with the Basics"
author: Danny Koppenhagen and Ferdinand Malcher
mail: dannyferdigravatar@fmalcher.de # Gravatar
published: 2025-10-13
lastModified: 2025-11-09
lastModified: 2025-11-13
keywords:
- Angular
- Signals
Expand Down Expand Up @@ -368,7 +368,7 @@ The next part will cover more advanced and complex scenarios – so stay tuned!

Signal Forms use the `schema()` function to define validation rules.
Angular comes with some very common rules by default, such as `required` and `minLength`.
The provided `fieldPath` parameter allows us to navigate through the form structure and apply validation rules to specific fields.
The provided `schemaPath` parameter allows us to navigate through the form structure and apply validation rules to specific fields.

```typescript
import {
Expand All @@ -378,9 +378,9 @@ import {
minLength,
} from '@angular/forms/signals';

export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
required(fieldPath.username, { message: 'Username is required' });
minLength(fieldPath.username, 3, {
export const registrationSchema = schema<RegisterFormData>((schemaPath) => {
required(schemaPath.username, { message: 'Username is required' });
minLength(schemaPath.username, 3, {
message: 'A username must be at least 3 characters long',
});
// ...
Expand Down Expand Up @@ -414,31 +414,31 @@ Signal Forms provide several built-in validation functions:

| Validator | Description | Example |
| -------------------------------- | ----------------------------------------------------------- | ----------------------------------------------- |
| `required(field, opts)` | Field must be filled. For boolean values, checks for `true` | `required(fieldPath.username)` |
| `minLength(field, length, opts)` | Minimum character count | `minLength(fieldPath.username, 3)` |
| `maxLength(field, length, opts)` | Maximum character count | `maxLength(fieldPath.username, 10)` |
| `min(field, value, opts)` | Minimum numeric value | `min(fieldPath.age, 18)` |
| `max(field, value, opts)` | Maximum numeric value | `max(fieldPath.age, 120)` |
| `email(field, opts)` | Valid email address format | `email(fieldPath.email)` |
| `pattern(field, regex, opts)` | Regular expression match | `pattern(fieldPath.username, /^[a-zA-Z0-9]+$/)` |
| `required(field, opts)` | Field must be filled. For boolean values, checks for `true` | `required(schemaPath.username)` |
| `minLength(field, length, opts)` | Minimum character count | `minLength(schemaPath.username, 3)` |
| `maxLength(field, length, opts)` | Maximum character count | `maxLength(schemaPath.username, 10)` |
| `min(field, value, opts)` | Minimum numeric value | `min(schemaPath.age, 18)` |
| `max(field, value, opts)` | Maximum numeric value | `max(schemaPath.age, 120)` |
| `email(field, opts)` | Valid email address format | `email(schemaPath.email)` |
| `pattern(field, regex, opts)` | Regular expression match | `pattern(schemaPath.username, /^[a-zA-Z0-9]+$/)` |

Each validator function accepts an optional `opts` parameter where you can specify a custom error message.
We can use this message later to display it in the component template.

A validation schema for our registration form could look like this:

```typescript
export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
export const registrationSchema = schema<RegisterFormData>((schemaPath) => {
// Username validation
required(fieldPath.username, { message: 'Username is required' });
minLength(fieldPath.username, 3, { message: 'A username must be at least 3 characters long' });
maxLength(fieldPath.username, 12, { message: 'A username can be max. 12 characters long' });
required(schemaPath.username, { message: 'Username is required' });
minLength(schemaPath.username, 3, { message: 'A username must be at least 3 characters long' });
maxLength(schemaPath.username, 12, { message: 'A username can be max. 12 characters long' });

// Age validation
min(fieldPath.age, 18, { message: 'You must be >=18 years old.' });
min(schemaPath.age, 18, { message: 'You must be >=18 years old.' });

// Terms and conditions
required(fieldPath.agreeToTermsAndConditions, {
required(schemaPath.agreeToTermsAndConditions, {
message: 'You must agree to the terms and conditions.',
});
});
Expand Down
44 changes: 23 additions & 21 deletions blog/2025-10-signal-forms-part2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: 'Angular Signal Forms Part 2: Advanced Validation and Schema Patterns'
author: Danny Koppenhagen and Ferdinand Malcher
mail: dannyferdigravatar@fmalcher.de # Gravatar
published: 2025-10-15
lastModified: 2025-11-09
lastModified: 2025-11-13
keywords:
- Angular
- Signals
Expand Down Expand Up @@ -87,7 +87,7 @@ We can directly use this path and pass it into our `email()` validation function
import { /* ... */, applyEach, email } from '@angular/forms/signals';

// ...
applyEach(fieldPath.email, (emailPath) => {
applyEach(schemaPath.email, (emailPath) => {
email(emailPath, { message: 'E-Mail format is invalid' });
});
```
Expand Down Expand Up @@ -139,10 +139,10 @@ The `message` is optional, but it is recommended to provide a user-friendly mess
```typescript
import { /* ... */, validate } from '@angular/forms/signals';

export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
export const registrationSchema = schema<RegisterFormData>((schemaPath) => {
// ...
// E-Mail validation
validate(fieldPath.email, (ctx) =>
validate(schemaPath.email, (ctx) =>
!ctx.value().some((e) => e)
? {
kind: 'atLeastOneEmail',
Expand Down Expand Up @@ -225,27 +225,29 @@ Errors can be assigned to individual fields (`pw1`) or to the grouping node (`pa
For validations that depend on multiple fields, Signal Forms provide a `validateTree()` function.
The `ChildFieldContext` passed to the callback gives access to the entire subtree, allowing us to compare values of different fields.
An interesting aspect of this function is that we can assign errors to any field within the subtree.
Access to fields is possible through the `fieldOf()` method.
We can also use the `valueOf()` method to access values of other fields in the tree.

```typescript
import { /* ... */, validateTree } from '@angular/forms/signals';

export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
export const registrationSchema = schema<RegisterFormData>((schemaPath) => {
// ...
// Password confirmation validation
validateTree(fieldPath.password, (ctx) => {
validateTree(schemaPath.password, (ctx) => {
return ctx.value().pw2 === ctx.value().pw1
? undefined
: {
field: ctx.fieldOf(fieldPath.password.pw2), // assign the error to the second password field
field: ctx.field.pw2, // assign the error to the second password field
kind: 'confirmationPassword',
message: 'The entered password must match with the one specified in "Password" field',
};
});
});
```

Apart from this example, access to other fields is possible through the `fieldTreeOf()` method.
We can also use `valueOf()` to access values of other fields in the tree.


> `validateTree()` defines custom validation logic for a group of related fields. It returns a validation error or `undefined` if the values are valid.


Expand Down Expand Up @@ -285,14 +287,14 @@ We use the `validate()` function to check if a topic is selected.
```typescript
import { /* ... */, applyWhen } from '@angular/forms/signals';

export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
export const registrationSchema = schema<RegisterFormData>((schemaPath) => {
// ...
// Only validate newsletter topics if user subscribed to newsletter
applyWhen(
fieldPath,
schemaPath,
(ctx) => ctx.value().newsletter,
(fieldPathWhenTrue) => {
validate(fieldPathWhenTrue.newsletterTopics, (ctx) =>
(schemaPathWhenTrue) => {
validate(schemaPathWhenTrue.newsletterTopics, (ctx) =>
!ctx.value().length
? {
kind: 'noTopicSelected',
Expand Down Expand Up @@ -339,10 +341,10 @@ We also have to handle errors in the asynchronous operation, which can be done u
import { /* ... */, resource } from '@angular/core';
import { /* ... */, validateAsync } from '@angular/forms/signals';

export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
export const registrationSchema = schema<RegisterFormData>((schemaPath) => {
// ...
// Check username availability on the server
validateAsync(fieldPath.username, {
validateAsync(schemaPath.username, {
// Reactive parameters for the async operation
params: ({ value }) => value(),

Expand Down Expand Up @@ -380,7 +382,7 @@ If we enter `johndoe`, the validation will fail, and the corresponding error mes
For HTTP endpoints, you can also use the simpler `validateHttp()` function:

```typescript
validateHttp(fieldPath.username, {
validateHttp(schemaPath.username, {
request: (ctx) => `/api/check?username=${ctx.value()}`,
errors: (taken: boolean) =>
taken ? ({ kind: 'userExists', message: 'Username already taken' }) : undefined,
Expand Down Expand Up @@ -412,24 +414,24 @@ The corresponding field will change its state when the condition is met.
```typescript
import { /* ... */, disabled, readonly, hidden } from '@angular/forms/signals';

export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
export const registrationSchema = schema<RegisterFormData>((schemaPath) => {
// ...
// Disable newsletter topics when newsletter is unchecked
disabled(fieldPath.newsletterTopics, (ctx) => !ctx.valueOf(fieldPath.newsletter));
disabled(schemaPath.newsletterTopics, (ctx) => !ctx.valueOf(schemaPath.newsletter));
});
```

Here are some more examples of how to use `readonly` and `hidden`:

```typescript
// make `someField` read-only if `otherField` has the value 'someValue'
readonly(fieldPath.someField, (ctx) => ctx.valueOf(fieldPath.otherField) === 'someValue');
readonly(schemaPath.someField, (ctx) => ctx.valueOf(schemaPath.otherField) === 'someValue');

// make `someField` read-only if `otherField` is invalid
readonly(fieldPath.someField, (ctx) => !ctx.fieldOf(fieldPath.otherField)().valid());
readonly(schemaPath.someField, (ctx) => !ctx.fieldTreeOf(schemaPath.otherField)().valid());

// hide `someField` if the value of `otherField` is falsy
hidden(fieldPath.someField, (ctx) => !ctx.valueOf(fieldPath.otherField));
hidden(schemaPath.someField, (ctx) => !ctx.valueOf(schemaPath.otherField));
```

Disabled and read-only states are automatically reflected in the template when using the `[field]` directive.
Expand Down
6 changes: 3 additions & 3 deletions blog/2025-10-signal-forms-part3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: 'Angular Signal Forms Part 3: Child Forms and Custom UI Controls'
author: Danny Koppenhagen and Ferdinand Malcher
mail: dannyferdigravatar@fmalcher.de # Gravatar
published: 2025-10-20
lastModified: 2025-11-11
lastModified: 2025-11-13
keywords:
- Angular
- Signals
Expand Down Expand Up @@ -193,11 +193,11 @@ Next, we use the `apply()` function within our main schema to integrate the chil
// registrsation-form.ts
import { GenderIdentity, IdentityForm, identitySchema, initialGenderIdentityState } from '../identity-form/identity-form';

export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
export const registrationSchema = schema<RegisterFormData>((schemaPath) => {
// ...

// apply child schema for identity checks
apply(fieldPath.identity, identitySchema);
apply(schemaPath.identity, identitySchema);
});
```

Expand Down