DEV Community

riyo singh
riyo singh

Posted on

Building a Signup Form in Blazor with Validation and API Integration

Signup Form

Here is the complete Blazor Signup form:

@page "/signup"
@using System.ComponentModel.DataAnnotations
@inject HttpClient http
@inject NavigationManager navigation
@inject SweetAlertService Swal

<div class="d-flex justify-content-center">

    <div class="w-25 rounded border shadow">

        <EditForm Enhance
                  EditContext="editContext"
                  method="post"
                  FormName="Signup"
                  OnValidSubmit="SignUped"
                  class="bg-light rounded p-4 shadow-sm">

            <DataAnnotationsValidator />

            <div class="mb-3">
                <label class="form-label">Name</label>
                <InputText class="form-control" @bind-Value="user.Name" />
                <ValidationMessage For="@(() => user.Name)" />
            </div>

            <div class="mb-3">
                <label>Email</label>
                <InputText class="form-control" @bind-Value="user.Email" />
                <ValidationMessage For="@(() => user.Email)" />
            </div>

            <div class="mb-3">
                <label>Password</label>

                <div class="d-flex flex-row">
                    <InputText
                        type="@(IsPasswordVisible ? "password" : "text")"
                        class="form-control d-flex"
                        @bind-Value:after="OnPassChanged"
                        @bind-Value="user.Password" />

                    <button class="btn border"
                            type="button"
                            @onclick="() => IsPasswordVisible = !IsPasswordVisible">

                        <i class="bi @(IsPasswordVisible
                            ? "bi-eye-slash"
                            : "bi-eye")">
                        </i>

                    </button>
                </div>

                <ValidationMessage For="@(() => user.Password)" />
            </div>

            <div class="mb-3">
                <label>Confirm Password</label>

                <InputText class="form-control"
                           @bind-Value="user.ConfirmPassword" />

                <ValidationMessage For="@(() => user.ConfirmPassword)" />
            </div>

            <div class="mb-3">
                <label>DateOfBirth</label>

                <InputDate class="form-control"
                           @bind-Value="user.DateOfBirth" />

                <ValidationMessage For="@(() => user.DateOfBirth)" />
            </div>

            <div class="mb-3">
                <label>Address</label>

                <InputText class="form-control"
                           @bind-Value="user.Address" />

                <ValidationMessage For="@(() => user.Address)" />
            </div>

            <div class="mb-3">
                <label>PhoneNumber</label>

                <InputText class="form-control"
                           @bind-Value="user.PhoneNumber" />

                <ValidationMessage For="@(() => user.PhoneNumber)" />
            </div>

            <div class="d-flex justify-content-around flex-row">

                <button type="submit"
                        class="btn btn-primary">
                    SignUp
                </button>

                <button type="button"
                        @onclick="GoToLogin"
                        class="btn btn-link">
                    Login
                </button>

            </div>

        </EditForm>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Code Behind

The @code section contains the component's logic, API call, navigation, and validation models.

