Developing Your First App with HTMX and .NET: Part VII

Developing Your First App with HTMX and .NET: Part VII

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.