So far, when an exception occurs in the application, a toast notification displays the details. Taking advantage of this mechanism, we throw exceptions when certain business rules are unmet.
While this approach is practical, we can enhance it by showing the message directly within the form. The starting code can be found here. Let's start by updating the RazorComponents/TextInput.razor
as follows:
<div class="form-group">
<label for=@Property class="form-label">@Label</label>
<input type="text"
class="form-control @(!string.IsNullOrEmpty(ErrorMessage)?"is-invalid":"")"
id=@Property
name=@Property
@attributes="Attributes" />
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<div id=@($"{Property}-error-message") class="invalid-feedback">
@ErrorMessage
</div>
}
</div>
@code {
[Parameter, EditorRequired]
public string Property { get; set; } = default!;
[Parameter, EditorRequired]
public string Label { get; set; } = default!;
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? Attributes { get; set; }
[Parameter]
public string ErrorMessage { get; set; } = default!;
}
By setting the ErrorMessage
property, a message will be displayed along with the input. Create a Products/ProductsForm.razor
file with the following content:
@using MyApp.RazorComponents;
<div id="product-form">
<div class="row mb-4">
<div class="col-4">
@if (InEdition)
{
<TextInput Property="Name"
Label="Name"
placeholder="Enter name"
maxlength=100
value=@Name
disabled
readonly />
}
else
{
<TextInput Property="Name"
Label="Name"
placeholder="Enter name"
maxlength=100
value=@Name
ErrorMessage=@NameErrorMessage
required />
}
</div>
<div class="col-4">
<NumericInput Property="Price"
Label="Price"
Prefix="$"
step="0.01"
min="0.01"
value=@Price
required />
</div>
<div class="col-4">
<SelectInput Property="Unit"
Label="Unit"
Value=@Unit
required
Source=@(new Dictionary<string, string>(){{"UN","Unit"},{"KG","Kilogram"}}) />
</div>
</div>
<div class="row mb-4">
<div class="col">
<TextAreaInput Property="Description"
Label="Description"
rows="5"
value=@Description
placeholder="Enter description"
maxlength=500 />
</div>
</div>
</div>
@code {
[Parameter]
public string Name { get; set; } = default!;
[Parameter]
public string NameErrorMessage { get; set; } = default!;
[Parameter]
public decimal Price { get; set; } = 0;
[Parameter]
public string Unit { get; set; } = default!;
[Parameter]
public string Description { get; set; } = default!;
[Parameter]
public bool InEdition { get; set; } = false;
}
This file is the form content of the Products/RegisterProductPage.razor
and Products/EditProductPage.razor
files. Update the Products/RegisterProductPage.razor
file to use the new component:
@using MyApp.RazorComponents;
<h4>Register Product</h4>
<Breadcrumbs Links=@(new []{
new Breadcrumbs.Link("List products", new HtmxAttributes("/products/list", "#main", "innerHTML")),
new Breadcrumbs.Link("Register product")}) />
<Section>
<Content>
<Form HtmxAttributes=@(new HtmxAttributes("/products/register","#main", "innerHTML"))>
<Content>
<ProductForm />
</Content>
</Form>
</Content>
</Section>
@code {
}
And the Products/EditProductPage.razor
file:
@using MyApp.RazorComponents;
<h4>Edit Product</h4>
<Breadcrumbs Links=@(new []{
new Breadcrumbs.Link("List products", new HtmxAttributes("/products/list", "#main", "innerHTML")),
new Breadcrumbs.Link("Edit product")})>
<MenuItems>
<MenuItem Text="Enable"
IsDisabled=@(Product.IsEnabled)
HtmxAttributes=@(new HtmxAttributes($"/products/{Product.ProductId}/enable", "#main", "innerHTML"){Confirm="Are you sure?"}) />
<MenuItem Text="Disable"
IsDisabled=@(!Product.IsEnabled)
Confirm="Are you sure?"
HtmxAttributes=@(new HtmxAttributes($"/products/{Product.ProductId}/disable", "#main", "innerHTML")) />
<ShowModal Text="Delete"
HtmxAttributes=@(new HtmxAttributes($"/products/{Product.ProductId}/delete", "#modal-dialog", "innerHTML")) />
</MenuItems>
</Breadcrumbs>
<nav class="navbar hstack gap-3 justify-content-end">
<StatusBadge IsEnabled=@Product.IsEnabled />
</nav>
<Section>
<Content>
<Form HtmxAttributes=@(new HtmxAttributes($"/products/{Product.ProductId}/edit","#main", "innerHTML"))>
<Content>
<ProductForm InEdition=true
Name=@Product.Name
Price=@Product.Price
Unit=@Product.Unit.ToString()
Description=@Product.Description />
</Content>
</Form>
</Content>
</Section>
@code {
[Parameter, EditorRequired]
public Product Product { get; set; } = default!;
}
At this point, if we run the application, it will behave the same as it did before all the changes. Update the HandleAction
method in the Products/RegisterProduct.cs
file as follows:
public static async Task<RazorComponentResult> HandleAction(
[FromServices] MyAppDbContext appDbContext,
[FromBody] Request request,
HttpContext httpContext)
{
var any = await appDbContext.Set<Product>().AsNoTracking().AnyAsync(p => p.Name == request.Name);
if (any)
{
httpContext.Response.Headers.Append("HX-Reswap", $"outerHTML");
httpContext.Response.Headers.Append("HX-Retarget", $"#product-form");
return new RazorComponentResult<ProductForm>(new
{
Name = request.Name,
NameErrorMessage = "The product already exists",
Price = request.Price,
Unit = request.Unit.ToString(),
Description = request.Description
});
}
var product = new Product()
{
ProductId = Guid.NewGuid(),
Name = request.Name,
Description = request.Description,
Price = request.Price,
Unit = request.Unit,
IsEnabled = false
};
appDbContext.Set<Product>().Add(product);
await appDbContext.SaveChangesAsync();
httpContext.Response.Headers.Append("HX-Trigger-After-Swap", @$"{{""successEvent"":""The product was register successfully""}}");
return await ListProducts.HandlePage(appDbContext, new ListProducts.Request(), httpContext);
}
The original exception throwing is replaced with a response that contains the HTML produced by our new component. To override the original hx-swap
and hx-target
attributes in the form, we use the HX-Reswap
and HX-Retarget
response headers. Run the application and try to create a duplicate product:
You can find the final code here. Thank you, and happy coding.