Featured image of post Server-side rendered SPAs with ASP.NET and no Javascript

Server-side rendered SPAs with ASP.NET and no Javascript

How to build single page apps with ASP.NET and HTMX

Follow me

Introduction Link to this section

Single-page applications (SPA) provide better user experience by removing full-page loads, giving smooth transitions between pages and performance improvements depending on the scale of the application, but they come at some costs which may not be worth for some application types. The main cost is the added complexity of a SPA framework like Angular or Vue.

In this post, I’ll show how to use ASP.NET and HTMX to build single-page applications without using JavaScript and SPA frameworks.

What is HTMX Link to this section

HTMX is a library that allows us to access modern browser features by extending HTML, without needing to use Javascript directly. It gives us access to AJAX, CSS Transitions, WebSockets and more, including extensions.

It simplifies making responsive apps that give our users a better experience without the complexity of Javascript.

HTMX is really powerfull, but in this post I’ll focus on the hx-boost attribute.

Base application Link to this section

I’ll use an old Razor Pages Sample Application by Damian Edwards to show how easy it is to make an SPA with ASP.NET and HTMX.

I’ve made some changes in his sample, including upgrading from .NET Core 2.0 to .NET 7. These changes are in the branch sample-base of the sample repository: AspNetCoreSPAHtmx.

💡 I’m using Razor pages but what I’ll show is also aplicable to ASP.NET MVC.

Let’s open the _Layout.cshtml and add the reference to HTMX at the end of the <body> tag:

1
2
3
4
    ...

    <script src="https://unpkg.com/htmx.org@1.9.6"></script>
</body>

HTMX has an attribute hx-boost that enable us to transform our Multi-Page Application in a Single Page Application (SPA) with just some tweaks, making <a> and <form> tags use AJAX requests instead of page reloads.

It works on all the children of the element it is applied to, so let’s include it in the <nav> tag so all the menu links use AJAX:

<nav class="navbar navbar-inverse navbar-fixed-top" hx-boost="true" hx-target="#main-content">

By default, hx-boost applies the AJAX response to the <body> tag, so we need to specify the id of target (in this sample, #main-content) in the hx-target attribute.

Now, let’s create a div with id main-content to receive the response content inside the body-content div:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div class="container body-content">
    <div id="main-content">
        @RenderBody()
    </div>

    <hr />

    <footer>
        <p>Full-page load at @DateTime.Now</p>
        <p>&copy; 2017 - Razor Pages</p>
    </footer>
</div>

ℹ️ Note that the time of the full-page load is in the footer, so we can check that the requests are done with AJAX.

Removing the layout for HTMX requests Link to this section

All requests done with HTMX will have a Hx-Request header set to true.

We will check for it in the _ViewStart.cshtml file and remove the page layout for HTMX requests:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@{
    @if (!ViewContext.HttpContext.Request.Headers["Hx-Request"].Contains("true"))
    {
        Layout = "_Layout";
    }
    else
    {
        Layout = "";
    }
}

Setting up the forms Link to this section

Now, we also need to include the hx-boost and hx-target attributes in the <form> tags in the Index.cshtml, New.cshtml and Edit.cshtml files:

<form method="post" class="form-horizontal" hx-boost="true" hx-target="#main-content">

Now we can test the application. Note that the time of the page load in the footer doesn’t change:

Testing the application

Looking at the browser’s console, we can see the request was done:

Request in browser’s console

The form content in the request payload:

Request’s payload with form content

The response without the _Layout.cshtml content:

Response in browser’s console

The response rendered in the page:

The response rendered in the page

💡 ASP.NET Form Validation will also work:

Validation message

Changing the page title according to the page load Link to this section

Until now, the page title is not being changed, because it is set in the _Layout.cshtml file, that is loaded just on the first page load:

<title>@ViewData["Title"] - Razor Pages Sample + HTMX</title>

The hx-boost attribute will automatically change the page title if the response returns a <title> tag. To do this, let’s create a new layout for the HTMX requests.

Create a _LayoutHtmxBoost.cshtml with the below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@if (!string.IsNullOrEmpty(ViewData["Title"]!.ToString()))
{
    <head>
        <title>@ViewData["Title"] - Razor Pages Sample + HTMX</title>
    </head>
}

@RenderBody()

@RenderSection("Scripts", required: false)

Note that we return a <title> tag if the ViewData["Title"] has any value.

Now, let’s change the _ViewStart.cshtml file to use the _LayoutHtmxBoost layout for HTMX requests:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@{
    @if (!ViewContext.HttpContext.Request.Headers["Hx-Request"].Contains("true"))
    {
        Layout = "_Layout";
    }
    else if (ViewContext.HttpContext.Request.Headers["Hx-Boosted"].Contains("true"))
    {
        Layout = "_LayoutHtmxBoost";
    }
    else
    {
        Layout = "";
    }
}

⚠️ We are checking for the Hx-Boosted header non-boosted HTMX requests don’t use this layout.

💡 HTMX automatically pushes the pages to the browser’s history:

Browser history

Indicating the loading status Link to this section

That’s cool, but we don’t have an indicator of the request being processed. For this, we can use the hx-indicator attribute, specifying the id of the element to show while awaiting for the request.

The hx-indicator works by adding the htmx-request css class to the element specified, that will set the opacity to 1. For this to work, we first need to hide the element with an opacity of 0, or use the htmx-indicator css class that does this with a css transaction.

First, I created a partial page with a loader from Pure CSS Loaders with the name _LoadSpinner, and set the id to spinner and added the htmx-indicator css class:

1
2
3
<div id="spinner" class="htmx-indicator">
    <div class="lds-ring"><div></div><div></div><div></div><div></div></div>
</div>

Then, I added it in the menu:

1
2
3
4
5
6
7
<div class="collapse navbar-collapse" id="header-navbar-collapse">
    <ul class="nav navbar-nav">
        <li><a asp-page="/Index">Home</a></li>
        <li><a asp-page="/Customers/Index">Customers</a></li>
        <li><partial name="_LoadSpinner" /></li>
    </ul>
</div>

Lastly, I add the hx-indicator attribute with the value #spinner to the <nav> element:

<nav class="navbar navbar-inverse navbar-fixed-top" hx-boost="true" hx-target="#main-content" hx-indicator="#spinner">

and all <form> elements:

<form method="post" class="form-horizontal" hx-boost="true" hx-target="#main-content" hx-indicator="#spinner">

💡 To make it easier to see the indicator, I included a delay of 500 milliseconds to all pages GET/POST methods:

1
2
3
4
5
6
public async Task<IActionResult> OnGetAsync(int id)
{
    await Task.Delay(TimeSpan.FromMilliseconds(500));

    ...
}

Running the app again, we can see the indicator showing in every actions:

Loading indicator in every action

Full source code Link to this section

GitHub Repository

💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy