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:
parent
8e2ced8bed
commit
6bd105632a
@ -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>
|
||||||
|
|||||||
@ -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) {
|
||||||
if(!environment.production) console.log('Login submitted:', this.loginForm.value);
|
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);
|
this.authStore.login(this.loginForm.value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
frontend/src/app/core/helpers/forms.ts
Normal file
52
frontend/src/app/core/helpers/forms.ts
Normal 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() };
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user