Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2023/using-htmx-with-sitecore-mvc

Using the htmx framework with Sitecore MVC

It's been five minutes. New JavaScript framework anybody?

Published 19 June 2023
Updated 19 June 2023

I'm not much of a front-end person. While I can do JavaScript and CSS if I need to, I tend to have to spend quite a bit of time in Google remembering all the key facts. But every so often I find myself needing to do some client-side code, so anything that can make that job easier seems like an interesting idea. Recently I came across the htmx framework - which offers a way to do common AJAX-like dynamic front-end tasks with little ceremony, and pretty much zero JavaScript. So I figured I should have a hack about and see if it's of any use with the sort of "traditional" Sitecore I'm confident with...

What is it?

The framework is a small (~14kb) script library which gives you a very simple way to add dynamic behaviour to web pages. It wires up events in your UI (like a button click) to an HTTP request for content, and replaces bits of your page with the results. For most of this, you don't need to write any JavaScript - just supply attributes and API endpoints to do the work.

For example, to steal a very minimal example from their documentation:

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

<button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
</button>

					

And when you run this, it broadly works out as:

graph LR;
  A["Your browser
loads the HTML & JS
above"] B["You click
the button"] C["htmx gets content
by calling the
'hx-&lt;action&gt;' url"] D["That server
responds with
some new HTML"] E["htmx replaces
content using
'hx-swap' attribute"] A-->B B-->C C-->D D-->E

The first line imports the library from its CDN hosting. The <button/> element is decorated with an hx-post attribute which says "when this is clicked, send a POST request to /clicked and fetch the response. And the hx-swap attribute says what to do with the result from the POST - in this case swap the outer HTMX of this element with it. So if a call to /clicked replied with an HTTP 200 response of <div>You clicked me!</div> that would replace the <button/> in the DOM after the click event.

So that's the sort of dynamic site behaviour even I can get behind.

There's a lot more too it - including how CSS transitions are handled, what elements get replaced, what events are handled etc.

Very quick experiment time!

So, how can we do this with Sitecore MVC?

The obvious starting point is a controller for the initial of a component. As a trivial example, say we need a component that loads a bulleted list of content items. To keep it very simple, the view could be:

<div>
    <h2 class="head">Base Component</h2>

    <ul class="slide-it" id="data-element">... load me ...</ul>

    <button hx-post="/api/children"
            hx-trigger="click"
            hx-target="#data-element"
            hx-swap="outerHTML transition:true">
        Component Load!
    </button>
<div>

					

The <ul/> is going to be the target of the new content. It needs an ID, so we can target it for replacement later, and it gets some content just so we can see it changes.

And the button to trigger the replacement is similar to the example above. It's going to make a POST call to /api/children on our site - which we'll get to later. The behaviour gets triggered by the click, based on the hx-trigger attribute. The hx-target element lets us specify a CSS selector to target the <ul/> for replacement. And the hx-swap attribute says we're swapping the outer html with whatever comes back from the API call, and that we want it to honour the CSS transitions that this invoves. (Again, more of that later)

So that can be set up as a Controller or View Rendering as necessary, and it could have whatever Sitecore data you wanted in it too.

Now fetching the API data is a bit more interesting here. The most common add-on for a Sitecore MVC website to return data for a JavaScript UI is to use an ApiController which returns JSON. But we don't want that here because the API call here needs to return HTML.

You could have a non-Sitecore Controller returning a View here. But that likely has a few challenges with getting Sitecore data easily. What database to use, or similar. Not a huge issue, but enough to waste some of your time... So what about using a Sitecore Rendering instead?

As a test, lets give a Controller Rendering a try. That can render a partial page with some data to use. If we add a page which has a field for a simple data query:

The Sitecore UI showing a page defining a data query

And a trivial example controller to run on that page could grab some data and generate a model:

public ActionResult GetData()
{
    var qry = Sitecore.Context.Item.Fields["Query"].Value;
    var items = Sitecore.Context.Database.SelectItems(qry);
    var data = items.Select(i => new Result { 
        Title = i.Fields["Title"].Value, 
        Text = i.Fields["Text"].Value 
    });

    return View(data);
}

					

And a view can format the returned data:

@model IEnumerable<HtmxExperiment.Controllers.Result>
<ul class="slide-it" id="data-element">
    <li>Component @DateTime.Now.ToLongTimeString()</li>
@foreach (var itm in Model)
{
    <li class="item">
        <h3 class="title">@itm.Title</h3>
        @itm.Text
    </li>
}
</ul>

					

With that wired up with the usual Controller Rendering definition items and Presentation details, that will return a new <ul/> element with the same attributes, but including some data fetched out of Sitecore.

(And in the background we can also add some CSS to animate in the new content)

With that published, we can load the base page, and click the button to see the content replacement occur:

And that's pretty good for me. A simple component that can load Sitecore content with zero JavaScript, and respects any CSS transitions you define into the bargain...

What about state?

The example above puts all the state for the data query into Sitecore, which isn't realistic. Usually the UI would send back some user data too. But it's a trivial change to send some state data as part of the page's request.

Adjusting the basic component view to include a form to post data looks like this:

<div>
    <h2 class="head">Base Component</h2>
    <ul class="slide-it" id="data-element">... load me ...</ul>

    <form hx-put="/api/children" hx-target="#data-element" hx-swap="outerHTML transition:true">
        <div>
            <label>Query:</label>
            <input type="text" name="query" value="/sitecore/Content/Home/*">
        </div>
        <button class="btn">Component Load!</button>
    </form>
</div>

					

The hx-<action> etc attributes move onto the form, and when it submits the same behaviour as above occurs.

And we can trivially update the controller to read that state data instead of reading the query from the context item:

public ActionResult GetData(string query)
{
    var items = Sitecore.Context.Database.SelectItems(query);
    var data = items.Select(i => new Result { 
        Title = i.Fields["Title"].Value, 
        Text = i.Fields["Text"].Value 
    });

    return View(data);
}

					

The view doesn't need to change, but this works as expected:

Obviously you woudn't post the Sitecore query like this in real life - security issues - but this is enough to prove the concept.

Conclusions

There's one important thing I've not considered here in my quick and dirty test, but I don't think it's a big issue: As I said at the start - I'm part of the old guard, clinging pointlessly to MVC... (Joke)

While that's the sort of code I'm maintaining, a lot of new stuff would be written in Headless patterns. Would this work there? Well yes I think it should. The same approach as above to provide the updated mark-up using a Sitecore "partial page" could be implemented with a headless UI too. And you could write edge function code, or have non-Sitecore responses from your head to retrun HTML too.

So this is definitely something I'll keep in my toolbox for those situations where I need a bit of dynamic-site action, but don't want to go full Front-End on it...

Updated to add: Talking about this with Corey Smith on Slack, he pointed out that the API view above does not play nice with Experience Editor. And he was right - I'd not actually looked at that...

I was using a Layout for the API call which returned nothing other than the HTML from the component it contained. All that was in the razor file was:

@Html.Sitecore().Placeholder("Main")

						

But since that doesn't add the <html/> and <body/> elements to the page, Experience Editor gets a bit broken. The right places for it to inject its markup and scripts don't exist, so you get a chunk of unprocessed JSON instead:

Experience editor showing a broken page. Instead of the usual editing ribbon there is a chunk of json rendered above the UI component

This is easy to fix though. It needs a bit of logic in the Layout so that Experience Editor does get that extra markup but the public view of the layout doesn't:

@if (Sitecore.Context.PageMode.IsPreview || Sitecore.Context.PageMode.IsExperienceEditorEditing)
{
<html>
<body>
    @Html.Sitecore().Placeholder("Main")
</body>
</html>
}
else
{
    @Html.Sitecore().Placeholder("Main")
}

						

With that, the htmx UI works fine in Experience Editor for editing both the public page and the API "partial page"...

↑ Back to top