feat: add email format validator and switch RegisterComponent to ReactiveFormsModule

This commit is contained in:
Chneemann 2025-05-06 09:06:23 +02:00
parent de381bf5b9
commit c049f29407
4 changed files with 123 additions and 101 deletions

View file

@ -10,54 +10,50 @@
<a routerLink="/login">To the login</a> <a routerLink="/login">To the login</a>
</div> </div>
} @else { } @else {
<form <form
#registerForm="ngForm" [formGroup]="form"
(ngSubmit)="onSubmit(registerForm)" (ngSubmit)="onSubmit()"
onsubmit="return false"
(input)="this.errorService.clearError()" (input)="this.errorService.clearError()"
> >
<div class="mail-field"> <div class="mail-field">
<input <input
type="mail" formControlName="mail"
type="email"
id="mail" id="mail"
name="mail"
#mail="ngModel"
placeholder="Email Address" placeholder="Email Address"
autocomplete="email" autocomplete="email"
[(ngModel)]="authData.mail"
[class.error-border]=" [class.error-border]="
(!mail.valid && mail.touched) || form.controls['mail'].invalid && form.controls['mail'].touched
(mail.touched &&
!isUserEmailValid(authData.mail) &&
authData.mail.length > 0)
" "
required
/> />
<img class="mail-icon" src="./assets/img/mail.svg" /> <img class="mail-icon" src="./assets/img/mail.svg" />
</div> </div>
<div class="error-msg">
@if (!mail.valid && mail.touched) { <div class="error-container">
<p>Please enter your email</p> @if (form.controls['mail'].touched && form.controls['mail'].invalid) {
} @else { @if (mail.touched && authData.mail.length > 0 && <div class="error-msg">
!isUserEmailValid(authData.mail)) { @if (form.controls['mail'].errors?.['required']) {
<p>This is not a valid email format</p> <p role="alert">Please enter your email</p>
}} } @if (form.controls['mail'].errors?.['invalidEmailFormat']) {
<p role="alert">This is not a valid email format</p>
}
</div>
}
</div> </div>
<div class="password-field"> <div class="password-field">
<input <input
[type]="authService.passwordFieldType" [type]="authService.passwordFieldType"
id="password" id="password"
name="password" formControlName="password"
#password="ngModel"
placeholder="Enter a password" placeholder="Enter a password"
autocomplete="new-password" autocomplete="new-password"
[(ngModel)]="authData.password" minlength="8"
pattern="[^<>]*"
minlength="6"
[class.error-border]=" [class.error-border]="
password.invalid && password.touched && authData.password.length < 6 form.controls['password'].invalid &&
form.controls['password'].touched
" "
required
/> />
<img class="password-icon" src="./assets/img/password.svg" /> <img class="password-icon" src="./assets/img/password.svg" />
<img <img
@ -66,31 +62,32 @@
[src]="authService.passwordIcon" [src]="authService.passwordIcon"
/> />
</div> </div>
<div class="error-msg">
@if (!password.valid && password.touched && authData.password.length < <div class="error-container">
1) { @if (form.controls['password'].touched &&
<p>Please enter a password</p> form.controls['password'].invalid) {
} @else { @if (authData.password && authData.password.length < 6 && <div class="error-msg">
password.touched) { @if (form.controls['password'].errors?.['required']) {
<p>Password is too short, min 6 characters</p> <p role="alert">Please enter a password</p>
} } } @if (form.controls['password'].errors?.['minlength']) {
<p role="alert">Password is too short, min 8 characters</p>
}
</div>
}
</div> </div>
<div class="password-field"> <div class="password-field">
<input <input
[type]="authService.passwordFieldType" [type]="authService.passwordFieldType"
id="passwordConfirm" id="passwordConfirm"
name="passwordConfirm" formControlName="passwordConfirm"
#passwordConfirm="ngModel"
placeholder="Confirm password" placeholder="Confirm password"
autocomplete="new-password" autocomplete="new-password"
[(ngModel)]="authData.passwordConfirm" minlength="8"
pattern="[^<>]*"
minlength="6"
[class.error-border]=" [class.error-border]="
authData.password !== authData.passwordConfirm && form.controls['passwordConfirm'].invalid &&
passwordConfirm.touched form.controls['passwordConfirm'].touched
" "
required
/> />
<img class="password-icon" src="./assets/img/password.svg" /> <img class="password-icon" src="./assets/img/password.svg" />
<img <img
@ -99,37 +96,35 @@
[src]="authService.passwordIcon" [src]="authService.passwordIcon"
/> />
</div> </div>
<div class="error-msg">
@if (authData.password !== authData.passwordConfirm && <div class="error-container">
passwordConfirm.touched) { @if ( form.controls['passwordConfirm'].touched &&
<p>The passwords do not match</p> form.controls['passwordConfirm'].invalid ) {
<div class="error-msg">
@if (form.controls['passwordConfirm'].errors?.['required']) {
<p>Please confirm your password</p>
} @if ( form.controls['password'].value !==
form.controls['passwordConfirm'].value &&
form.controls['passwordConfirm'].touched ) {
<p>The passwords do not match</p>
}
</div>
} }
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label class="container" <label class="container">
>I agree to the&nbsp; I agree to the&nbsp;
<a routerLink="/privacy-policy">Privacy policy</a>. <a routerLink="/privacy-policy">Privacy policy</a>.
<input <input type="checkbox" formControlName="privacyPolicy" />
type="checkbox"
id="checkbox"
name="checkbox"
#checkbox
[(ngModel)]="authData.privacyPolicy"
/>
<span class="checkmark"></span> <span class="checkmark"></span>
</label> </label>
</div> </div>
<app-btn-large <app-btn-large
[value]="'Get Started'" [value]="'Get Started'"
[disabled]=" [disabled]="
(!isUserEmailValid(authData.mail) && form.invalid || form.controls['privacyPolicy'].value === false
authData.mail &&
authData.mail.length > 0) ||
authData.password !== authData.passwordConfirm ||
!authData.password ||
!authData.passwordConfirm ||
!authData.privacyPolicy ||
authData.send
" "
></app-btn-large> ></app-btn-large>
</form> </form>

View file

@ -1,34 +1,41 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BtnLargeComponent } from '../../../shared/components/buttons/btn-large/btn-large.component'; import { BtnLargeComponent } from '../../../shared/components/buttons/btn-large/btn-large.component';
import { FormsModule, NgForm } from '@angular/forms'; import {
FormBuilder,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AuthService } from '../../../services/auth.service'; import { AuthService } from '../../../services/auth.service';
import { ErrorService } from '../../../services/error.service'; import { ErrorService } from '../../../services/error.service';
import { emailFormatValidator } from '../../../validators/email-format.validator';
@Component({ @Component({
selector: 'app-register', selector: 'app-register',
standalone: true, standalone: true,
imports: [CommonModule, BtnLargeComponent, FormsModule, RouterLink], imports: [
CommonModule,
BtnLargeComponent,
FormsModule,
ReactiveFormsModule,
RouterLink,
],
templateUrl: './register.component.html', templateUrl: './register.component.html',
styleUrl: './register.component.scss', styleUrl: './register.component.scss',
}) })
export class RegisterComponent implements OnInit { export class RegisterComponent implements OnInit {
registrationSuccess: boolean = false; registrationSuccess: boolean = false;
form: FormGroup = new FormGroup({});
authData = {
mail: '',
password: '',
passwordConfirm: '',
privacyPolicy: false,
send: false,
};
/** /**
* Initializes the RegisterComponent with ActivatedRoute, AuthService, and ErrorService. * Initializes the RegisterComponent with ActivatedRoute, FormBuilder, AuthService, and ErrorService.
*/ */
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private fb: FormBuilder,
public authService: AuthService, public authService: AuthService,
public errorService: ErrorService public errorService: ErrorService
) {} ) {}
@ -37,68 +44,69 @@ export class RegisterComponent implements OnInit {
* Initializes the component by setting up necessary data. * Initializes the component by setting up necessary data.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.createRegisterForm();
this.setEmailFromQueryParams(); this.setEmailFromQueryParams();
} }
/**
* Initializes the registration form with required fields and validators.
*/
private createRegisterForm(): void {
this.form = this.fb.group({
mail: ['', [Validators.required, emailFormatValidator()]],
password: ['', Validators.required],
passwordConfirm: ['', Validators.required],
privacyPolicy: [false, Validators.requiredTrue],
});
}
/** /**
* Sets the email address from the query parameters in the URL. * Sets the email address from the query parameters in the URL.
*/ */
private setEmailFromQueryParams(): void { private setEmailFromQueryParams(): void {
this.route.queryParams.subscribe((params) => { this.route.queryParams.subscribe((params) => {
this.authData.mail = params['mail'] || ''; const email = params['mail'] || '';
this.form.patchValue({ mail: email });
}); });
} }
/** /**
* Validates the given email address. * Handles the submission of the registration form.
* Converts to lowercase before checking against the regex.
* *
* @param emailValue The email address to validate. * @returns A Promise that resolves when the registration process is complete.
* @returns True if the email format is valid, false otherwise.
*/ */
isUserEmailValid(emailValue: string): boolean { async onSubmit(): Promise<void> {
const emailRegex = /^[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,}$/; if (this.form.valid) {
return emailRegex.test(emailValue.toLowerCase());
}
/**
* Handles form submission for user registration.
* Sends registration request and handles success/error responses.
*
* @param ngForm The registration form instance.
*/
async onSubmit(ngForm: NgForm): Promise<void> {
if (ngForm.submitted && ngForm.form.valid) {
const body = this.createRegistrationRequestBody(); const body = this.createRegistrationRequestBody();
this.form.disable();
this.authData.send = true;
try { try {
await this.authService.register(body); await this.authService.register(body);
ngForm.resetForm(); this.form.reset();
this.registrationSuccess = true; this.registrationSuccess = true;
this.errorService.clearError(); this.errorService.clearError();
} catch (error) { } catch (error) {
this.authData.send = false; this.form.enable();
this.errorService.handleError(error); this.errorService.handleError(error);
} }
} }
} }
/** /**
* Creates the request body for the registration API. * Constructs the registration request payload from form values.
* *
* @returns The registration request body. * @returns An object containing the email (lowercased), username (from email), and password.
*/ */
private createRegistrationRequestBody(): { private createRegistrationRequestBody(): {
email: string; email: string;
username: string; username: string;
password: string; password: string;
} { } {
const formValue = this.form.value;
return { return {
email: this.authData.mail.toLowerCase(), email: formValue.mail.toLowerCase(),
username: this.authData.mail.split('@')[0], username: formValue.mail.split('@')[0],
password: this.authData.password, password: formValue.password,
}; };
} }
} }

View file

@ -0,0 +1,11 @@
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function emailFormatValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const email = control.value;
const emailRegex = /^[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,}$/;
const isValid = emailRegex.test(email?.toLowerCase());
return isValid ? null : { invalidEmailFormat: true };
};
}

View file

@ -47,6 +47,14 @@ form {
} }
} }
.error-container {
display: flex;
align-items: center;
justify-content: center;
height: 24px;
padding: 9px;
}
.error-msg { .error-msg {
display: flex; display: flex;
align-items: center; align-items: center;