@code {
    private string errorMessage = "";

    public User user = new();

    bool IsPasswordVisible = false;

    EditContext? editContext;

    protected override void OnInitialized()
    {
        editContext = new EditContext(user);
    }

    void OnPassChanged()
    {
        if (editContext is null)
        {
            return;
        }

        var ConfirmPassWordFields =
            editContext.Field(nameof(user.ConfirmPassword));

        editContext.NotifyFieldChanged(ConfirmPassWordFields);
    }

    public async Task SignUped()
    {
        var response =
            await http.PostAsJsonAsync(
                "api/Auth/signup",
                user);

        if (response.IsSuccessStatusCode)
        {
            Console.WriteLine(
                response.Content.ToString());

            navigation.NavigateTo("/login");
        }
        else
        {
            // Get error message returned by API
            var message =
                await response.Content
                    .ReadFromJsonAsync<ErrorResponse>();

            List<string> showError =
                message.Message.Split(",").ToList();

            var stringOfError = "";

            foreach (var i in showError)
            {
                stringOfError += $"{i} \n";
            }

            await Swal.FireAsync(
                new SweetAlertOptions
                {
                    Title = stringOfError,
                    Icon = SweetAlertIcon.Error
                });
        }
    }

    void GoToLogin()
    {
        navigation.NavigateTo("login");
    }

    public class User
    {
        [Required]
        [RegularExpression(
            @"^[A-Za-z\s]+$",
            ErrorMessage =
                "Only alphabets and spaces are allowed.")]
        [MaxLength(100)]
        public string Name { get; set; }

        [Required]
        [RegularExpression(
            @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
            ErrorMessage = "Enter Valid Email")]
        [EmailAddress(
            ErrorMessage = "Invalid email address.")]
        public string Email { get; set; }

        [Required(
            ErrorMessage = "Password is required")]
        [RegularExpression(
            @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])" +
            @"[A-Za-z\d@$!%*?&]{8,}$",
            ErrorMessage = "Enter Valid Passord")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        [Required(
            ErrorMessage = "Confirm Password is required")]
        [DataType(DataType.Password)]
        [Compare(
            nameof(Password),
            ErrorMessage = "Passwords do not match")]
        public string ConfirmPassword { get; set; }

        [Required]
        [MinimumAge(18)]
        public DateOnly? DateOfBirth { get; set; }

        [Required]
        [StringLength(
            300,
            ErrorMessage =
                "Home Address cannot exceed 300 characters.")]
        public string? Address { get; set; }

        [Required]
        [RegularExpression(
            @"^\d{10}$",
            ErrorMessage =
                "Phone number must be exactly 10 digits.")]
        public string? PhoneNumber { get; set; }
    }

    public class ErrorResponse
    {
        public int Statuscode { get; set; }

        public string Message { get; set; }

        public string? Details { get; set; }

        public DateTime TimeSpent { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating a Custom Minimum Age Validation Attribute

For the Date of Birth field, I created a custom validation attribute called MinimumAgeAttribute.

This allows us to validate that the user is at least 18 years old.

Create a file named:

MinimumAgeAttribute.cs

using System.ComponentModel.DataAnnotations;

namespace Frontend.Services
{
    public class MinimumAgeAttribute : ValidationAttribute
    {
        private readonly int _minimumAge;

        public MinimumAgeAttribute(int minimumAge)
        {
            _minimumAge = minimumAge;

            ErrorMessage =
                $"You must be at least {_minimumAge} years old.";
        }

        protected override ValidationResult? IsValid(
            object? value,
            ValidationContext validationContext)
        {
            // No value - let [Required] handle null
            if (value == null)
            {
                return ValidationResult.Success;
            }

            // DateOnly? is boxed as DateOnly when it has a value
            if (value is not DateOnly dob)
            {
                return ValidationResult.Success;
            }

            var today =
                DateOnly.FromDateTime(DateTime.Today);

            // Prevent future dates
            if (dob > today)
            {
                return new ValidationResult(
                    "Date of birth cannot be in the future.",
                    new[]
                    {
                        validationContext.MemberName!
                    });
            }

            // Calculate age
            var age = today.Year - dob.Year;

            if (dob > today.AddYears(-age))
            {
                age--;
            }

            // Check minimum age
            if (age < _minimumAge)
            {
                return new ValidationResult(
                    ErrorMessage,
                    new[]
                    {
                        validationContext.MemberName!
                    });
            }

            return ValidationResult.Success;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Using the Custom Attribute

Now we can use the custom attribute on the DateOfBirth property:

[Required]
[MinimumAge(18)]
public DateOnly? DateOfBirth { get; set; }
Enter fullscreen mode Exit fullscreen mode

The 18 passed to [MinimumAge(18)] becomes the minimum age required for the user.

For example:

MinimumAge(18)
       ↓
User enters Date of Birth
       ↓
Calculate user's age
       ↓
Is age >= 18?
    ↙       ↘
  Yes        No
   ↓          ↓
 Valid      Error
Enter fullscreen mode Exit fullscreen mode

Why use a custom ValidationAttribute?

Instead of putting the age calculation directly inside the Blazor component, we keep the validation logic in a reusable class.

This makes the component cleaner and allows the same validation attribute to be reused for other models or forms.

The attribute also handles an important edge case: a future Date of Birth is rejected with a specific validation message.

Top comments (0)