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>
|
||||
</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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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