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...
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-<action>' 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.
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:
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...
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.
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...↑ Back to topI 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: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"...