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

Daniel Genezini - Sep 16 - - Dev Community

Introduction

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

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

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.

Including the HTMX lib and menu links configurations

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

    ...

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

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">
Enter fullscreen mode Exit fullscreen mode

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:

<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>
Enter fullscreen mode Exit fullscreen mode

ℹ️ 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

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:

@{
    @if (!ViewContext.HttpContext.Request.Headers["Hx-Request"].Contains("true"))
    {
        Layout = "_Layout";
    }
    else
    {
        Layout = "";
    }
}
Enter fullscreen mode Exit fullscreen mode

Setting up the forms

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">
Enter fullscreen mode Exit fullscreen mode

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

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>
Enter fullscreen mode Exit fullscreen mode

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:

@if (!string.IsNullOrEmpty(ViewData["Title"]!.ToString()))
{
    <head>
        <title>@ViewData["Title"] - Razor Pages Sample + HTMX</title>
    </head>
}

@RenderBody()

@RenderSection("Scripts", required: false)
Enter fullscreen mode Exit fullscreen mode

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:

@{
    @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 = "";
    }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ 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

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:

<div id="spinner" class="htmx-indicator">
    <div class="lds-ring"><div></div><div></div><div></div><div></div></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Then, I added it in the menu:

<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>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

and all <form> elements:

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

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

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

GitHub Repository

References and Links

Related

Liked this post?

I post extra content in my personal blog.

Daniel Genezini | It works on my machine

My blog where I share my experiences about .NET, Blazor, AWS, and other tech stuff. Very important note: Everything that I post works on my machine!

blog.genezini.com

Follow me

. . . . . . . . . . . . . . . . . . .