Photo by Clint Adair on Unsplash
A few months ago, the HTMX team published an essay on so-called template fragments to adhere to the Locality of Behavior (and in my opinion also cohesion) principle in server side rendered views.
If you're using a template engine it is likely you end up with several views like typical parent / child/detail views:
Until now, it has been common that this results in multiple view/component files. However, this comes with a disadvantage:
While separation of concern is improved now, in the same way cohesion and locality of behavior deteriorates:
Locality of Behavior
The behaviour of a unit of code should be as obvious as possible by looking only at that unit of code
Cohesion
Cohesion refers to the degree to which the elements inside a module belong together.
The proposed solution is to have a single template file but being able to address fragments of it by a key. By providing the key by e.g. a controller method calling the view engine, it will just render the fragment.
This is the proposed sample as an HTML file providing template fragment support:
<html>
<body>
<div hx-target="this">
#fragment archive-ui
#if contact.archived
<button hx-patch="/contacts/${contact.id}/unarchive">Unarchive</button>
#else
<button hx-delete="/contacts/${contact.id}">Archive</button>
#end
#end
</div>
<h3>Contact</h3>
<p>${contact.email}</p>
</body>
</html>
The fragment id here is archive-ui
. So you can either render the full view or just two buttons.
When reading this essay after it has been published, it appealed to me a lot, but I didn't think it could be supported by ASP.NET Core Razor without extending the View engine.
So I went over to GitHub and created an issue, requesting for that enhancement. Although it has been triaged and moved to the Backlog this won't be available soon (read: at least not in .NET 8).
After creating the issue, I was repeatedly annoyed by the lack of support, but did not pursue the issue further - until yesterday.
The topic came up again in the dotnet-htmx channel on the HTMX discord server.
Based on the discussion, a rough idea formed in my head how this could be solved without extending Razor at all, getting full IDE support and all the like.
Here's what I came up with - let's just talk code:
First, we define some models:
public class FragmentModel
{
public FragmentModel(
string fragmentId
)
{
FragmentId = fragmentId;
}
public string FragmentId { get; set; }
}
public class ChildModel : FragmentModel
{
public int Id { get; }
public ChildModel(
int id
) : base("Detail")
{
Id = id;
}
}
public class ParentModel : FragmentModel
{
public List<ChildModel> Childs { get; }
public ParentModel(
List<ChildModel> childs
) : base("Full")
{
Childs = childs;
}
}
Then we define a single View file:
@model RazorFragments.Models.FragmentModel
@{
Layout = "_Layout";
}
@{
void RenderDetail(
ChildModel child)
{
<div class="bg-gray-200">
@Html.ActionLink(child.Id.ToString(), "Detail", new
{
id = child.Id
})
</div>
}
void RenderFull(
ParentModel parent)
{
<div class="bg-blue-500 p-4">
@{
@foreach (var parentChild in parent.Childs)
{
RenderDetail(parentChild);
}
}
</div>
}
}
@{
switch (Model.FragmentId)
{
case "Full":
RenderFull(Model as ParentModel);
break;
case "Detail":
RenderDetail(Model as ChildModel);
break;
}
}
And everything gets tied together in our controller:
public class RazorFragmentController : Controller
{
// GET
public IActionResult Index()
{
return View(
new ParentModel(
new List<ChildModel>()
{
new(1),
new(2)
}
)
);
}
[Route("/detail/{id}")]
public IActionResult Detail(
[FromRoute] int id
)
{
return View("Index", new ChildModel(id));
}
}
This is just a first draft stitched together in the middle of the night so I'm curious about your thoughts, feedback and suggestions for improvement.
The full sample can be found on GitHub.
I created another sample which is closer to the mockup in from the intro:
List view
Details view