Building Your First App with HTMX and .NET - Part II
By FoxLearn 2/7/2025 8:50:43 AM 50
Before we dive into the feature implementation, we’ll refactor some parts of the code for better maintainability by creating reusable Razor components.
Refactoring the ListArticlesPage.razor
We begin by refactoring the ListArticlesPage.razor
file. First, create the RazorComponents/DataTable.razor
file:
@typeparam TItem <div class="table-responsive"> <table class="table"> <thead class="table-light"> <tr>@TableHeader</tr> </thead> <tbody> @if (Items is not null && Items.Any()) { @foreach (var item in Items) { <tr> @RowTemplate(item) </tr> } } </tbody> </table> </div> @code { [Parameter, EditorRequired] public IEnumerable<TItem> Items { get; set; } = default!; [Parameter, EditorRequired] public RenderFragment<TItem> RowTemplate { get; set; } = default!; [Parameter, EditorRequired] public RenderFragment? TableHeader { get; set; } = default!; }
This component will help us display a list of articles. Create the RazorComponents/HtmxAttributes.cs
file:
namespace BlogApp.RazorComponents; public class HtmxAttributes { public string Target { get; set; } = default!; public string Swap { get; set; } = "innerHTML"; public string Endpoint { get; set; } = default!; public string Select { get; set; } = default!; public HtmxAttributes() { } public HtmxAttributes(string endpoint, string target) { Endpoint = endpoint; Target = target; } public HtmxAttributes(string endpoint, string target, string swap) : this(endpoint, target) { Swap = swap; } public HtmxAttributes(string endpoint, string target, string swap, string select) : this(endpoint, target, swap) { Select = select; } }
The HtmxAttributes
class stores HTMX attributes such as the target and swap options to pass between components. Next, create the RazorComponents/SearchFilter.razor
component:
<input type="search" class="form-control" name=@Property hx-trigger="input changed delay:500ms, search" [email protected] [email protected] [email protected] [email protected] @attributes="Attributes" /> @code { [Parameter, EditorRequired] public string Property { get; set; } = default!; [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? Attributes { get; set; } [Parameter, EditorRequired] public HtmxAttributes HtmxAttributes { get; set; } = default!; }
The SearchFilter
component allows for a search field with HTMX support. Next, create the RazorComponents/ActionButton.razor
file:
<button type="button" [email protected] [email protected] [email protected] class="@Icon btn btn-primary"> New Article </button> @code { [Parameter, EditorRequired] public string Label { get; set; } = default!; [Parameter, EditorRequired] public HtmxAttributes HtmxAttributes { get; set; } = default!; [Parameter] public string Icon { get; set; } = string.Empty; }
Finally, create the RazorComponents/Section.razor
file:
<div class="card mb-4"> <div class="card-body"> @Content </div> </div> @code { [Parameter, EditorRequired] public RenderFragment? Content { get; set; } = default!; }
With these components in place, the ListArticlesPage.razor
file can be refactored as follows:
@using BlogApp.RazorComponents; <h4>List Articles</h4> <nav class="navbar hstack gap-3 justify-content-end"> <ActionButton Label="New Article" Icon="bi bi-plus-lg" HtmxAttributes=@(new HtmxAttributes("/articles/register", "#main", "innerHTML")) /> </nav> <Section> <Content> <div class="row mb-4"> <div class="col"> <SearchFilter Property="Title" placeholder="Enter title" HtmxAttributes=@(new HtmxAttributes("/articles/list", "#results", "OuterHTML", "#results")) /> </div> </div> <div id="results"> <DataTable Items=@Articles Context="item"> <TableHeader> <th>#</th> <th>Title</th> <th>Author</th> <th>Created At</th> <th>Is Published</th> </TableHeader> <RowTemplate> <td>@item.ArticleId</td> <td>@item.Title</td> <td>@item.Author</td> <td>@item.CreatedAt</td> <td>@item.IsPublished</td> </RowTemplate> </DataTable> </div> </Content> </Section> @code { [Parameter] public List<Article> Articles { get; set; } = default!; }
To display icons, include the following link tag in your MainPage.razor
file:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css" />
Create the RegisterArticlePage.razor
The registration page allows users to create a new article. First, we need to create several new reusable components.
RazorComponents/TextInput.razor
<div class="form-group"> <label for=@Property class="form-label">@Label</label> <input type="text" class="form-control" id=@Property name=@Property @attributes="Attributes" /> </div> @code { [Parameter, EditorRequired] public string Property { get; set; } = default!; [Parameter, EditorRequired] public string Label { get; set; } = default!; [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? Attributes { get; set; } }
RazorComponents/TextAreaInput.razor
<div class="form-group"> <label for=@Property class="form-label">@Label</label> <textarea class="form-control" id=@Property name=@Property @attributes="Attributes" /> </div> @code { [Parameter, EditorRequired] public string Property { get; set; } = default!; [Parameter, EditorRequired] public string Label { get; set; } = default!; [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? Attributes { get; set; } }
RazorComponents/Form.razor
<form [email protected] [email protected] [email protected] hx-ext="json-enc"> @Content <button type="submit" class="btn btn-primary"> <span>Save Changes</span> </button> </form> @code { [Parameter, EditorRequired] public RenderFragment? Content { get; set; } = default!; [Parameter, EditorRequired] public HtmxAttributes HtmxAttributes { get; set; } = default!; }
Now, using these components, the RegisterArticlePage.razor
will look like this:
@using BlogApp.RazorComponents; <h4>Register Article</h4> <Section> <Content> <Form HtmxAttributes=@(new HtmxAttributes("/articles/register","#main", "innerHTML"))> <Content> <div class="row mb-4"> <div class="col-6"> <TextInput Property="Title" Label="Title" placeholder="Enter title" maxlength=200 required /> </div> <div class="col-6"> <TextInput Property="Author" Label="Author" placeholder="Enter author's name" required /> </div> </div> <div class="row mb-4"> <div class="col"> <TextAreaInput Property="Content" Label="Content" rows="5" placeholder="Enter content" maxlength=4000 required /> </div> </div> </Content> </Form> </Content> </Section> @code { }
Editing an Article
To edit an article, we will create a handler for the EditArticle
functionality.
Articles/EditArticle.cs
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace BlogApp.Articles; public static class EditArticle { public class Request { public string? Content { get; set; } public string? Title { get; set; } public bool IsPublished { get; set; } } public static async Task<RazorComponentResult> HandlePage([FromRoute] Guid articleId, [FromServices] BlogAppDbContext appDbContext) { var article = await appDbContext.Set<Article>().AsNoTracking().FirstAsync(a => a.ArticleId == articleId); return new RazorComponentResult<EditArticlePage>(new { Article = article }); } public static async Task<RazorComponentResult> HandleAction([FromRoute] Guid articleId, [FromServices] BlogAppDbContext appDbContext, [FromBody] Request request) { var article = await appDbContext.Set<Article>().FindAsync(articleId); article.Content = request.Content; article.Title = request.Title; article.IsPublished = request.IsPublished; await appDbContext.SaveChangesAsync(); return await ListArticles.HandlePage(appDbContext, new ListArticles.Request()); } }
The HandlePage
method renders the edit form, while the HandleAction
method updates the article after submission.
Now, update the Endpoints.cs
file:
namespace BlogApp.Articles; public static class Endpoints { public static void RegisterArticleEndpoints(this WebApplication app) { var group = app.MapGroup("/articles"); group.MapGet("/list", ListArticles.HandlePage); group.MapGet("/register", RegisterArticle.HandlePage); group.MapPost("/register", RegisterArticle.HandleAction); group.MapGet("/{articleId:guid}/edit", EditArticle.HandlePage); group.MapPost("/{articleId:guid}/edit", EditArticle.HandleAction); } }
Now, create an ActionLink
component to show the edit button:
<a class="icon-link" href="#" [email protected] [email protected] [email protected]> <span class=@Icon></span> </a> @code { [Parameter, EditorRequired] public string Icon { get; set; } = default!; [Parameter, EditorRequired] public HtmxAttributes HtmxAttributes { get; set; } = default!; }
Finally, update the ListArticlesPage.razor
to add the edit functionality:
<DataTable Items=@Articles Context="item"> <TableHeader> <th></th> <th>#</th> <th>Title</th> <th>Author</th> <th>Created At</th> <th>Is Published</th> </TableHeader> <RowTemplate> <td> <div class="hstack gap-1"> <ActionLink Icon="bi bi-pencil" HtmxAttributes=@(new HtmxAttributes($"/articles/{item.ArticleId}/edit", "#main", "innerHTML")) /> </div> </td> <td>@item.ArticleId</td> <td>@item.Title</td> <td>@item.Author</td> <td>@item.CreatedAt</td> <td>@item.IsPublished</td> </RowTemplate> </DataTable>
The Edit Article Page
The EditArticlePage.razor
component will look like this:
@using BlogApp.RazorComponents; <h4>Edit Article</h4> <Section> <Content> <Form HtmxAttributes=@(new HtmxAttributes($"/articles/{Article.ArticleId}/edit","#main", "innerHTML"))> <Content> <div class="row mb-4"> <div class="col-6"> <TextInput Property="Title" Label="Title" placeholder="Enter title" maxlength=200 [email protected] disabled readonly /> </div> <div class="col-6"> <TextInput Property="Author" Label="Author" [email protected] disabled readonly /> </div> </div> <div class="row mb-4"> <div class="col"> <TextAreaInput Property="Content" Label="Content" rows="5" [email protected] placeholder="Enter content" maxlength=4000 /> </div> </div> <div class="row mb-4"> <div class="col"> <label for="IsPublished">Is Published</label> <input type="checkbox" id="IsPublished" name="IsPublished" checked="@Article.IsPublished" /> </div> </div> </Content> </Form> </Content> </Section> @code { }
This page will allow users to edit the title, content, and publication status of an article.