add client side validation errors

This commit is contained in:
kusowl 2026-02-23 15:07:43 +05:30
parent 77532aaac2
commit 78bf326622
6 changed files with 151 additions and 60 deletions

View File

@ -1,51 +1,74 @@
<section class="my-10 md:my-30 flex flex-col md:flex-row space-x-20 space-y-10 justify-center items-center"> <section
class="my-10 md:my-30 flex flex-col md:flex-row space-x-20 space-y-10 justify-center items-center"
>
<article class="space-y-6"> <article class="space-y-6">
<h1 class="text-3xl text-gray-800 font-space font-bold">Register</h1> <h1 class="text-3xl text-gray-800 font-space font-bold">Register</h1>
<h2 class="text-xl text-gray-600">Sign up with your<br/>email address to get started</h2> <h2 class="text-xl text-gray-600">Sign up with your<br />email address to get started</h2>
<div class="text-xs text-red-600 space-y-2"> <div class="text-xs text-red-600 space-y-2">
@for (error of errors(); track error) { @for (error of errors(); track error) {
<p>{{ error }}</p> <p>{{ error }}</p>
} }
</div> </div>
</article> </article>
<form [formGroup]="registerForm" (ngSubmit)="registerUser()" <form
class="card max-w-11/12 sm:max-w-8/12 grid md:grid-cols-2 gap-4"> [formGroup]="registerForm"
<article class="space-y-4"> (ngSubmit)="registerUser()"
<fieldset class="fieldset"> class="card max-w-11/12 sm:max-w-8/12 grid md:grid-cols-2 gap-4"
<legend class="fieldset-legend">Name</legend> >
<input type="text" class="input" formControlName="name" placeholder="Jhon Doe"/> <fieldset class="fieldset">
</fieldset> <legend class="fieldset-legend">Name*</legend>
<fieldset class="fieldset"> <input type="text" class="input" formControlName="name" placeholder="Jhon Doe" />
<legend class="fieldset-legend">Mobile Number</legend> <app-error fieldName="name" [control]="registerForm.get('name')" />
<input type="text" class="input" formControlName="mobile_number" placeholder="+X1 XXXXXXXXXX"/> </fieldset>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Email</legend>
<input type="text" class="input" formControlName="email" placeholder="Enter email here"/>
<p class="label">your-email-address@email.com</p>
</fieldset>
</article>
<article class="space-y-4">
<fieldset class="fieldset">
<legend class="fieldset-legend">Password</legend>
<input type="password" class="input" formControlName="password" placeholder="Type here"/>
</fieldset>
<fieldset class="fieldset"> <fieldset class="fieldset">
<legend class="fieldset-legend">Confirm Password</legend> <legend class="fieldset-legend">Mobile Number*</legend>
<input type="text" class="input" formControlName="password_confirmation" placeholder="Type here"/> <input
</fieldset> type="text"
class="input"
formControlName="mobile_number"
placeholder="+X1 XXXXXXXXXX"
/>
<app-error fieldName="mobile number" [control]="registerForm.get('mobile_number')" />
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Email*</legend>
<input type="text" class="input" formControlName="email" placeholder="Enter email here" />
<p class="label">your-email-address@email.com</p>
<app-error fieldName="email" [control]="registerForm.get('email')" />
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">City*</legend>
<input type="text" class="input" formControlName="city" placeholder="Your city name" />
<app-error fieldName="city" [control]="registerForm.get('city')" />
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Password*</legend>
<input type="password" class="input" formControlName="password" placeholder="Type here" />
<app-error fieldName="password" [control]="registerForm.get('password')" />
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">Confirm Password*</legend>
<input
type="text"
class="input"
formControlName="password_confirmation"
placeholder="Type here"
/>
<app-error [control]="registerForm.get('password_confirmation')" />
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend">City</legend>
<input type="text" class="input" formControlName="city" placeholder="Your city name"/>
</fieldset>
</article>
<div class="flex flex-col col-span-2 gap-y-2"> <div class="flex flex-col col-span-2 gap-y-2">
<button type="submit" class="btn btn-black py-2 w-full">Register</button> <button type="submit" [disabled]="registerForm.invalid" class="btn btn-black py-2 w-full">
Register
</button>
<a href="" class="text-xs text-gray-800 text-center w-full block hover:text-teal-600" <a href="" class="text-xs text-gray-800 text-center w-full block hover:text-teal-600"
>Already have an account ? Login</a >Already have an account ? Login</a
> >
</div> </div>
</form> </form>

