# Developing Your First App with HTMX and .NET: Part III

So far, our application does not provide any feedback to the user when a new product is registered or edited, whether the operation is successful, has failed, or is in progress. HTMX provides the mechanism to implement these features in combination with Bootstrap. You can download the starting code from the previous [article](https://blog.raulnq.com/developing-your-first-app-with-htmx-and-net-part-ii).

## In-Progress Request

During an in-progress request, we will disable the `Save Changes` button and add a spinner animation. Open the `RazorComponents/Form.razor` file and make the following changes:

```xml
<form hx-post=@HtmxAttributes.Endpoint
      hx-target=@HtmxAttributes.Target
      hx-swap=@HtmxAttributes.Swap
      hx-indicator="#form-spinner"
      hx-disabled-elt="#form-button"
      hx-ext="json-enc">
    @Content
    <button id="form-button" type="submit" class="btn btn-primary">
        <span>Save Changes</span>
        <span id="form-spinner" class="spinner-border spinner-border-sm htmx-indicator" />
    </button>
</form>
@code {
    [Parameter, EditorRequired]
    public RenderFragment? Content { get; set; } = default!;
    [Parameter, EditorRequired]
    public HtmxAttributes HtmxAttributes { get; set; } = default!;
}
```

To disable the button, we use the [`hx-disabled-elt`](https://htmx.org/attributes/hx-disabled-elt/) attribute. The element specified in this attribute will have the `disabled` attribute added to it while the request is in progress. For the [spinner](https://getbootstrap.com/docs/5.3/components/spinners/), we add the Bootstrap component with the class `htmx-indicator`. The `hx-indicator` attribute lets us specify the element shown during the request. Run the application to see the results:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1717394597136/63ce0c9c-9b17-49e8-a4fb-d6872505a83d.png align="center")

## Handling Errors

Let's update the `Products/RegisterProduct.cs` and update the `HandleAction` method file as follows:

```csharp
public static async Task<RazorComponentResult> HandleAction(
	[FromServices] MyAppDbContext appDbContext,
	[FromBody] Request request)
{
	var any = await appDbContext.Set<Product>().AsNoTracking().AnyAsync(p => p.Name == request.Name);
	if (any)
	{
		throw new InvalidOperationException("The product already exists");
	}
	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();
	return await ListProducts.HandlePage(appDbContext, new ListProducts.Request(), httpContext);
}
```

We throw an exception if the product name already exists in the database. To complete the exception handling, create a `DefaultExceptionHandler.cs` file with the following content:

```csharp
using Microsoft.AspNetCore.Diagnostics;
using System.Net;

namespace MyApp;

public class DefaultExceptionHandler : IExceptionHandler
{
    private readonly IProblemDetailsService _problemDetailsService;

    public DefaultExceptionHandler(IProblemDetailsService problemDetailsService)
    {
        _problemDetailsService = problemDetailsService;
    }

    public ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        if (exception is InvalidOperationException ex)
        {
            httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            return _problemDetailsService.TryWriteAsync(new ProblemDetailsContext
            {
                HttpContext = httpContext,
                ProblemDetails =
                {
                    Title = "An error occurred",
                    Detail = ex.Message,
                    Status = (int)HttpStatusCode.BadRequest,
                    Type = "application-error",
                },
                Exception = exception
            });
        }
        httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        return _problemDetailsService.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = httpContext,
            ProblemDetails =
                {
                    Title = "An error occurred",
                    Detail = exception.Message,
                    Status = (int)HttpStatusCode.InternalServerError,
                    Type = "internal-server-error",
                },
            Exception = exception
        });
    }
}
```

The class above will handle the exceptions to return the correct response. If an `InvalidOperationException` is thrown, a `BadRequest` will be returned; otherwise, an `InternalServerError` will be returned. Update the `Program.cs` file as follows to use the exception middleware:

```csharp
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using MyApp;
using MyApp.Products;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents();
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<DefaultExceptionHandler>();
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddDbContext<MyAppDbContext>(options => options.UseSqlServer(builder.Configuration["ConnectionString"]));
var app = builder.Build();
app.MapGet("/", () =>
{
    return new RazorComponentResult<MainPage>();
});
app.UseExceptionHandler();
app.RegisterProductEndpoints();
app.Run();
```

With the current code, if a duplicate product is detected during registration, the application will get stuck without providing any feedback to the user. To solve this problem, we can use HTMX [events](https://htmx.org/events/).

> Htmx provides an extensive events system that can be used to modify and enhance behavior.

The specific event we will use is the `htmx:responseError` event. This event is triggered when an HTTP error response occurs. To show the error, we will use the [toast](https://getbootstrap.com/docs/5.3/components/toasts/) component from Bootstrap. In the `MainPage.razor` file, include the following fragment after the `main` tag:

```xml
<div class="toast-container position-fixed top-0 end-0 p-3">
	<div id=error-toast class="toast align-items-center text-bg-danger border-0">
		<div class="d-flex">
			<div class="toast-body">
			</div>
			<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
		</div>
	</div>
</div>
```

To handle the error, add the following `script` section:

```javascript
<script>
	; (function () {
		const errorToastElement = document.getElementById("error-toast")
		const errorToastBody = errorToastElement.querySelector(".toast-body")
		const errorToast = bootstrap.Toast.getOrCreateInstance(errorToastElement)
		htmx.on("htmx:responseError", (e) => {
			errorToastBody.innerText = JSON.parse(e.detail.xhr.response).detail
			errorToast.show()
		})
	})()
</script>
```

Run the application and register a duplicated product to see the results:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1717733039804/5cbd05e0-183b-4b31-8c96-b228e5f81b15.png align="center")

## Success Message

HTMX includes several useful headers in the [requests](https://htmx.org/docs/#requests). At the same time, we can include headers in the [response](https://htmx.org/docs/#requests) to change the behavior on the client side. The most useful could be [`HX-Trigger`](https://htmx.org/headers/hx-trigger/) response headers to trigger events as soon as the response is received. Navigate to the `Products/RegisterProduct.cs` file and update the content as follows:

```csharp
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)
	{
		throw new InvalidOperationException("The product already exists");
	}
	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);
}
```

With the `HX-Trigger-After-Swap` header, we will trigger the custom `successEvent` event on the client side (To understand the lifecycle of the HTMX request, check [here](https://htmx.org/docs/#request-operations).). To show the error, we will use another toast, go to `MainPage.razor` file, and include the following fragment after the `main` tag:

```xml
<div class="toast-container position-fixed top-0 end-0 p-3">
	<div id=error-toast class="toast align-items-center text-bg-danger border-0">
		<div class="d-flex">
			<div class="toast-body">
			</div>
			<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
		</div>
	</div>
</div>
```

To listen to the event, add the following script section:

```javascript
<script>
	; (function () {
		const successToastElement = document.getElementById("success-toast")
		const successToastBody = successToastElement.querySelector(".toast-body")
		const sucessToast = bootstrap.Toast.getOrCreateInstance(successToastElement)
		htmx.on("successEvent", (e) => {
			successToastBody.innerText = e.detail.value
			sucessToast.show()
		})
	})()
</script>
```

Run the application and register a new product to see the results:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1717736327986/cad1f23b-25a7-46aa-bb8d-e8f6ec952e7c.png align="center")

## Push URL to the Browser

So far, we have noticed that the URL in the browser always stays the same, which makes sense because we are just rendering parts of the HTML and not the entire page. So, how do we push a URL to the browser?

> The `hx-push-url` attribute allows you to push a URL into the browser [location history](https://developer.mozilla.org/en-US/docs/Web/API/History_API). This creates a new history entry, allowing navigation with the browser’s back and forward buttons. htmx snapshots the current DOM and saves it into its history cache, and restores from this cache on navigation.

The change is quite easy. Navigate to `RazorComponents/ActionButton.razor` file and update the HTML section as follows:

```xml
<button type="button"
        hx-get=@HtmxAttributes.Endpoint
        hx-target=@HtmxAttributes.Target
        hx-swap=@HtmxAttributes.Swap
        hx-push-url="true"
        class="@Icon btn btn-primary">
    New
</button>
```

The same change in the `RazorComponents/ActionLink.razor` file:

```xml
<a class="icon-link"
   href="#"
   hx-get=@HtmxAttributes.Endpoint
   hx-target=@HtmxAttributes.Target
   hx-push-url="true"
   hx-swap=@HtmxAttributes.Swap>
    <span class=@Icon></span>
</a>
```

For the final change, we will use the [`HX-Push-Url`](https://htmx.org/attributes/hx-push-url/) response header. Navigate to the `Products/ListProducts.cs` file and update the `HandlePage` method as follows:

```csharp
public static async Task<RazorComponentResult> HandlePage(
	[FromServices] MyAppDbContext appDbContext,
	[AsParameters] Request request,
	HttpContext httpContext)
{
	var name = request.Name ?? string.Empty;
	var results = await appDbContext.Set<Product>().AsNoTracking().Where(p => p.Name.Contains(name)).ToListAsync();
	httpContext.Response.Headers.Append("HX-Push-Url", $"/products/list");
	return new RazorComponentResult<ListProductsPage>(new { Results = results });
}
```

Run the application to see the browser's URL change according to our navigation. You can find the final code [here](https://github.com/raulnq/htmx-dotnet-app/tree/part-iii). Thank you, and happy coding.
