refactor(core): extract validation logic into a Signal Factory

- Created getFormValidationErrors to get formatted angular form errors
- Implemented getMergedValidationErrors factory to unify local and server errors.
- Simplified Login component by using the new reusable utility.
- Implemented validation errors in register form
This commit is contained in:
kushal-saha 2026-04-29 06:03:00 +00:00
parent 8e2ced8bed
commit 6bd105632a
5 changed files with 100 additions and 22 deletions

View File

@ -9,7 +9,7 @@
<p class="m-0 mt-2 text-sm text-slate-400">Sign in to continue to Post Assistant.</p> <p class="m-0 mt-2 text-sm text-slate-400">Sign in to continue to Post Assistant.</p>
</div> </div>
<app-validation-errors [errors]="authStore.validationErrors()" /> <app-validation-errors [errors]="allErrors()" />
<form [formGroup]="loginForm" (ngSubmit)="login()" class="flex flex-col gap-5"> <form [formGroup]="loginForm" (ngSubmit)="login()" class="flex flex-col gap-5">
@ -40,7 +40,7 @@
<button <button
type="submit" type="submit"
class="mt-4 bg-gradient-to-br from-[#00f2fe] to-[#4facfe] border-none py-3.5 rounded-xl font-semibold text-white text-lg transition-all duration-200 hover:scale-[1.02] hover:-translate-y-0.5 hover:shadow-[0_10px_20px_rgba(79,172,254,0.3)] active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none" class="mt-4 bg-gradient-to-br from-[#00f2fe] to-[#4facfe] border-none py-3.5 rounded-xl font-semibold text-white text-lg transition-all duration-200 hover:scale-[1.02] hover:-translate-y-0.5 hover:shadow-[0_10px_20px_rgba(79,172,254,0.3)] active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none"
[disabled]="loginForm.invalid" [disabled]="authStore.isLoading()"
> >
Sign In Sign In
</button> </button>

View File

@ -1,10 +1,11 @@
import { Component, inject} from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { AuthStore } from '../auth.store'; import { AuthStore } from '../auth.store';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { ValidationErrors } from '../../shared/validation-errors/validation-errors'; import { ValidationErrors } from '../../shared/validation-errors/validation-errors';
import { getFormValidationErrors, getMergedValidationErrors } from '../../core/helpers/forms';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@ -16,18 +17,29 @@ import { ValidationErrors } from '../../shared/validation-errors/validation-erro
export class Login { export class Login {
loginForm: FormGroup; loginForm: FormGroup;
protected readonly authStore = inject(AuthStore); protected readonly authStore = inject(AuthStore);
private formErrors = signal(<Record<string, string[]>>{});
protected readonly allErrors = getMergedValidationErrors(
this.formErrors,
this.authStore.validationErrors,
);
constructor(private fb: FormBuilder) { constructor(private fb: FormBuilder) {
this.loginForm = this.fb.group({ this.loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required] password: ['', Validators.required],
}); });
} }
login() { login() {
if (this.loginForm.valid) { if (this.loginForm.touched && !this.loginForm.valid) {
this.loginForm.markAllAsTouched();
this.formErrors.set(getFormValidationErrors(this.loginForm));
return;
}
if (!environment.production) console.log('Login submitted:', this.loginForm.value); if (!environment.production) console.log('Login submitted:', this.loginForm.value);
this.formErrors.set({});
this.authStore.login(this.loginForm.value); this.authStore.login(this.loginForm.value);
} }
} }
}

View File