View File

@ -1,39 +1,42 @@
import {Component, inject, signal} from "@angular/core"; import { Component, inject, signal } from "@angular/core";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import {AuthService} from "../../services/auth-service"; import { AuthService } from "../../services/auth-service";
import {RegisterUserRequest} from "../../../../core/models/user.model"; import { RegisterUserRequest } from "../../../../core/models/user.model";
import { Error } from "../../../../shared/components/error/error";
@Component({ @Component({
selector: "app-register", selector: "app-register",
imports: [ReactiveFormsModule], imports: [ReactiveFormsModule, Error],
templateUrl: "./register.html", templateUrl: "./register.html",
styleUrl: "./register.css", styleUrl: "./register.css",
}) })
export class Register { export class Register {
authService = inject(AuthService); authService = inject(AuthService);
errors = signal<string[]>([]); errors = signal<string[]>([]);
registerForm = new FormGroup({ registerForm = new FormGroup({
name: new FormControl(''), name: new FormControl("", { validators: [Validators.required] }),
email: new FormControl(''), email: new FormControl("", { validators: [Validators.required, Validators.email] }),
mobile_number: new FormControl(''), mobile_number: new FormControl("", { validators: [Validators.required] }),
password: new FormControl(''), password: new FormControl("", { validators: [Validators.required] }),
password_confirmation: new FormControl(''), password_confirmation: new FormControl("", { validators: [Validators.required] }),
city: new FormControl(''), city: new FormControl("", { validators: [Validators.required] }),
}); });
registerUser() { registerUser() {
this.authService.register(this.registerForm.value as RegisterUserRequest) // validation errors if user submit early
.subscribe({ if (this.registerForm.invalid) {
next: () => console.log('success'), this.registerForm.markAllAsTouched();
error: (error) => { return;
const errors: Record<number, string[]> = error?.error?.errors || {}; }
const errorMessages :string[] = Object.values(errors).flat();
this.errors.set(errorMessages) ; this.authService.register(this.registerForm.value as RegisterUserRequest).subscribe({
} next: () => console.log("success"),
}); error: (error) => {
const errors: Record<number, string[]> = error?.error?.errors || {};
const errorMessages: string[] = Object.values(errors).flat();
this.errors.set(errorMessages);
},
});
} }
} }

View File

@ -0,0 +1,47 @@
import { Component, Input } from "@angular/core";
import { AbstractControl } from "@angular/forms";
import { UpperCaseFirstPipe } from "../../pipes/upper-case-first-pipe";
@Component({
selector: "app-error",
imports: [UpperCaseFirstPipe],
template: `
@if (this.control && this.control.touched) {
@for (error of getErrorMessages(); track error) {
<p class="ml-2 text-xs text-red-400">{{ error | upperCaseFirst }}</p>
}
}
`,
})
export class Error {
@Input() control!: AbstractControl | null;
@Input() fieldName = "This field";
getErrorMessages() {
const messages: string[] = [];
if (this.control && this.control.errors) {
const errors = this.control.errors;
if (errors["required"]) {
messages.push(`${this.fieldName} is required.`);
}
if (errors["email"]) {
messages.push(`Please enter a valid email address.`);
}
if (errors["minlength"]) {
messages.push(
`${this.fieldName} must be at least ${errors["minlength"].requiredLength} characters.`,
);
}
if (errors["pattern"]) {
messages.push(`${this.fieldName} is formatted incorrectly.`);
}
if (errors["serverError"]) {
messages.push(errors["serverError"]);
}
}
return messages;
}
}

View File

@ -0,0 +1,8 @@
import { UpperCaseFirstPipe } from "./upper-case-first-pipe";
describe("UpperCaseFirstPipe", () => {
it("create an instance", () => {
const pipe = new UpperCaseFirstPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,10 @@
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: "upperCaseFirst",
})
export class UpperCaseFirstPipe implements PipeTransform {
transform(value: string): unknown {
return value.charAt(0).toUpperCase() + value.slice(1);
}
}

View File

@ -21,7 +21,7 @@ body {
} }
.btn { .btn {
@apply rounded-full transition-all duration-200 font-medium ease-out flex justify-center active:translate-y-[1px]; @apply rounded-full transition-all duration-200 font-medium ease-out flex justify-center active:translate-y-px disabled:opacity-50 disabled:cursor-not-allowed;
} }
.btn-ghost { .btn-ghost {