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>
</div>
<app-validation-errors [errors]="authStore.validationErrors()" />
<app-validation-errors [errors]="allErrors()" />
<form [formGroup]="loginForm" (ngSubmit)="login()" class="flex flex-col gap-5">
@ -40,7 +40,7 @@
<button
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"
[disabled]="loginForm.invalid"
[disabled]="authStore.isLoading()"
>
Sign In
</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 { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterLink } from '@angular/router';
import {AuthStore} from '../auth.store';
import {environment} from '../../../environments/environment';
import { AuthStore } from '../auth.store';
import { environment } from '../../../environments/environment';
import { ValidationErrors } from '../../shared/validation-errors/validation-errors';
import { getFormValidationErrors, getMergedValidationErrors } from '../../core/helpers/forms';
@Component({
selector: 'app-login',
@ -16,18 +17,29 @@ import { ValidationErrors } from '../../shared/validation-errors/validation-erro
export class Login {
loginForm: FormGroup;
protected readonly authStore = inject(AuthStore);
private formErrors = signal(<Record<string, string[]>>{});
protected readonly allErrors = getMergedValidationErrors(
this.formErrors,
this.authStore.validationErrors,
);
constructor(private fb: FormBuilder) {
this.loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required]
password: ['', Validators.required],
});
}
login() {
if (this.loginForm.valid) {
if(!environment.production) console.log('Login submitted:', this.loginForm.value);
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);
this.formErrors.set({});
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="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">
<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>
</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>
</div>
<app-validation-errors [errors]="allErrors()" />
<form [formGroup]="registerForm" (ngSubmit)="register()" class="flex flex-col gap-5">
<div class="flex flex-col gap-1.5">
@ -61,8 +63,7 @@
<button
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"
[disabled]="registerForm.invalid"
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"
>
Sign Up
</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 { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterLink } from '@angular/router';
import {environment} from '../../../environments/environment';
import {AuthStore} from '../auth.store';
import { environment } from '../../../environments/environment';
import { AuthStore } from '../auth.store';
import { getFormValidationErrors, getMergedValidationErrors } from '../../core/helpers/forms';
import { ValidationErrors } from '../../shared/validation-errors/validation-errors';
@Component({
selector: 'app-register',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterLink],
imports: [CommonModule, ReactiveFormsModule, RouterLink, ValidationErrors],
templateUrl: './register.html',
styleUrl: './register.css',
})
export class Register {
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) {
this.registerForm = this.fb.group({
@ -26,9 +34,14 @@ export class Register {
}
register() {
if (this.registerForm.valid) {
if(!environment.production) console.log('Register submitted:', this.registerForm.value);
if (this.registerForm.touched && !this.registerForm.valid) {
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);
}
}
}

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() };
});
}