@ -1,14 +1,16 @@
<div class="w-full max-w-[450px] mx-auto mt-27 flex flex-col bg-white/5 backdrop-blur-[20px] border border-white/10 rounded-3xl shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5),inset_0_1px_0_rgba(255,255,255,0.1)] overflow-hidden animate-[slideUpFade_0.8s_cubic-bezier(0.16,1,0.3,1)] p-8"> <div class="w-full max-w-112.5 mx-auto mt-27 flex flex-col bg-white/5 backdrop-blur-[20px] border border-white/10 rounded-3xl shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5),inset_0_1px_0_rgba(255,255,255,0.1)] overflow-hidden animate-[slideUpFade_0.8s_cubic-bezier(0.16,1,0.3,1)] p-8">
<div class="text-center mb-8"> <div class="text-center mb-8">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-[#00f2fe] to-[#4facfe] flex items-center justify-center font-semibold text-2xl shadow-[0_0_20px_rgba(79,172,254,0.4)] animate-[pulse_2s_infinite]"> <div class="w-16 h-16 mx-auto mb-4 rounded-full bg-linear-to-br from-[#00f2fe] to-[#4facfe] flex items-center justify-center font-semibold text-2xl shadow-[0_0_20px_rgba(79,172,254,0.4)] animate-[pulse_2s_infinite]">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path>
</svg> </svg>
</div> </div>
<h1 class="m-0 text-3xl font-semibold tracking-wide bg-gradient-to-r from-white to-indigo-300 bg-clip-text text-transparent">Create Account</h1> <h1 class="m-0 text-3xl font-semibold tracking-wide bg-linear-to-r from-white to-indigo-300 bg-clip-text text-transparent">Create Account</h1>
<p class="m-0 mt-2 text-sm text-slate-400">Join us and start using Post Assistant.</p> <p class="m-0 mt-2 text-sm text-slate-400">Join us and start using Post Assistant.</p>
</div> </div>
<app-validation-errors [errors]="allErrors()" />
<form [formGroup]="registerForm" (ngSubmit)="register()" class="flex flex-col gap-5"> <form [formGroup]="registerForm" (ngSubmit)="register()" class="flex flex-col gap-5">
<div class="flex flex-col gap-1.5"> <div class="flex flex-col gap-1.5">
@ -61,8 +63,7 @@
<button <button
type="submit" type="submit"
class="mt-4 bg-gradient-to-br from-[#00f2fe] to-[#4facfe] border-none py-3.5 rounded-xl font-semibold text-white text-lg transition-all duration-200 hover:scale-[1.02] hover:-translate-y-0.5 hover:shadow-[0_10px_20px_rgba(79,172,254,0.3)] active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none" class="mt-4 bg-linear-to-br from-[#00f2fe] to-[#4facfe] border-none py-3.5 rounded-xl font-semibold text-white text-lg transition-all duration-200 hover:scale-[1.02] hover:-translate-y-0.5 hover:shadow-[0_10px_20px_rgba(79,172,254,0.3)] active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none"
[disabled]="registerForm.invalid"
> >
Sign Up Sign Up
</button> </button>

View File

@ -1,20 +1,28 @@
import { Component, inject} from '@angular/core'; import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { AuthStore } from '../auth.store'; import { AuthStore } from '../auth.store';
import { getFormValidationErrors, getMergedValidationErrors } from '../../core/helpers/forms';
import { ValidationErrors } from '../../shared/validation-errors/validation-errors';
@Component({ @Component({
selector: 'app-register', selector: 'app-register',
standalone: true, standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterLink], imports: [CommonModule, ReactiveFormsModule, RouterLink, ValidationErrors],
templateUrl: './register.html', templateUrl: './register.html',
styleUrl: './register.css', styleUrl: './register.css',
}) })
export class Register { export class Register {
registerForm: FormGroup; registerForm: FormGroup;
private readonly authStore = inject(AuthStore); protected readonly authStore = inject(AuthStore);
private formErrors = signal(<Record<string, string[]>>{});
protected readonly allErrors = getMergedValidationErrors(
this.formErrors,
this.authStore.validationErrors,
);
constructor(private fb: FormBuilder) { constructor(private fb: FormBuilder) {
this.registerForm = this.fb.group({ this.registerForm = this.fb.group({
@ -26,9 +34,14 @@ export class Register {
} }
register() { register() {
if (this.registerForm.valid) { if (this.registerForm.touched && !this.registerForm.valid) {
if(!environment.production) console.log('Register submitted:', this.registerForm.value); this.registerForm.markAllAsTouched();
this.formErrors.set(getFormValidationErrors(this.registerForm));
return;
}
if (!environment.production) console.log('Registration started:', this.registerForm.value);
this.formErrors.set({});
this.authStore.register(this.registerForm.value); this.authStore.register(this.registerForm.value);
} }
} }
}

View File

@ -0,0 +1,52 @@
import { FormGroup, ValidationErrors } from '@angular/forms';
import { computed, Signal } from '@angular/core';
export type FormErrors = Record<string, string[]> | null | undefined;
export function getFormValidationErrors(form: FormGroup): Record<string, string[]> {
const result: Record<string, string[]> = {};
Object.keys(form.controls).forEach((key) => {
const controlErrors: ValidationErrors | null | undefined = form.get(key)?.errors;
if (controlErrors) {
result[key] = [];
Object.keys(controlErrors).forEach((keyError) => {
let errorMessage = keyError;
// Translate Angular's built-in errors into readable sentences. add other validation errors here
switch (keyError) {
case 'required':
errorMessage = `The ${key} field is required.`;
break;
case 'email':
errorMessage = `The ${key} must be a valid email address.`;
break;
case 'minlength':
errorMessage = `The ${key} must be at least ${controlErrors[keyError].requiredLength} characters.`;
break;
case 'maxlength':
errorMessage = `The ${key} must not be greater than ${controlErrors[keyError].requiredLength} characters.`;
break;
case 'pattern':
errorMessage = `The ${key} format is invalid.`;
break;
}
result[key].push(errorMessage);
});
}
});
return result;
}
export function getMergedValidationErrors(
formErrors: Signal<FormErrors>,
serverErrors: Signal<FormErrors>,
): Signal<FormErrors> {
return computed(() => {
return { ...formErrors(), ...serverErrors() };
});
}