Welcome to our journey of building a full-stack web application using Angular and GraphQL.
In the previous article of this series, we learned how to add the edit and delete features to our application. We also configured the home page and provided the sort and filter options for the list of movies.
In this article, we will add the feature for user registration in our application.
First, let’s create a class named UserRoles.cs within the Models folder and define the user roles in our app.
public static class UserRoles { public const string Admin = "Admin"; public const string User = "User"; }
This static class specifies the allowed user roles in our application.
Next, add another class named UserRegistration.cs in the Models folder with the following code.
public class UserRegistration { [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } [Required] public string Username { get; set; } [Required] [RegularExpression(@"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$")] public string Password { get; set; } [Required] [Compare("Password")] public string ConfirmPassword { get; set; } [Required] public string Gender { get; set; } public UserRegistration() { FirstName = string.Empty; LastName = string.Empty; Gender = string.Empty; Username = string.Empty; Password = string.Empty; ConfirmPassword = string.Empty; } }
This class is used to capture user registration details. All the fields of this class are marked optional. The [RegularExpression] attribute applied to the Password field is used to enforce password complexity rules: the password should have a minimum of 8 characters, at least 1 uppercase letter, 1 lowercase letter, and 1 number.
Next, add another class named RegistrationResponse.cs inside the Models folder.
public class RegistrationResponse { public bool IsRegistrationSuccess { get; set; } public string? ErrorMessage { get; set; } }
This class returns the registration response to the client.
In the MovieApp\Interfaces folder, add a new file called IUser.cs with the following method declarations.
public interface IUser { Task<bool> RegisterUser(UserMaster userData); Task<bool> IsUserExists(int userId); bool CheckUserNameAvailability(string username); }
Add a class named UserDataAccessLayer.cs inside the MovieApp\DataAccess folder with the following code.
public class UserDataAccessLayer: IUser { readonly MovieDbContext _dbContext; public UserDataAccessLayer(IDbContextFactory<MovieDbContext> dbContext) { _dbContext = dbContext.CreateDbContext(); } public async Task<bool> IsUserExists(int userId) { UserMaster? user = await _dbContext.UserMasters.FirstOrDefaultAsync(x => x.UserId == userId); return user != null; } public async Task<bool> RegisterUser(UserMaster userData) { bool isUserNameAvailable = CheckUserNameAvailability(userData.Username); try { if (isUserNameAvailable) { await _dbContext.UserMasters.AddAsync(userData); await _dbContext.SaveChangesAsync(); return true; } else { return false; } } catch { throw; } } public bool CheckUserNameAvailability(string userName) { string? user = _dbContext.UserMasters.FirstOrDefault(x => x.Username == userName)?.ToString(); return user == null; } }
We have implemented the IUser interface and added the definition for all the required methods:
In the MovieApp\GraphQL folder, add a class named AuthMutationResolver.cs.
[ExtendObjectType(typeof(MovieMutationResolver))] public class AuthMutationResolver { readonly IUser _userService; public AuthMutationResolver(IUser userService) { _userService = userService; } [GraphQLDescription("Register a new user.")] public async Task<RegistrationResponse> UserRegistration([FromBody] UserRegistration registrationData) { UserMaster user = new() { FirstName = registrationData.FirstName, LastName = registrationData.LastName, Username = registrationData.Username, Password = registrationData.Password, Gender = registrationData.Gender, UserTypeName = UserRoles.User }; bool userRegistrationStatus = await _userService.RegisterUser(user); if (userRegistrationStatus) { return new RegistrationResponse { IsRegistrationSuccess = true }; } else { return new RegistrationResponse { IsRegistrationSuccess = false, ErrorMessage = "This username is not available." }; } } }
The AuthMutationResolver class allows registering a new user in the app, using the IUser service to interact with the user data.
The UserRegistration asynchronous method is used to handle the user registration. It creates a new UserMaster object from the provided registration data and then uses the _userService to register the new user. A RegistrationResponse instance is returned with IsRegistrationSuccess set to true if the registration is successful. If the registration was unsuccessful, a RegistrationResponse instance is returned with IsRegistrationSuccess set to false and an error message indicating that the username is unavailable.
The ExtendObjectType attribute tells the GraphQL schema that the AuthMutationResolver class is extending the MovieMutationResolver type. This means that the fields and methods defined in AuthMutationResolver will be added to the MovieMutationResolver type in the GraphQL schema. This is a feature of the Hot Chocolate library that allows for a more modular and organized structure of your GraphQL schema.
Note: GraphQL restricts the creation of more than one mutation type.
As a new mutation resolver has been added, it must be registered in our middleware. Update the Program.cs file with the following code.
builder.Services.AddGraphQLServer() .AddQueryType<MovieQueryResolver>() .AddMutationType<MovieMutationResolver>() .AddTypeExtension<AuthMutationResolver>() .AddFiltering() .AddErrorFilter(error => { return error; });
We use the AddTypeExtension method to register the new mutation resolver type MovieMutationResolver. Then, register the transient lifetime of the IUser service using the following code.
builder.Services.AddTransient<IUser, UserDataAccessLayer>();
We are done with the server configuration. Let’s move to the client side of the app.
Add the following GraphQL mutation in the src\app\GraphQL\mutation.ts file. It will allow us to register a new user in the app.
export const REGISTER_USER = gql` mutation register($registrationData: UserRegistrationInput!) { userRegistration(registrationData: $registrationData) { isRegistrationSuccess } } `;
Create a new file named userRegistration.ts under the src\app\models folder and include the following code.
export interface UserRegistration { firstName: string; lastName: string; username: string; password: string; confirmPassword: string; gender: string; } export interface RegistrationResponse { isRegistrationSuccess: boolean; errorMessage: string; } export type RegistrationType = { userRegistration: RegistrationResponse; };
In the previous code example:
Then, create a new file named userRegistrationForm.ts in the Models folder and add the following code to it.
import { FormControl } from '@angular/forms'; export interface UserRegistrationForm { firstName: FormControl<string>; lastName: FormControl<string>; userName: FormControl<string>; password: FormControl<string>; confirmPassword: FormControl<string>; gender: FormControl<string>; }
This interface creates a strongly typed reactive form for capturing user registration details.
Run the following command in the ClientApp folder to generate a service file.
ng g s services\registration
Add the following code to the registration.service.ts file.
import { Injectable } from '@angular/core'; import { Mutation } from 'apollo-angular'; import { REGISTER_USER } from '../GraphQL/mutation'; import { RegistrationType } from '../models/userRegistration'; @Injectable({ providedIn: 'root', }) export class RegistrationService extends Mutation<RegistrationType> { document = REGISTER_USER; }
This service is used to register a new user using the GraphQL mutation.
Let’s create a custom form validator service. First, run the following command to generate a service file.
ng g s services\custom-form-validator
Then, add the following code to the custom-form-validator.service.ts file.
import { Injectable } from '@angular/core'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; @Injectable({ providedIn: 'root', }) export class CustomFormValidatorService { passwordPatternValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { if (!control.value) { return null; } const regex = new RegExp('^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$'); const validPattern = regex.test(control.value); return validPattern ? null : { invalidPassword: true }; }; } matchPasswordValidator(password: string, confirmPassword: string) { return (formGroup: AbstractControl): ValidationErrors | null => { const passwordControl = formGroup.get(password); const confirmPasswordControl = formGroup.get(confirmPassword); if (!passwordControl || !confirmPasswordControl) { return null; } if ( confirmPasswordControl.errors && !confirmPasswordControl.errors['passwordMismatch'] ) { return null; } if (passwordControl.value !== confirmPasswordControl.value) { confirmPasswordControl.setErrors({ passwordMismatch: true }); return { passwordMismatch: true }; } else { confirmPasswordControl.setErrors(null); return null; } }; } }
The custom form validator service provides two methods for validating form inputs, specifically for password fields:
Run the following command to create the user registration component.
ng g c components\user-registration
Update the UserRegistrationComponent class in the src\app\components\user-registration\user-registration.component.ts file as follows.
export class UserRegistrationComponent { protected userRegistrationForm!: FormGroup<UserRegistrationForm>; private destroyed$ = new ReplaySubject<void>(1); protected submitted = false; public data = ['Male', 'Female']; constructor( private readonly router: Router, private readonly formBuilder: NonNullableFormBuilder, private readonly customFormValidator: CustomFormValidatorService, private readonly registrationService: RegistrationService ) { this.initializeForm(); } private initializeForm(): void { this.userRegistrationForm = this.formBuilder.group( { firstName: this.formBuilder.control('', Validators.required), lastName: this.formBuilder.control('', Validators.required), userName: this.formBuilder.control('', Validators.required), password: this.formBuilder.control('', [ Validators.required, this.customFormValidator.passwordPatternValidator(), ]), confirmPassword: this.formBuilder.control('', Validators.required), gender: this.formBuilder.control('', Validators.required), }, { validators: [ this.customFormValidator.matchPasswordValidator( 'password', 'confirmPassword' ), ], } ); } }
The initializeForm method is called in the constructor to initialize the userRegistrationForm. It creates a form group with form controls for the first name, last name, username, password, confirm password, and gender. Each form control is initialized with an empty string and is required.
The password form control has an additional custom validator, the passwordPatternValidator, that validates the password pattern. The form group has a custom validator, matchPasswordValidator, that ensures the password and confirms that the password fields match.
Add the following functions to the UserRegistrationComponent class.
registerUser(): void { this.submitted = true; const userRegistrationData: UserRegistration = { firstName: this.userRegistrationForm.value.firstName ?? '', lastName: this.userRegistrationForm.value.lastName ?? '', username: this.userRegistrationForm.value.userName ?? '', password: this.userRegistrationForm.value.password ?? '', confirmPassword: this.userRegistrationForm.value.confirmPassword ?? '', gender: this.userRegistrationForm.value.gender ?? '', }; if (this.userRegistrationForm.valid) { this.registrationService .mutate({ registrationData: userRegistrationData, }) .pipe(takeUntil(this.destroyed$)) .subscribe({ next: (response) => { if (response.data?.userRegistration.isRegistrationSuccess) { ToastUtility.show({ content: 'User registration successful.', position: { X: 'Right', Y: 'Top' }, cssClass: 'e-toast-success', }); this. router.navigate(['/']); } else { this.userRegistrationForm.controls.userName.setErrors({ userNameNotAvailable: true, }); console.error( 'Error occurred during registration: ', response.data?.userRegistration.errorMessage ); } }, }); } }
get registrationFormControl() { return this.userRegistrationForm.controls; }
ngOnDestroy() { this.destroyed$.next(); this.destroyed$.complete(); }
The registerUser method is called to register a new user. It first sets the submitted property to true, which triggers validation messages in the template. Then, it creates a userRegistrationData object from the form values. The nullish coalescing operator (??) ensures that the object’s properties are not null or undefined.
If the form is valid, it calls the mutate method on the registrationService to register the new user. If the registration is successful, it displays a success toast notification and navigates to the root route. If the registration is unsuccessful, an error is set on the username form control indicating that the username is unavailable, and an error message is logged to the console.
The registrationFormControl returns the controls of the userRegistrationForm. This is used to access the form controls in the template.
Add the following code to the user-registration.component.html file.
<div class="row justify-content-center"> <div class="col-md-6 col-lg-6 col-sm-12"> <div class="title-container p-2 d-flex align-items-center justify-content-between"> <h2 class="m-0">User Registration</h2> <div> <span class="mat-h4">Already Registered? </span> <button ejs-button cssClass="e-primary" [routerLink]="['/login']"> Login </button> </div> </div> <div class="e-card"> <div class="e-card-content row g-0"> <form [formGroup]="userRegistrationForm" (ngSubmit)="registerUser()"> <div class="e-input-section"> <ejs-textbox placeholder="First name" cssClass="e-outline" floatLabelType="Auto" formControlName="firstName"></ejs-textbox> </div> <div *ngIf="(registrationFormControl.firstName.touched || submitted) && registrationFormControl.firstName.errors?.['required']" class="e-error"> First Name is required. </div> <div class="e-input-section"> <ejs-textbox placeholder="Last Name" cssClass="e-outline" floatLabelType="Auto" formControlName="lastName"></ejs-textbox> </div> <div *ngIf="(registrationFormControl.lastName.touched || submitted) && registrationFormControl.lastName.errors?.['required']" class="e-error"> Last Name is required. </div> <div class="e-input-section"> <ejs-textbox placeholder="User Name" cssClass="e-outline" floatLabelType="Auto" formControlName="userName"></ejs-textbox> </div> <div *ngIf="(registrationFormControl.userName.touched || submitted) && registrationFormControl.userName.errors?.['required']" class="e-error" > Usern Name is required. </div> <div *ngIf="(registrationFormControl.userName.touched || submitted) && registrationFormControl.userName.errors?.['userNameNotAvailable']" class="e-error" > Usern Name is not available. </div> <div class="e-input-section"> <ejs-textbox type="password" placeholder="Password" cssClass="e-outline" floatLabelType="Auto" formControlName="password"></ejs-textbox> </div> <div *ngIf="(registrationFormControl.password.touched || submitted) && registrationFormControl.password.errors?.['required']" class="e-error" > Password is required. </div> <div *ngIf="registrationFormControl.password.touched && registrationFormControl.password.errors?.['invalidPassword']" class="e-error"> Password should have minimum 8 characters, at least 1 uppercase letter, 1 lowercase letter, and 1 number. </div> <div class="e-input-section"> <ejs-textbox type="password" placeholder="Confirm Password" cssClass="e-outline" floatLabelType="Auto" formControlName="confirmPassword"></ejs-textbox> </div> <div *ngIf="(registrationFormControl.confirmPassword.touched || submitted) && registrationFormControl.confirmPassword.errors?.['required']" class="e-error"> Confirm password is required.</div> <div *ngIf=" registrationFormControl.confirmPassword.touched && registrationFormControl.confirmPassword.errors?.['passwordMismatch']" class="e-error"> Passwords do not match. </div> <div class="e-input-section"> <ejs-dropdownlist [dataSource]="data" placeholder="Select gender" cssClass="e-outline" floatLabelType="Auto" formControlName="gender"></ejs-dropdownlist> </div> <div *ngIf="(registrationFormControl.gender.touched || submitted) && registrationFormControl.gender.errors?.['required']" class="e-error"> Gender is required.</div> <div class="e-card-actions d-flex justify-content-end"> <button type="submit" ejs-button cssClass="e-info">Register</button> </div> </form> </div> </div> </div> </div>
The form element represents the user registration form. The formGroup attribute is bound to the userRegistrationForm property, and the ngSubmit event is bound to the registerUser method. The ejs-textbox components represent the form controls. The formControlName attribute binds each ejs-textbox to a form control in the userRegistrationForm.
On selecting the form controls or when the form is submitted, it will validate the details. If anything in the form control is empty, it will display an error message. If the password provided in the password form control is not in a valid pattern, then it will display an Invalid Password error message. If the password provided in the confirm password form control is wrong, it will display a Password Mismatch error message. If the username form control is empty, it will display a Username is Not Available error message.
The button element with the type=”submit” attribute is used to submit the form. The ejs-button directive is used to style the button.
The ejs-button directive with the routerLink attribute is used to navigate to the /login route, where a user can log in.
Note:
Finally, open the src\app\app-routing.module.ts file and add the route for the UserRegistration component under the appRoutes array.
const appRoutes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'register', component: UserRegistrationComponent }, // Existing code ];
After executing the previous code examples, we will get output like in the following image.
For more details, refer to the complete source code for the full-stack web app with Angular and GraphQL on GitHub.
Thanks for reading! In this article, we learned how to add the user registration feature to our full-stack web app using Angular and GraphQL.
In the next article of this series, we’ll learn to implement the login functionality and add role-based authorization to our app.
Whether you’re already a valued Syncfusion user or new to our platform, we extend an invitation to explore our Angular components with the convenience of a complimentary trial. This trial allows you to experience the full potential of our components to enhance your app’s user interface and functionality.
Our dedicated support system is readily available if you need guidance or have any inquiries. Contact us through our support forum, support portal, or feedback portal. Your success is our priority, and we’re always delighted to assist you on your development journey!