@@ -3,7 +3,7 @@ title: 'Angular Signal Forms Part 2: Advanced Validation and Schema Patterns'
33author : Danny Koppenhagen and Ferdinand Malcher
44mail : dannyferdigravatar@fmalcher.de # Gravatar
55published : 2025-10-15
6- lastModified : 2025-10-16
6+ lastModified : 2025-10-30
77keywords :
88 - Angular
99 - Signals
@@ -132,22 +132,22 @@ The callback function provides access to the field state, represented as a `Chil
132132This object can be used to access the ` value ` signal and read the current value of the email array.
133133
134134Since the value is a ` string[] ` , we can use ` Array.some() ` to check if at least one non-empty email address exists.
135- To produce an error, we use the ` customError() ` function to create a validation error object with a ` kind ` and a ` message ` .
135+ To produce an error, we return a validation error object with a ` kind ` and a ` message ` .
136136If no error occurs, we return ` undefined ` .
137137The ` message ` is optional, but it is recommended to provide a user-friendly message that can be displayed in the UI later.
138138
139139``` typescript
140- import { /* ... */ , validate , customError } from ' @angular/forms/signals' ;
140+ import { /* ... */ , validate } from ' @angular/forms/signals' ;
141141
142142export const registrationSchema = schema <RegisterFormData >((fieldPath ) => {
143143 // ...
144144 // E-Mail validation
145145 validate (fieldPath .email , (ctx ) =>
146146 ! ctx .value ().some ((e ) => e )
147- ? customError ( {
147+ ? {
148148 kind: ' atLeastOneEmail' ,
149149 message: ' At least one E-Mail address must be added' ,
150- })
150+ }
151151 : undefined
152152 );
153153});
@@ -237,11 +237,11 @@ export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
237237 validateTree (fieldPath .password , (ctx ) => {
238238 return ctx .value ().pw2 === ctx .value ().pw1
239239 ? undefined
240- : customError ( {
240+ : {
241241 field: ctx .fieldOf (fieldPath .password .pw2 ), // assign the error to the second password field
242242 kind: ' confirmationPassword' ,
243243 message: ' The entered password must match with the one specified in "Password" field' ,
244- }) ;
244+ };
245245 });
246246});
247247```
@@ -294,10 +294,10 @@ export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
294294 (fieldPathWhenTrue ) => {
295295 validate (fieldPathWhenTrue .newsletterTopics , (ctx ) =>
296296 ! ctx .value ().length
297- ? customError ( {
297+ ? {
298298 kind: ' noTopicSelected' ,
299299 message: ' Select at least one newsletter topic' ,
300- })
300+ }
301301 : undefined
302302 );
303303 }
@@ -332,7 +332,8 @@ export class RegistrationService {
332332To perform async validation, we can use the ` validateAsync() ` function in our schema.
333333The ` params ` property allows us to pick the required data from the field state, again represented as a ` ChildFieldContext ` object.
334334The ` factory ` property is a function that creates a resource that actually performs the async operation.
335- Finally, the ` errors ` function maps the value of the resource to a validation error, just as we did before with custom synchronous validations.
335+ Finally, the ` onSuccess ` function maps the value of the resource to a validation error, just as we did before with custom synchronous validations.
336+ We also have to handle errors in the asynchronous operation, which can be done using the ` onError ` property. If the validation fails due to a server error, we ignore it by returning ` undefined ` .
336337
337338``` typescript
338339import { /* ... */ , resource } from ' @angular/core' ;
@@ -357,14 +358,15 @@ export const registrationSchema = schema<RegisterFormData>((fieldPath) => {
357358 },
358359
359360 // Map the result to validation errors
360- errors : (result ) => {
361+ onSuccess : (result ) => {
361362 return result
362- ? customError ( {
363+ ? {
363364 kind: ' userExists' ,
364365 message: ' The username you entered was already taken' ,
365- })
366+ }
366367 : undefined ;
367368 },
369+ onError : () => undefined
368370 });
369371});
370372```
@@ -381,7 +383,7 @@ For HTTP endpoints, you can also use the simpler `validateHttp()` function:
381383validateHttp (fieldPath .username , {
382384 request : (ctx ) => ` /api/check?username=${ctx .value ()} ` ,
383385 errors : (taken : boolean ) =>
384- taken ? customError ({ kind: ' userExists' , message: ' Username already taken' }) : undefined ,
386+ taken ? ({ kind: ' userExists' , message: ' Username already taken' }) : undefined ,
385387});
386388```
387389
@@ -452,15 +454,15 @@ When using the `submit()` function, we can return an array of validation errors
452454The helper type ` WithField ` ensures that each error contains a reference to the field it belongs to.
453455
454456``` typescript
455- import { /* ... */ , WithField , CustomValidationError , ValidationError } from ' @angular/forms/signals' ;
457+ import { /* ... */ , WithField , ValidationErrorWithField } from ' @angular/forms/signals' ;
456458
457459export class RegistrationForm {
458460 // ...
459461 protected async submitForm(e : Event ) {
460462 e .preventDefault ();
461463
462464 await submit (this .registrationForm , async (form ) => {
463- const errors: WithField < CustomValidationError | ValidationError > [] = [];
465+ const errors: ValidationErrorWithField [] = [];
464466
465467 try {
466468 await this .#registrationService .registerUser (form ().value );
@@ -469,24 +471,20 @@ export class RegistrationForm {
469471 } catch (e ) {
470472 // Add server-side errors
471473 errors .push (
472- customError ( {
474+ {
473475 field: form , // form-level error
474- error: {
475- kind: ' serverError' ,
476- message: ' Registration failed. Please try again.' ,
477- },
478- })
476+ kind: ' serverError' ,
477+ message: ' Registration failed. Please try again.' ,
478+ }
479479 );
480480
481481 // Or assign to specific field
482482 errors .push (
483- customError ( {
483+ {
484484 field: form .username ,
485- error: {
486- kind: ' serverValidation' ,
487- message: ' Username is not available.' ,
488- },
489- })
485+ kind: ' serverValidation' ,
486+ message: ' Username is not available.' ,
487+ }
490488 );
491489 }
492490
0 commit comments