Exploring htmx with Razor Pages: An Old New Web Development Approach?
Tags: dotnet, aspnet, htmx
Published: 2023-08-29 22:10:38

In the realm of web development, it's crucial to stay updated with the latest tools and methodologies. One such tool creating a buzz is **htmx**. I am usually not swayed by the 'shiny object syndrome', but I worked a lot with **Razor Pages** recently, and touched a lot of jQuery, which started to annoy me a bit.

However, after watching a [video from @t3dotgg](https://youtu.be/NA5Fcgs_viU?si=0B0Ws7TMAZVveFCM) I was convinced that **htmx** might be helpful here and that it could fit me nicely. So, I decided to go through some [use cases](https://htmx.org/examples/) for it and do some tests to check if it is any good.

> 💡 The results of these tests are available in [the repo](https://github.com/xakpc/RazorHtmx)</a>. 
> The demo application is hosted on Azure free plan and is available [here](https://razorhtmx.azurewebsites.net/). 
> To get maximum from this article you need to be familiar with ASP.NET MVC or RazorPages

## 1. Introduction

First, let me briefly introduce what **htmx** and **Razor Pages** are:

**htmx** is a js library that gives access to AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML, using attributes. The library is built around "Hypermedia-Driven Application Architecture," which, in simple terms, could be described as "return HTML instead of JSON."

**Razor Pages** extends ASP.NET Core MVC, making coding page-focused scenarios easier and more productive than controllers and views. **Razor Pages** group the action (called a *handler*) and the viewmodel (called a *PageModel*) in one class and link it to a view. It uses a routing convention based on location and name in a Pages folder. This tends to keep **Razor Pages** and its handlers smaller and more focused while at the same time making it easier to find and work with.

I also want to mention [**pico.css**](https://picocss.com/)**.** This is a minimal CSS framework for semantic HTML. It focuses on using simple native HTML tags as much as possible and provides consistent adaptive spacings and typography on all devices.

## 2. Background

Before I dive into integrating **htmx** and **Razor Pages**, let me briefly describe each technology. Both are praised for simplifying web development, but what are they?

### 2.1. htmx

**htmx** started as an `intercooler.js` - a small helper library to make AJAX calls through HTML tag attributes.

```xml
  <!-- This anchor tag posts to '/click' when it is clicked -->
  <a ic-post-to="/click">
    Click Me!
  </a>
```

<details data-node-type="hn-details-summary"><summary>Asynchronous JavaScript And XML (AJAX)</summary><div data-type="detailsContent">To retrieve data from a web server, AJAX utilizes a blend of a browser's built-in XMLHttpRequest object and JavaScript and HTML DOM to display or utilize the data. However, newer browsers can use the Fetch API instead of the XMLHttpRequest Object. XML in the name is obsolete and misleading, in fact, any data could be transported: from JSON to HTML.</div></details>

Around three years ago, intercooler 2.0 (renamed to **htmx 1.0**) has been released. It was smaller, more expressive, and no longer depended on jQuery.

```xml
  <!-- have a button POST a click via AJAX -->
  <button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
  </button>
```

**htmx** intend to help web developers to create dynamic, interactive web content without the overhead of complex JavaScript frameworks.

Leveraging HTML attributes allows seamless partial page updates, delivering the power of **AJAX**, **CSS Transitions**, **WebSockets**, and **Server-Sent Events**.

I would not talk about WebSockets and SSE or try them, though, because:

* They have experimental support
* If I want WebSockets and SSE, I probably would use Blazor and SignalR
    

#### 2.1.1. AJAX

The core of **htmx** is a set of attributes that allow you to issue AJAX requests directly from HTML:

| Attribute | Description |
| --- | --- |
| `hx-get` | Issues a `GET` request to the given URL |
| `hx-post` | Issues a `POST` request to the given URL |
| `hx-put` | Issues a `PUT` request to the given URL |
| `hx-patch` | Issues a `PATCH` request to the given URL |
| `hx-delete` | Issues a `DELETE` request to the given URL |

Each of these attributes takes a URL to issue an AJAX request to. The element will issue a request of the specified type to the given URL when the element is triggered:

```html
<div hx-put="/messages">
    Put To Messages
</div>
```

This tells the browser:

> When a user clicks on this div, issue a PUT request to the URL /messages and load the response into the div

By default, AJAX requests are triggered by the “natural” event of an element:

* `input`, `textarea` & `select` are triggered on the `change` event
* `form` is triggered on the `submit` event
* everything else is triggered by the `click` event

The `hx-trigger` attribute could change this to specify which event will cause the request. Here is a `div` that posts to `/mouse_entered` when a mouse enters it:

```html
<div hx-post="/mouse_entered" hx-trigger="mouseenter">
    [Here Mouse, Mouse!]
</div>
```

#### 2.1.2. CSS Transitions

**htmx** makes it easy to use CSS Transitions without javascript. Imagine that we need to replace content by **htmx** via an AJAX request with this new content:

```xml
<div id="div1">Original Content</div>
<!-- replaced -->
<div id="div1" class="red">New Content</div>
```

Note two things:

* The div has the *same* `id` in the original and the new content
    
* The `red` class has been added to the new content
    

Given this situation, we can write a CSS transition from the old state to the new state:

```css
.red {
    color: red;
    transition: all ease-in 1s ;
}
```

When **htmx** swaps in this new content, it will do so in such a way that the CSS transition will apply to the new content, giving you a smooth transition to the new state. It happened because we keep its `id` stable across requests.

#### 2.1.3. The philosophy behind "Hypermedia-Driven Applications"

I would not delve into details here, but it is worth mentioning because it's a pretty interesting concept. They wrote a [book](https://hypermedia.systems/) on it, read it sometime.

In short, it's all about the central role of HTML (or hypermedia) in driving and representing the state of web applications. Instead of relying heavily on intricate client-side scripting or full-page reloads to reflect changes in application state, this principle endorses the use of hypermedia itself.

The application state evolves fluidly by allowing individual page pieces to update in response to user actions or events, reducing the need for heavy JavaScript while promoting a more resilient and accessible web experience.

We did this with early ASP.NET MVC applications, utilizing jQuery to render partials, and now the proposal is to revert to this method for the sake of simplicity.


![](https://htmx.org/img/memes/original.png)

### 2.2. Razor Pages

**Razor Pages**, introduced in ASP.NET Core 2.0, offer a streamlined alternative to the MVC UI pattern, which Microsoft has supported since 2009.

While MVC promotes separation of concerns, it often creates a sprawling structure of controllers, views, and viewmodels across multiple folders. In contrast, **Razor Pages** encapsulate the action (referred to as a *handler*) and the viewmodel (now the *PageModel*) in a single class, directly tied to a corresponding Razor Page.

All these pages reside in a central `Pages` folder, following a naming-based routing convention. Handlers, prefixed with HTTP verbs like `OnGet`, simplify actions, typically returning the associated page by default. This structure enables concise, focused pages, making an application's navigation and updates more intuitive.

**Razor Pages** overall a vast topic, but I want to point out two essential for the htmx parts of it: **handlers** and **partials**

#### 2.2.1. Razor Pages Handlers

**Handler methods** are the public methods in **Razor Pages** `PageModel` and they are automatically executed on a request, implicitly returning a result for the current page.

The **Razor Pages** framework uses a naming convention to select the appropriate handler method to execute. The default convention works by matching the HTTP method used for the request to the name of the method, which is prefixed with "On": `OnGet(), OnPost(), OnPut()` or async versions `OnPostAsync(), OnGetAsync()`, etc.

Handler methods can return `void`, `Task` if asynchronous, or an `IActionResult` (or `Task<IActionResult>`).

> _IActionResult_: The IActionResult return type is appropriate when multiple ActionResult return types are possible in an action. The ActionResult types represent various HTTP status codes. Any non-abstract class deriving from ActionResult qualifies as a valid return type. Some common return types in this category are BadRequestResult (400), NotFoundResult (404), and OkObjectResult (200).

```csharp
public class ValidationModel : PageModel
{
    public void OnGet()
    {
    }

    public IActionResult OnPost()
    {
        return Page();
    }
}
```

**Razor Pages** include a feature called "**named handler methods**". This feature enables you to specify multiple methods that can be executed for a single verb.

```csharp
	public IActionResult OnGetItem()
    {
        return Partial("_ItemPartial");
    }
		
    public IActionResult OnPostItem()
    {
        return Partial("_ItemPartial");
    }
```

We could use tag helpers to generate links like `/validation?handler=Item` , where the handler would be set as a query parameter. There is also an option to generate path links like `/validation/Item` by setting a page route `@page "{handler?}"`

> _Tag Helpers_: Tag Helpers in Razor files allow server-side code to create and render HTML elements. They include built-in helpers for common tasks like creating forms, links, and loading assets. Custom Tag Helpers can also be created in C# to target elements by name, attribute, or parent tag.

```xml
<!-- Razor code -->
<article>
    <a asp-page="Validation" asp-page-handler="Item">Link</a>
</article>

<!-- HTML code -->
<article>
    <a href="/validation?handler=Item">Link</a>
</article>
```

Unfortunately, these Tag Helpers exist only for `<a>` and `<form>` tags. One of the questions **htmx** trying to solve is `Why should only <a> and <form> be able to make HTTP requests?` so we could call handlers from any tag. MVC provides a way to generate a handler with a helper function: `@Url.Page("BulkUpdate", "Activate")` - would generate `/BulkUpdate?handler=Activate` .

But in **htmx** context, handlers usually belong to the same page, so to make it easier, I wrote a simple URL helper method:

```csharp
public static class UrlHandlerExtensions
{
    public static string Handler(this IUrlHelper urlHelper, string handler, object? values = null)
    {
        // Convert the values object to a dictionary
        var routeValues = new RouteValueDictionary(values)
        {
            ["handler"] = handler // Add the handler to the dictionary
        };

        var url = urlHelper.RouteUrl(new UrlRouteContext
        {
            Values = routeValues
        });

        return url;
    }
}
```

It could be used with any HTML tag to set the handler URL to a htmx attribute:

```csharp
<button class="outline" hx-delete="@Url.Handler("Item", new { id = Model.Id })" antiforgery="true">
    Delete
</button>
```

#### 2.2.2. Razor Pages Partials

**Partial Views** in **Razor Pages** contain reusable HTML and code snippets that simplify complex pages by breaking them into smaller units.

Partial Views in ASP.NET MVC were introduced with the initial release of ASP.NET MVC itself. ASP.NET MVC 1.0 was officially released in March 2009, and from that first version, developers could use partial views to encapsulate and reuse parts of their views.

Partial Views are set up as *cshtml* files that do not take part in routing, so they do not have `@page` directive. Usually, partials are used to reuse some code on razor pages with the use of `partial` tag helper.

```csharp
<partial name="_ItemPartial" model="Model.UserInfo" />
```

On top of that `PageModel` provides a set of methods that implement `IActionResult` , one of them: `Partial` - would apply the model, render a partial HTML view to the response, and return it as a response body.

| Method | Description |
|--|--|
| `Page()` | Creates a [PageResult](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pageresult?view=aspnetcore-7.0) object that renders the page. |
| `Partial(String)` | Creates a [PartialViewResult](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.partialviewresult?view=aspnetcore-7.0) by specifying the name of a partial to render. |
| `Partial(String, Object)` | Creates a [PartialViewResult](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.partialviewresult?view=aspnetcore-7.0) by specifying the name of a partial to render and the model object. |

That gives us the ability to define razor partial view

```xml
@model Xakpc.RazorHtmx.Data.UserInfoViewModel

<tr>
    <td>@Model.FirstName</td>
    <td>@Model.Email</td>
    <td>
        <button class="outline" hx-get="@Url.Handler("EditItem", new { id = Model.Id })">
            Edit
        </button>
    </td>
</tr>
```

and return it as generated HTML in a response

```csharp
    public IActionResult OnGetItem(int id)
    {
        var item = TestData.Users.First(x => x.Id == id);

        return Partial("_TableRow", item);
    }
```

```xml
<tr>
    <td>Krystal</td>
    <td>krystal.heaney@bergnaummetz.us</td>
    <td>
        <button class="outline" hx-get="/edit-row?id=2&amp;handler=EditItem">
            Edit
        </button>
    </td>
</tr>
```

And we could also re-use the exact partial for the initial page generation

```xml
        <tbody hx-target="closest tr" hx-swap="outerHTML">
        @foreach (var userInfo in Model.Users)
        {
            <partial name="_TableRow" model="userInfo"/>
        }
        </tbody>
```

In my opinion this feature is a single reason why **Razor Pages** could even work with **htmx**.

## 3. Setting the Stage: Prerequisites and Initial Setup

To start, you don't need much - an installed [dotnet core SDK](https://dotnet.microsoft.com/en-us/download) and a favorite editor. I use Visual Studio because it's been my default editor since version VS 2013. But VS Code, or VS on Mac, or anything else, is fine. Check [official tutorial](https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start?view=aspnetcore-7.0&tabs=visual-studio-code) if you feel stuck.

Run the following command to create a new **Razor Pages** project:

```powershell
dotnet new webapp -o RazorHtmx
```

It would create a default project with Bootstrap and jQuery applied by default. I prefer to use pico.css in my projects, so I have a reduced version (you can check it [here](https://github.com/xakpc/RazorHtmx/releases/tag/boilerplate)), but unfortunately, it's not a template yet.

### 3.1. Integrating htmx into Razor Pages

To integrate **htmx**, you should include the js file into `_Layout`. Choose whatever option you prefer from [docs#installing](https://htmx.org/docs/#installing). I downloaded and included `htmx.min.js` to a head tag of `_Layout.cshtml`

> __Layout.cshtml_: Web apps use a common layout for consistency. This includes elements like headers, navigation, and footers. Layout files reduce duplicate code and the default layout for ASP.NET Core is named _Layout.cshtml.

```xml
<head>
    <!-- rest of the code -->
    <script src="~/js/htmx.min.js" asp-append-version="true"></script>
</head>
```

## 4. Making First htmx Call in Razor

As I mentioned earlier, my aim was to explore each example from the **htmx** examples page to understand what is possible and how that could be helpful. The first example is click-to-edit, which is my perfect use case.

> The click-to-edit pattern provides a way to offer inline editing of all or part of a record without a page refresh.

The example ended with several files

* `_Partial.cshtml` to present record
* `_Edit.cshtml` to present form for editing
* `ClickToEdit.cshtml` to present the page
* `ClickToEdit.cshtml.cs` for PageModel
* `UserInfoViewModel.cs` for model

Let me describe each of them.

**UserInfoViewModel.cs**

It's a pretty generic model class. Each property has data annotation to render labels in forms.

```csharp
    public class UserInfoViewModel
    {
        public int Id { get; set; }

        [Display(Name = "First Name")]
        public string FirstName { get; set; }

        [Display(Name = "Last Name")]
        public string LastName { get; set; }

        [Display(Name = "Email")]
        public string Email { get; set; }

        [Display(Name = "Status")]
        public string? Status { get; set; }

        public Guid RowId { get; set; } = Guid.Empty;
    }
```

**ClickToEdit.cshtml.cs**

`PageModel` for this page consists of all required handler methods, basic `OnGet` for initial loading, a couple named `GET` handlers to show the edit form and view form, and `PUT` method to update form data.

```csharp
public class ClickToEditModel : PageModel
{
    public UserInfoViewModel UserInfoViewModel { get; set; }

    public void OnGet()
    {
        UserInfoViewModel = GetUserInfo(1);
    }

    public IActionResult OnGetPartial(int id)
    {
        UserInfoViewModel = GetUserInfo(id);
        return Partial("_Partial", UserInfoViewModel);
    }

    public IActionResult OnGetEdit(int id)
    {
        UserInfoViewModel = GetUserInfo(id);
        return Partial("_Edit", UserInfoViewModel);
    }

    public IActionResult OnPutEdit(UserInfoViewModel userInfoViewModel)
    {
        UserInfoViewModel = userInfoViewModel;
        return Partial("_Partial", UserInfoViewModel);
    }
}
```

**ClickToEdit.cshtml**

The page itself is nothing other than a placeholder for partials. Instead of using `<partial>` tag helper, I'm using `Html.PartialAsync` helper method, but the outcome would be the same.

```xml
@page "/click-to-edit"
@model ClickToEditModel

<article>
    @await Html.PartialAsync("_Partial", Model.UserInfoViewModel)
</article>
```

Now, about partials. We have two of them - one renders a form, and another renders a view. Note that none of the HTML tags have classes because I use the semantic HTML CSS library pico.css. This approach significantly reduces the size of the partial. Consider the bulk that would be added if you were to use a class loaded with Tailwind CSS attributes.

First, **_Partial.cshtml**

```xml
@model Xakpc.RazorHtmx.Data.UserInfoViewModel

<div hx-target="this" hx-swap="outerHTML">
    <p>
        <strong><label asp-for="@Model.FirstName"></label></strong>
        @Model.FirstName
    </p>
    <p>
        <strong><label asp-for="@Model.LastName"></label></strong>
        @Model.LastName
    </p>
    <p>
        <strong><label asp-for="@Model.Email"></label></strong>
        @Model.Email
    </p>
    <button hx-get="@Url.Page("ClickToEdit", "Edit", new { id = Model.Id })">
        Click To Edit
    </button>
</div>
```

* `hx-target="this"` makes a div that updates itself when changed. It's also inherited, so it works when placed on the parent element.
    
* `hx-swap="outerHTML"` instructs to replace the entire target element with the response.
    
* `hx-get="@Url.Page("ClickToEdit", "Edit", new { id = Model.Id })"` would generate a URL to `ClickToEdit.OnGetEdit` handler method and would pass `id` query parameter.
    

```xml
<button hx-get="/click-to-edit?id=1&handler=Edit">
        Click To Edit
</button>
```

**_Edit.cshtml**

```xml
@model Xakpc.RazorHtmx.Data.UserInfoViewModel

<form hx-put="@Url.Page("ClickToEdit", "Edit")"
      hx-target="this" hx-swap="outerHTML">

    @Html.AntiForgeryToken()
    <input asp-for="Id" type="hidden"/>

    <label asp-for="@Model.FirstName"></label>
    <input asp-for="@Model.FirstName" type="text" required/>

    <label asp-for="@Model.LastName"></label>
    <input asp-for="@Model.LastName" type="text" required/>

    <label asp-for="@Model.Email"></label>
    <input asp-for="@Model.Email" type="email" required/>

    <div class="grid">
        <button>Submit</button>
        <button hx-get="@Url.Page("ClickToEdit", "Partial", new { id = Model.Id })" class="secondary">Cancel</button>
    </div>
</form>
```

* `@Html.AntiForgeryToken()` will generate a hidden field with an anti-forgery token to prevent CSRF vulnerabilities. More on this later.
    
* `hx-put="@Url.Page("ClickToEdit", "Edit")"` will perform `PUT` a request to `OnPutEdit` handler because updates should be `PUT` according to RESTful best practices.
    
* `hx-target="this"` makes a div that updates itself when changed. It's also inherited and works when placed on the parent element.
    
* `hx-swap="outerHTML"` instructs to replace the entire target element with the response.
    
* `hx-get="@Url.Page("ClickToEdit", "Partial", new { id = Model.Id })"` will perform `GET` request to `OnGetPartial` handler and set query parameter `id` to model id. This query would return rendered HTML of the record view:
    

```xml
    <div hx-target="this" hx-swap="outerHTML">
        <p>
            <strong><label for="FirstName">First Name</label></strong>
            Kennedy
        </p>
        <p>
            <strong><label for="LastName">Last Name</label></strong>
            Heaney
        </p>
        <p>
            <strong><label for="Email">Email</label></strong>
            kennedy_heaney@botsford.uk
        </p>
        <button hx-get="/click-to-edit?id=1&amp;handler=Edit">
            Click To Edit
        </button>
    </div>
```

As a result, a fully functional click-to-edit form in **Razor Pages** and **htmx** with 100-120 lines of code.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693338867659/4eae9486-ee6b-4fc8-aa95-6d7acdda16b0.gif)

## 5. Exploring Other htmx Use-Cases

I surely could not even try to describe every feature **htmx** has. I selected a couple of examples that might be useful references for any web application.

### 5.1. Progress Bar

The progress bar is a very simple partial, just two lines. Having it as a separate file is almost a crime, but unfortunately, it's how it is.

```xml
@model int

<progress id="progress" value="@Model" max="100"></progress>
```

But I want to show you here is a **_ProgresPartial,** which hosts a progress bar.

Based on the progress model (is progress done or not), we could alter how this partial is rendered and what `hx-trigger` value is set. This is a primary function of Razor markup syntax.

Therefore, we could decide on the application's state and what should be returned as a view on the backend, which is very convenient.

```xml
@model Progress

<div hx-trigger="done" hx-get="@Url.Handler("Progress")" hx-swap="outerHTML" hx-target="this">
    @if (Model.Done)
    {
        <h3 role="status" tabindex="-1" autofocus>Complete</h3>
    }
    else
    {
        <h3 role="status" tabindex="-1" autofocus>Running</h3>
    }

    <div
        hx-get="@Url.Handler("Progress")"
        hx-trigger="@(Model.Done ? "none" : "every 600ms")"
        hx-target="this"
        hx-swap="innerHTML">
        <partial name="_ProgressBarPartial" model="@Model.Value"/>
    </div>

    @if (Model.Done)
    {
        <button hx-post="@Url.Handler("Start")" antiforgery="true" classes="add show:600ms">
            Restart Job
        </button>
    }
</div>
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693338966920/c3935bda-0082-40b8-8345-5da664df2475.gif)

### 5.2. Table Scroll, Edit, Delete

Building interactive tables is a common task of any web application. With the help of **htmx**, we could easily expand a static table with scroll loading in place of edit and deletion.

I implemented several tests related to tables:

* Infinite Scroll
* Table Row Edit
* Table Row Delete

For **infinite scroll**, we have a partial in the `tbody`

```xml
        <tbody id="tbody">
            @await Html.PartialAsync("_TableBody", Model.UsersTable)
        </tbody>
```

The partial would draw table rows, and the last row would add `hx-get` attribute with `revelaed` trigger.

```xml
@foreach (var userInfo in Model.Users)
{
    @if (userInfo == Model.Users.Last())
    {
        <tr hx-get="@Url.Page("InfiniteScroll", "Page", new { page = Model.Page + 1 })"
            hx-trigger="revealed"
            hx-swap="afterend"
            hx-indicator="#ind">
            <td>@userInfo.FirstName @userInfo.LastName</td>
            <td>@userInfo.Email</td>
            <td>@userInfo.RowId.ToString("N").ToUpperInvariant()</td>
        </tr>
    }
    else
    {
        <tr>
            <td>@userInfo.FirstName @userInfo.LastName</td>
            <td>@userInfo.Email</td>
            <td>@userInfo.RowId.ToString("N").ToUpperInvariant()</td>
        </tr>
    }
}
```

The handler method load and render the next page of rows. There is also `Task.Delay` to simulate some loading.

```csharp
    public async Task<IActionResult> OnGetPageAsync(int page)
    {
        await Task.Delay(1000);

        var data = TestData.Users.GetRange(page * PageSize, PageSize);

        return Partial("_TableBody", data);
    }
```

Here is how it works:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693339110448/db6e0f8d-87f2-41c9-a8de-2876e2e56c15.gif)

For **table editing**, I have two partials, for presenting and for editing, which are swapped on the button click.

```xml
<tr>
    <td>@Model.FirstName</td>
    <td>@Model.Email</td>
    <td>
        <button class="outline" hx-get="@Url.Handler("EditItem", new { id = Model.Id })">
            Edit
        </button>
    </td>
</tr>
```

```xml
<tr>
    <td>
        <input type="text" asp-for="@Model.FirstName"/>
    </td>
    <td>
        <input type="email" asp-for="@Model.Email"/>
    </td>
    <td>
        <button hx-get="@Url.Handler("Item", new { id = Model.Id })" class="secondary" style="margin-bottom: 6px;">
            Cancel
        </button>
        <button hx-put="@Url.Handler("Item", new { id = Model.Id })" hx-include="closest tr" antiforgery="true">
            Save
        </button>
    </td>
</tr>
```

Handlers for this page are straightforward: get item, get item form, update (put) item

```csharp
    public IActionResult OnGetItem(int id)
    {
        var item = TestData.Users.First(x => x.Id == id);

        return Partial("_TableRow", item);
    }

    public IActionResult OnGetEditItem(int id)
    {
        var item = TestData.Users.First(x => x.Id == id);

        return Partial("_TableRowForm", item);
    }

    public IActionResult OnPutItem(UserInfoViewModel user)
    {
        var item = TestData.Users.First(x => x.Id == user.Id);
        item.FirstName = user.FirstName;
        item.Email = user.Email;

        return Partial("_TableRow", item);
    }
```

And now we can easily update table rows

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693339214699/639ed542-baeb-43cd-b773-cff738696974.gif)

Delete Row has a similar table row partial, but with `hx-delete` handler, because REST.

```csharp
<tr>
    <td>@Model.FirstName @Model.LastName</td>
    <td>@Model.Email</td>
    <td>@Model.Status</td>
    <td>
        <button class="outline" hx-delete="@Url.Handler("Item", new { id = Model.Id })" antiforgery="true">
            Delete
        </button>
    </td>
</tr>
```

On the delete call, the handler would return 200 OK with an empty body instructing **htmx** to remove the row. We also use `hx-confirm` tag to confirm deletion

```csharp
        <tbody hx-confirm="Are you sure?" hx-target="closest tr" hx-swap="outerHTML swap:1s">
        @foreach (var userInfo in Model.Users)
        {
            @await Html.PartialAsync("_TableRow", userInfo)
        }
        </tbody>
```

There is also some transition animation

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693339371099/f397a7b0-15ab-4130-b1c2-7ec891c7cbf4.gif)

Let's look at a couple of practical scenarios where **htmx** and **Razor Pages** integration shines.

### 5.3. Dialogs and tabs

A full code listing of how HTML5 dialogs could be created with **htmx** and **Razor Pages**. In this example, I used some JavaScript to show and hide the dialog.

At the same time, when dialog is shown, **htmx** would issue a get request to get data from the page handler and set the result as dialog content.

Actual dialog content would be generated on the backend from Razor partial.

```xml
<!-- DialogsHtml.cshtml --> 
@page "/dialogs-html"
@model DialogsHtmlModel

<!-- The Dialog -->
<dialog id="myDialog">
    <article id="dialogContent">
    </article>
</dialog>

<article>
    <!-- Trigger buttons -->
    <button data-target="myDialog"
            hx-get="@Url.Handler("Modal")"
            hx-target="#dialogContent"
            hx-trigger="click"
            onClick="showDialog()">
        Show Dialog
    </button>
</article>

@section Scripts
{
    <script>
        function showDialog() {
            const dialog = document.getElementById('myDialog');
            dialog.showModal(); // Opens the dialog
        }

        function closeDialog() {
            const dialog = document.getElementById('myDialog');
            dialog.close(); // Closes the dialog
        }
    </script>

}

<!-- DialogsHtml.cshtml.cs --> 
public class DialogsHtmlModel : PageModel
{
    public void OnGet()
    {
    }

    public IActionResult OnGetModal()
    {
        return Partial("_ModalPartial", ("Model Content Header", """
                Nunc nec ligula a tortor sollicitudin dictum in vel enim.
                Quisque facilisis turpis vel eros dictum aliquam et nec turpis.
                Sed eleifend a dui nec ullamcorper.
                Praesent vehicula lacus ac justo accumsan ullamcorper.
                """));
    }
}

<!-- _ModalPartial.cshtml -->
@model (string Header, string Text)

<header>
    <a href="#close" aria-label="Close" class="close" onclick="closeDialog(); return false;"></a>
    @Model.Header
</header>
<div>
    <p>
        @Model.Text
    </p>
</div>
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693339517554/c1c1baf8-88bd-4e9e-ba96-1b41118ec74c.gif)

A full code listing of how tabs could be created with **htmx** and **Razor Pages**. Here, I have tabs div, which is replaced with tab partial.

The interesting part is a `load` trigger, which is fired on page load to get the initial tab. That could be done through a razor like I did previously, as well.

```xml
<!-- Tabs.chtmls -->
@page "/tabs-hateoas"
@model TabsModel

<div id="tabs"
     hx-get="@Url.Handler("Tab1")"
     hx-trigger="load delay:100ms"
     hx-target="#tabs"
     hx-swap="innerHTML">
</div>

<!-- Tabs.chtmls.cs -->
public class TabsModel : PageModel
{
    public void OnGet()
    {
    }

    public IActionResult OnGetTab1()
    {
        return Partial("_Tab1Partial");
    }

    public IActionResult OnGetTab2()
    {
        return Partial("_Tab2Partial");
    }

    public IActionResult OnGetTab3()
    {
        return Partial("_Tab3Partial");
    }
}

<!-- _Tab1Partial.cshtml -->
<div class="grid">
    <button hx-get="@Url.Handler("Tab1")">Tab 1</button>
    <button hx-get="@Url.Handler("Tab2")" class="outline">Tab 2</button>
    <button hx-get="@Url.Handler("Tab3")" class="outline">Tab 3</button>
</div>

<article>
    <div>
        some text...
    </div>
</article>

<!-- _Tab2Partial.cshtml -->
<div class="grid" role="tablist">
    <button hx-get="@Url.Handler("Tab1")" class="outline">Tab 1</button>
    <button hx-get="@Url.Handler("Tab2")">Tab 2</button>
    <button hx-get="@Url.Handler("Tab3")" class="outline">Tab 3</button>
</div>

<article>
    <div>
        some text 2...
    </div>
</article>

<!-- _Tab3Partial.cshtml -->
<div class="grid" role="tablist">
    <button hx-get="@Url.Handler("Tab1")" class="outline">Tab 1</button>
    <button hx-get="@Url.Handler("Tab2")" class="outline">Tab 2</button>
    <button hx-get="@Url.Handler("Tab3")">Tab 3</button>
</div>

<article>
    <div>
        some text 3...
    </div>
</article>
```

Tabs, in my case, are buttons, but it still looks good

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693339622933/d10ebe4e-b859-4839-92b9-d1916643fdde.gif)

### 5.4. Live search feature on a Razor Page

Here we use `keyup` trigger to send a search query to `OnPostSearch` handler, which would render partial based on search results.

```xml
<!-- ActiveSearch.cshtml -->
@page "/active-search"
@model ActiveSearchModel

<article>
    <h3>
        Search Contacts
        <span class="htmx-indicator" aria-busy="true">Searching...</span>
    </h3>

    <input type="search" name="search"
           placeholder="Begin Typing To Search Users..."
           hx-post="@Url.Handler("Search")"
           hx-trigger="keyup changed delay:500ms, search"
           hx-target="#search-results"
           hx-indicator=".htmx-indicator"
           antiforgery="true"/>

    <table class="table">
        <thead>
        <tr>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Email</th>
        </tr>
        </thead>
        <tbody id="search-results">
        </tbody>
    </table>
</article>

<!-- ActiveSearch.cshtml.cs -->
public class ActiveSearchModel : PageModel
{
    public void OnGet()
    {
    }

    public IActionResult OnPostSearch(string search)
    {
        if (search == null || string.IsNullOrEmpty(search))
        {
            return Partial("_TableBody", new List<UserInfoViewModel>());
        }

        var users = TestData.Users.Where(u => u.Email.Contains(search.ToLowerInvariant())).ToList();

        if (users.Any())
        {
            return Partial("_TableBody", users);
        }

        return Partial("_TableBody", TestData.Users.Take(7).ToList());
    }
}

<!-- _TableBody.cshtml -->
@model List<Xakpc.RazorHtmx.Data.UserInfoViewModel>

@foreach (var userInfo in Model)
{
    <tr>
        <td>@userInfo.FirstName</td>
        <td>@userInfo.LastName</td>
        <td>@userInfo.Email</td>
    </tr>
}
```

Here is how it works

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693339714303/a8f9c891-c913-4ffa-b1dc-9ae2f37aaf77.gif)

### 5.5 Animations

While my primary focus wasn't on aesthetics during testing, it's worth noting that it could be done. **htmx** supports `CSS transitions` so we could apply some cool animations to our partials.

```xml
@page "/animations"
@model AnimationsModel

<article>
    <h2>Using the View Transition API</h2>
    <div class="slide-it">
        <partial name="_SwapContentPartial", model="@("Initial Content", "Swap It")"/>
    </div>
</article>

@section Styles
{
  <style>
   @@keyframes fade-in {
     from { opacity: 0; }
   }

   @@keyframes fade-out {
     to { opacity: 0; }
   }

   @@keyframes slide-from-right {
     from { transform: translateX(90px); }
   }

   @@keyframes slide-to-left {
     to { transform: translateX(-90px); }
   }

   .slide-it {
     view-transition-name: slide-it;
   }

   ::view-transition-old(slide-it) {
     animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
     600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
   }
   ::view-transition-new(slide-it) {
     animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
     600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
   }
</style>
}
```

In partial, we instruct **htmx** to use transition.

```xml
@model (string Title, string Button)

<h6>@Model.Title</h6>
<button hx-get="@Url.Handler("NewContent")" hx-swap="innerHTML transition:true" hx-target="closest div">
    @Model.Button
</button>
```

This gives us nice animation.

![animation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/640hdyrnc748b6vd6sjt.gif)

## 6. Benefits of the Integration

Simplicity, simplicity, simplicity.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1693302736218/f0fa052e-2ee5-4adc-9b5e-3a40c763f753.gif)

I am a huge advocate of simplicity. I work with highly complex systems in my daily job, where dozens of microservices process gigabytes of data 24/7.

Probably, that's why I wouldn't say I like to overcomplicate anything. And like **Razor Pages** was invented to simplify MVC web development, **pico.css** was invented to simplify web design, so as far as I see, **htmx** could be the tool to simplify interactivity.

But these are all personal preferences. Let's evaluate what actual benefits **Razor Pages** together with **htmx** might produce.

### 6.1. Speed and Efficiency: Faster page loads and enhanced user experience.

**htmx** allows for seamless partial page updates, meaning web pages can reflect real-time data changes without requiring a complete page refresh. This leads to a more interactive and immersive user experience, particularly beneficial for applications where up-to-the-minute data display is crucial, like dashboards or monitoring systems.

Due to the lightweight nature of **htmx**, only necessary data is transferred between client and server. This minimizes the bandwidth usage and server processing power required for each user interaction, making it possible to serve more users without a proportional resource increase.

### 6.2. Maintainability: Separation of concerns with Razor's code-behind model and htmx's lightweight nature.

**Razor Pages** is MPA (Multi-Page Application) framework. It is designed for creating web applications where each user interaction or action corresponds to a different web page loaded from the server.

Historically, dotnet developers relied on JavaScript and jQuery to add interactivity. I believe there was never a "best-way" of how to organize JavaScript: there was a custom colocation option `cshtml.js`, section `@Scripts` , setup `grunt` and `npm` aside dotnet project, but most of the time, js was simply dumped into `wwwroot` folder.

htmx takes razor page concept of self-contained Page with its associated logic, data model, and view and adds interactivity there but keeps it **clean and maintainable**.

### 6.3. Simplified Development Workflow

By merging **Razor Pages** and **htmx**, developers can enjoy a more streamlined development process. The inherent structure of **Razor Pages**, which emphasizes a clear delineation between view and logic, combined with the straightforward integration of **htmx**, reduces the learning curve for new team members.

This synergy minimizes the need for extensive reliance on JavaScript for dynamic content, which can often introduce complexities. As a result, development teams can bring features to market faster and address bugs or changes with greater agility.

## 7. Potential Challenges and Solutions

While the benefits of this integration are numerous, like all technologies, it comes with its set of challenges.

### 7.1. Antiforgery Token

Running the code below to update data will result in the error:  `Response Status Error Code 400 from /click-to-edit?handler=Edit`

```xml
<form hx-put="@Url.Page("ClickToEdit", "Edit")" 
        hx-target="this" 
        hx-swap="outerHTML"> 
   <!-- form body -->
</form>
```

This is because any POST or PUT request to the razor page requires AntiforgeryToken to prevent CSRF attacks.

> Cross-Site Request Forgery (CSRF) is an attack where a malicious entity tricks a user's browser into performing undesired actions on a trusted site where the user is authenticated. This is possible because browsers automatically include cookies, including session cookies, with requests.
> 
> To counteract this, many sites employ CSRF tokens, generated server-side. These tokens, which can be created per session or request, ensure that every action taken on a site is genuinely intended by the authenticated user.
> 
> The server validates the token with every request; if they don't match, the request is denied, potentially flagging it as a CSRF attempt.

Antiforgery Token check could be disabled by applying `[IgnoreAntiforgeryToken(Order = 1001)]` attribute on the page model, but that is a security risk.

You could manually expand forms and add anti-forgery token with a helper method `@Html.AntiForgeryToken()`. This would provide tokens as a hidden field and include it into form data.

Tokens also could be provided as a header. It is considered a more secure option, and we could add a header to our htmx request using `hx-headers` attribute.

```xml
<form hx-put="@Url.Page("ClickToEdit", "Edit")" 
    hx-headers='{"RequestVerificationToken": "@Model.AntiforgeryToken"}'
    hx-target="this" hx-swap="outerHTML" >
```

For forms, it is better to use the helper method. But to add tokens to other HTML elements, such as `<button>` or `<input>` I created a helper tag class. It would generate `hx-headers` tag and set token value when I add an attribute `antiforgery="true"` to element.

```csharp
[HtmlTargetElement("input", Attributes = AntiForgeryAttributeName)]
[HtmlTargetElement("button", Attributes = AntiForgeryAttributeName)]
public class AntiForgeryHeaderTagHelper : TagHelper
{
    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    [HtmlAttributeName(AntiForgeryAttributeName)]
    public bool AntiForgery { get; set; }

    private const string AntiForgeryAttributeName = "antiforgery";
    private readonly IAntiforgery _antiforgery;

    public AntiForgeryHeaderTagHelper(IAntiforgery antiforgery)
    {
        _antiforgery = antiforgery;
    }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (AntiForgery)
        {
            var token = _antiforgery.GetAndStoreTokens(ViewContext.HttpContext).RequestToken;
            var currentHeaderValue = output.Attributes["hx-headers"]?.Value.ToString();
            var newHeaderValue = $"\"RequestVerificationToken\": \"{token}\"";

            if (string.IsNullOrEmpty(currentHeaderValue))
            {
                output.Attributes.SetAttribute("hx-headers", newHeaderValue);
            }
            else
            {
                // Append the anti-forgery token to existing hx-headers
                newHeaderValue = $"{currentHeaderValue}, {newHeaderValue}";
                output.Attributes.SetAttribute("hx-headers", newHeaderValue);
            }
        }
    }
}
```

### 7.2. Validation

MVC and DataAttributes provide a lot of helpers to generate validation. Unfortunately, most of it is designed for `jquery.validate.js` and `jquery.validate.unobtrusive.js`.

On the other hand, **htmx** is designed to utilize HTML5 validation.

Of course, we could disable client-side validation and write everything by hand, but that sounds annoying.

```csharp
services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });
```

At the moment I didn't found a good solution for that. I could see a several options here:

* Link together HTML 5 and MVC-generated validation, by writing custom js code
* Link together MVC-generated validation and htmx, but keep using `jquery.validate`

If you know a solution to that problem, feel free to ping me.

## 8. Conclusion

To sum it up, I tried different use cases of **htmx** and dotnet **Razor Pages** during my tests. In this article, I presented the history of **htmx** and **Razor Pages**, and show you how they can be integrated seamlessly.

I was impressed by how a technology from 2009, **partials**, integrates so well with **htmx**. It also fits well with **Razor Pages** and **pico.css**, which I often use for side projects. I only have positive impressions here:

* It works with razor and MVC partials perfectly
* It's a good fit with semantic HTML CSS libraries
* It's two times smaller than jQuery
* It's straightforward to use

Regarding the negative aspects

* There is no IntelliSense support of htmx tags in Visual Studio yet (there is for Visual Studio Code)
* Need to figure out validation to reuse all that helpers **Razor Pages** provides
* Some kind of [fragments](https://htmx.org/essays/template-fragments/) would be nice
* **htmx** might not be accepted by enterprises where dotnet is most used

That would be all. Once again, the source code of my tests is available on [github](https://github.com/xakpc/RazorHtmx), the demo project [deployed on Azure](https://razorhtmx.azurewebsites.net/), and if you have any questions, feel free to ping [me](https://x.com/xakpc) on 
Twitter
, sorry, on 𝕏.

## 9. Further Reading/References

In the future, I might explore some more advanced scenarios, like validation, boosting, history, and maybe even touch hyperscript (but I doubt that; it does not bring me joy at first glance).

Some useful resources

* [Microsoft Razor Pages documentation](https://learn.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-7.0&tabs=visual-studio)
* [Learn Razor Pages](https://www.learnrazorpages.com/) by Mike Brind
* [htmx Documentation](https://htmx.org/docs/)