Skip to content

Form validation

Hydro components provide model validation capabilities similar to traditional ASP.NET Core MVC models. Use Data Annotations to define validation rules in your components.

csharp
// ProductForm.cshtml.cs

public class ProductForm : HydroComponent
{
    [Required, MaxLength(50)]
    public string Name { get; set; }
    
    public async Task Submit()
    {
        if (!Validate())
        {
            return;
        }
        
        // your submit logic
    }
}
// ProductForm.cshtml.cs

public class ProductForm : HydroComponent
{
    [Required, MaxLength(50)]
    public string Name { get; set; }
    
    public async Task Submit()
    {
        if (!Validate())
        {
            return;
        }
        
        // your submit logic
    }
}
razor
<!-- ProductForm.cshtml -->

@model ProductForm

<form on:submit="@(() => Model.Submit())">
  <label asp-for="Name"></label>
  <input asp-for="Name" bind />
  <span asp-validation-for="Name"></span>  

  <button type="submit">Submit</button>
</form>
<!-- ProductForm.cshtml -->

@model ProductForm

<form on:submit="@(() => Model.Submit())">
  <label asp-for="Name"></label>
  <input asp-for="Name" bind />
  <span asp-validation-for="Name"></span>  

  <button type="submit">Submit</button>
</form>

Validating nested models and collections

Data Annotations don't offer a way to validate nested models or collections, that's why Hydro provides custom validators to handle these scenarios.

Nested model:

csharp
// ProductForm.cshtml.cs

public class ProductForm : HydroComponent
{
    [ValidateObject]
    public ProductData Product { get; set; }
    
    // ...
}

public class ProductData
{
    [Required, MaxLength(50)]
    public string Name { get; set; }
    
    [Range(0, 100)]
    public decimal Price { get; set; }
}
// ProductForm.cshtml.cs

public class ProductForm : HydroComponent
{
    [ValidateObject]
    public ProductData Product { get; set; }
    
    // ...
}

public class ProductData
{
    [Required, MaxLength(50)]
    public string Name { get; set; }
    
    [Range(0, 100)]
    public decimal Price { get; set; }
}

Collection:

csharp
// ProductForm.cshtml.cs

public class InvoiceForm : HydroComponent
{
    [ValidateCollection]
    public List<LineData> Lines { get; set; }
    
    // ...
}

public class LineData
{
    [Required, MaxLength(50)]
    public string Name { get; set; }
    
    [Range(0, 100)]
    public decimal Price { get; set; }
}
// ProductForm.cshtml.cs

public class InvoiceForm : HydroComponent
{
    [ValidateCollection]
    public List<LineData> Lines { get; set; }
    
    // ...
}

public class LineData
{
    [Required, MaxLength(50)]
    public string Name { get; set; }
    
    [Range(0, 100)]
    public decimal Price { get; set; }
}

Custom validation

It's possible to execute also custom validation. For example:

csharp
// Counter.cshtml.cs

public class Counter : HydroComponent
{
    public int Count { get; set; }
    
    public void Add()
    {
        if (Count > 5)
        {
            ModelState.AddModelError(nameof(Count), "Value is too high");
            return;
        }
        
        Count++;
    }
}
// Counter.cshtml.cs

public class Counter : HydroComponent
{
    public int Count { get; set; }
    
    public void Add()
    {
        if (Count > 5)
        {
            ModelState.AddModelError(nameof(Count), "Value is too high");
            return;
        }
        
        Count++;
    }
}

Attaching Fluent Validation:

csharp
// Counter.cshtml.cs

public class Counter(IValidator<Counter> validator) : HydroComponent
{
    public int Count { get; set; }
    
    public void Add()
    {
        if (!this.Validate(validator))
        {
            return;
        }
        
        Count++;
    }
    
    public class Validator : AbstractValidator<Counter>
    {
        public Validator()
        {
            RuleFor(c => c.Count).LessThan(5);
        }
    }
}

// HydroValidationExtensions.cs

public static class HydroValidationExtensions
{
    public static bool Validate<TComponent>(this TComponent component, IValidator<TComponent> validator) where TComponent : HydroComponent
    {
        component.IsModelTouched = true;
        var result = validator.Validate(component);

        foreach (var error in result.Errors) 
        {
            component.ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
        }

        return result.IsValid;
    }
}
// Counter.cshtml.cs

public class Counter(IValidator<Counter> validator) : HydroComponent
{
    public int Count { get; set; }
    
    public void Add()
    {
        if (!this.Validate(validator))
        {
            return;
        }
        
        Count++;
    }
    
    public class Validator : AbstractValidator<Counter>
    {
        public Validator()
        {
            RuleFor(c => c.Count).LessThan(5);
        }
    }
}

// HydroValidationExtensions.cs

public static class HydroValidationExtensions
{
    public static bool Validate<TComponent>(this TComponent component, IValidator<TComponent> validator) where TComponent : HydroComponent
    {
        component.IsModelTouched = true;
        var result = validator.Validate(component);

        foreach (var error in result.Errors) 
        {
            component.ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
        }

        return result.IsValid;
    }
}