diff --git a/apps/forms/62-crossfield-validation-signal-form/src/app/app.component.ts b/apps/forms/62-crossfield-validation-signal-form/src/app/app.component.ts index 7d2005867..6a89ba5e7 100644 --- a/apps/forms/62-crossfield-validation-signal-form/src/app/app.component.ts +++ b/apps/forms/62-crossfield-validation-signal-form/src/app/app.component.ts @@ -1,67 +1,25 @@ import { JsonPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { - AbstractControl, - FormControl, - FormGroup, - ReactiveFormsModule, - ValidationErrors, - ValidatorFn, - Validators, -} from '@angular/forms'; + FormField, + email, + form, + minLength, + required, + validate, +} from '@angular/forms/signals'; -function passwordMatchValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - const form = control as FormGroup; - if (!form) { - return null; - } - - const password = form.value.password; - const confirmPassword = form.value.confirmPassword; - - if (!confirmPassword) { - return null; - } - - if (password !== confirmPassword) { - form.controls['confirmPassword'].setErrors({ passwordMismatch: true }); - } - - return null; - }; -} - -function endDateAfterStartDateValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - const form = control as FormGroup; - if (!form) { - return null; - } - - const startDate = form.value.startDate; - const endDate = form.value.endDate; - - if (!startDate || !endDate) { - return null; - } - - const start = new Date(startDate).getTime(); - const end = new Date(endDate).getTime(); - - if (end > start) { - form.controls['endDate'].setErrors(null); - } else { - form.controls['endDate'].setErrors({ endDateBeforeStart: true }); - } - - return null; - }; +interface IFormValue { + email: string; + password: string; + confirmPassword: string; + startDate: string; + endDate: string; } @Component({ selector: 'app-root', - imports: [ReactiveFormsModule, JsonPipe], + imports: [FormField, JsonPipe], template: `
@@ -70,7 +28,7 @@ function endDateAfterStartDateValidator(): ValidatorFn { This form demonstrates cross field validation with reactive forms

-
+
@@ -198,22 +142,15 @@ function endDateAfterStartDateValidator(): ValidatorFn { - @if ( - form.controls.endDate.invalid && !form.controls.endDate.untouched - ) { + @if (form.endDate().invalid() && form.endDate().touched()) {

- @if (form.controls.endDate.hasError('required')) { - End date is required - } @else if ( - form.controls.endDate.hasError('endDateBeforeStart') - ) { - End date must be after start date + @for (error of form.endDate().errors(); track error) { + {{ error.message }} }

} @@ -222,7 +159,7 @@ function endDateAfterStartDateValidator(): ValidatorFn {
@@ -241,25 +178,25 @@ function endDateAfterStartDateValidator(): ValidatorFn {
Valid: - {{ form.valid ? 'Yes' : 'No' }} + [class.text-green-600]="form().valid()" + [class.text-red-600]="form().invalid()"> + {{ form().valid() ? 'Yes' : 'No' }}
Touched: - {{ !form.untouched ? 'Yes' : 'No' }} + {{ form().touched() ? 'Yes' : 'No' }}
Dirty: - {{ form.dirty ? 'Yes' : 'No' }} + {{ form().dirty() ? 'Yes' : 'No' }}

Form Value:

{{ form.value | json }}
{{ form().value() | json }}
@@ -271,7 +208,7 @@ function endDateAfterStartDateValidator(): ValidatorFn {
{{ this.form.getRawValue() | json }}
{{ form().value() | json }}
} @@ -283,46 +220,64 @@ function endDateAfterStartDateValidator(): ValidatorFn { export class AppComponent { public isSubmitted = signal(false); - form = new FormGroup( - { - email: new FormControl('', [Validators.required, Validators.email]), - password: new FormControl('', [ - Validators.required, - Validators.minLength(6), - ]), - confirmPassword: new FormControl('', [Validators.required]), - startDate: new FormControl('', [Validators.required]), - endDate: new FormControl('', [Validators.required]), - }, - { - validators: [passwordMatchValidator(), endDateAfterStartDateValidator()], - }, - ); + formDefaultValue: IFormValue = { + email: '', + password: '', + confirmPassword: '', + startDate: '', + endDate: '', + }; + + formModel = signal(this.formDefaultValue); + + form = form(this.formModel, (path) => { + required(path.email, { message: 'Email is required' }); + email(path.email, { message: 'Please enter a valid email address' }); + + required(path.password, { message: 'Password is required' }); + minLength(path.password, 6, { + message: 'Password must be at least 6 characters', + }); + + required(path.confirmPassword, { message: 'Please confirm your password' }); + validate(path.confirmPassword, ({ value, valueOf }) => + value() !== valueOf(path.password) + ? { + kind: 'passwordMismatch', + message: 'Passwords do not match', + } + : null, + ); + + required(path.startDate, { message: 'Start date is required' }); - // constructor() { - // this.form.controls.password.valueChanges - // .pipe(takeUntilDestroyed()) - // .subscribe(() => { - // this.form.controls.confirmPassword.updateValueAndValidity(); - // }); - // - // this.form.controls.startDate.valueChanges - // .pipe(takeUntilDestroyed()) - // .subscribe(() => { - // this.form.controls.endDate.updateValueAndValidity(); - // }); - // } + required(path.endDate, { message: 'End date is required' }); + validate(path.endDate, ({ value, valueOf }) => { + const startDate = valueOf(path.startDate); + const endDate = value(); + if (!startDate || !endDate) { + return null; + } + return new Date(startDate) > new Date(endDate) + ? { + kind: 'endDateBeforeStart', + message: 'End date must be after start date', + } + : null; + }); + }); - onSubmit() { - console.log('Submitting form...', this.form); - if (this.form.valid) { + onSubmit(e: Event) { + e.preventDefault(); + console.log('Submitting form...', this.form()); + if (this.form().valid()) { this.isSubmitted.set(true); - console.log('Form submitted:', this.form.getRawValue()); + console.log('Form submitted:', this.form().value()); } } onReset() { - this.form.reset(); + this.formModel.set(this.formDefaultValue); this.isSubmitted.set(false); } }