Skip to content
Open
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
253 changes: 104 additions & 149 deletions apps/forms/62-crossfield-validation-signal-form/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -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: `
<div class="min-h-screen bg-gray-100 px-4 py-12 sm:px-6 lg:px-8">
<div class="mx-auto max-w-md rounded-lg bg-white p-8 shadow-md">
Expand All @@ -70,7 +28,7 @@ function endDateAfterStartDateValidator(): ValidatorFn {
This form demonstrates cross field validation with reactive forms
</p>

<form [formGroup]="form" (ngSubmit)="onSubmit()" class="space-y-6">
<form (submit)="onSubmit($event)" class="space-y-6">
<div>
<label
for="email"
Expand All @@ -81,20 +39,16 @@ function endDateAfterStartDateValidator(): ValidatorFn {
<input
id="email"
type="email"
formControlName="email"
[formField]="form.email"
placeholder="Enter your email"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
[class.border-red-500]="
form.controls.email.invalid && !form.controls.email.untouched
form.email().invalid() && form.email().touched()
" />
@if (
form.controls.email.invalid && !form.controls.email.untouched
) {
@if (form.email().invalid() && form.email().touched()) {
<p class="mt-1 text-sm text-red-600">
@if (form.controls.email.hasError('required')) {
Email is required
} @else if (form.controls.email.hasError('email')) {
Please enter a valid email address
@for (error of form.email().errors(); track error) {
{{ error.message }}
}
</p>
}
Expand All @@ -110,22 +64,16 @@ function endDateAfterStartDateValidator(): ValidatorFn {
<input
id="password"
type="password"
formControlName="password"
[formField]="form.password"
placeholder="Enter your password"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
[class.border-red-500]="
form.controls.password.invalid &&
!form.controls.password.untouched
form.password().invalid() && form.password().touched()
" />
@if (
form.controls.password.invalid &&
!form.controls.password.untouched
) {
@if (form.password().invalid() && form.password().touched()) {
<p class="mt-1 text-sm text-red-600">
@if (form.controls.password.hasError('required')) {
Password is required
} @else if (form.controls.password.hasError('minlength')) {
Password must be at least 6 characters
@for (error of form.password().errors(); track error) {
{{ error.message }}
}
</p>
}
Expand All @@ -141,24 +89,20 @@ function endDateAfterStartDateValidator(): ValidatorFn {
<input
id="confirmPassword"
type="password"
formControlName="confirmPassword"
[formField]="form.confirmPassword"
placeholder="Confirm your password"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
[class.border-red-500]="
form.controls.confirmPassword.invalid &&
!form.controls.confirmPassword.untouched
form.confirmPassword().invalid() &&
form.confirmPassword().touched()
" />
@if (
form.controls.confirmPassword.invalid &&
!form.controls.confirmPassword.untouched
form.confirmPassword().invalid() &&
form.confirmPassword().touched()
) {
<p class="mt-1 text-sm text-red-600">
@if (form.controls.confirmPassword.hasError('required')) {
Please confirm your password
} @else if (
form.controls.confirmPassword.hasError('passwordMismatch')
) {
Passwords do not match
@for (error of form.confirmPassword().errors(); track error) {
{{ error.message }}
}
</p>
}
Expand All @@ -174,17 +118,17 @@ function endDateAfterStartDateValidator(): ValidatorFn {
<input
id="startDate"
type="date"
formControlName="startDate"
[formField]="form.startDate"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
[class.border-red-500]="
form.controls.startDate.invalid &&
!form.controls.startDate.untouched
form.startDate().invalid() && form.startDate().touched()
" />
@if (
form.controls.startDate.invalid &&
!form.controls.startDate.untouched
) {
<p class="mt-1 text-sm text-red-600">Start date is required</p>
@if (form.startDate().invalid() && form.startDate().touched()) {
<p class="mt-1 text-sm text-red-600">
@for (error of form.startDate().errors(); track error) {
{{ error.message }}
}
</p>
}
</div>

Expand All @@ -198,22 +142,15 @@ function endDateAfterStartDateValidator(): ValidatorFn {
<input
id="endDate"
type="date"
formControlName="endDate"
[formField]="form.endDate"
class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
[class.border-red-500]="
form.controls.endDate.invalid &&
!form.controls.endDate.untouched
form.endDate().invalid() && form.endDate().touched()
" />
@if (
form.controls.endDate.invalid && !form.controls.endDate.untouched
) {
@if (form.endDate().invalid() && form.endDate().touched()) {
<p class="mt-1 text-sm text-red-600">
@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 }}
}
</p>
}
Expand All @@ -222,7 +159,7 @@ function endDateAfterStartDateValidator(): ValidatorFn {
<div class="flex gap-4">
<button
type="submit"
[disabled]="form.invalid"
[disabled]="form().invalid()"
class="flex-1 rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400">
Submit
</button>
Expand All @@ -241,25 +178,25 @@ function endDateAfterStartDateValidator(): ValidatorFn {
<div class="flex justify-between">
<span class="font-medium text-gray-700">Valid:</span>
<span
[class.text-green-600]="form.valid"
[class.text-red-600]="form.invalid">
{{ form.valid ? 'Yes' : 'No' }}
[class.text-green-600]="form().valid()"
[class.text-red-600]="form().invalid()">
{{ form().valid() ? 'Yes' : 'No' }}
</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-700">Touched:</span>
<span>{{ !form.untouched ? 'Yes' : 'No' }}</span>
<span>{{ form().touched() ? 'Yes' : 'No' }}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-700">Dirty:</span>
<span>{{ form.dirty ? 'Yes' : 'No' }}</span>
<span>{{ form().dirty() ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="mt-4">
<h3 class="mb-2 font-medium text-gray-700">Form Value:</h3>
<pre
class="overflow-x-auto rounded bg-gray-800 p-3 text-xs text-gray-100"
>{{ form.value | json }}</pre
>{{ form().value() | json }}</pre
>
</div>
</div>
Expand All @@ -271,7 +208,7 @@ function endDateAfterStartDateValidator(): ValidatorFn {
</h2>
<pre
class="overflow-x-auto rounded bg-green-800 p-3 text-xs text-green-100"
>{{ this.form.getRawValue() | json }}</pre
>{{ form().value() | json }}</pre
>
</div>
}
Expand All @@ -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<IFormValue>(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);
}
}
Loading