Initial commit
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
namespace PowderCoating.Web.TagHelpers;
|
||||
|
||||
/// <summary>
|
||||
/// Razor tag helper that transforms any <c><th sortable="ColumnName"></c>
|
||||
/// element into a clickable, sortable column header with a sort-direction chevron.
|
||||
/// <para>
|
||||
/// Usage in a Razor view:
|
||||
/// <code>
|
||||
/// <th sortable="CustomerName"
|
||||
/// current-sort="@Model.SortColumn"
|
||||
/// current-direction="@Model.SortDirection">
|
||||
/// Customer
|
||||
/// </th>
|
||||
/// </code>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Design decisions:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// All existing query-string parameters (search term, page number, filters)
|
||||
/// are preserved when building the sort URL. This means clicking a column
|
||||
/// header does not reset an active search — a common UX frustration in
|
||||
/// simpler implementations.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// The page number is explicitly reset to 1 when the sort column changes
|
||||
/// because sorted data shifts row positions, making a previously-active
|
||||
/// page number meaningless.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// The tag helper targets <c><th></c> elements (rather than a custom
|
||||
/// element) so that existing table markup does not need structural changes —
|
||||
/// adding the <c>sortable</c> attribute is sufficient.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// The <c>sortable</c> css class is merged with any existing class on the
|
||||
/// <c><th></c> (e.g. <c>text-end</c>, <c>ps-4</c>) so column-specific
|
||||
/// alignment and spacing are preserved.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[HtmlTargetElement("th", Attributes = "sortable")]
|
||||
public class SortableColumnTagHelper : TagHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The column identifier that will be sent as the <c>sortColumn</c> query
|
||||
/// parameter when the header link is clicked. This should match the string
|
||||
/// the controller uses to switch on sort behaviour.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("sortable")]
|
||||
public string ColumnName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The column currently being sorted, passed from the view model.
|
||||
/// When this equals <see cref="ColumnName"/> the header shows a directional
|
||||
/// chevron; otherwise a neutral double-arrow icon is shown.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("current-sort")]
|
||||
public string? CurrentSort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current sort direction (<c>"asc"</c> or <c>"desc"</c>), passed from
|
||||
/// the view model. Clicking the header toggles this to the opposite value.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("current-direction")]
|
||||
public string? CurrentDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current view context, injected automatically by the Razor tag helper
|
||||
/// infrastructure. <see cref="HtmlAttributeNotBound"/> prevents Razor from
|
||||
/// treating <c>view-context</c> as an HTML attribute on the element.
|
||||
/// Required to access <c>HttpContext.Request</c> for reading existing
|
||||
/// query string parameters.
|
||||
/// </summary>
|
||||
[ViewContext]
|
||||
[HtmlAttributeNotBound]
|
||||
public ViewContext ViewContext { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Rewrites the <c><th></c> element's content to contain a sort link.
|
||||
/// The method:
|
||||
/// <list type="number">
|
||||
/// <item>Copies all existing query parameters from the current URL.</item>
|
||||
/// <item>Overrides <c>sortColumn</c>, <c>sortDirection</c>, and <c>pageNumber</c>.</item>
|
||||
/// <item>Selects the appropriate Bootstrap Icon chevron for the current sort state.</item>
|
||||
/// <item>Wraps the cell's original text in an anchor tag pointing to the new URL.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="context">Tag helper execution context (provides access to all attributes).</param>
|
||||
/// <param name="output">The output object that controls the rendered HTML.</param>
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
var request = ViewContext.HttpContext.Request;
|
||||
var queryParams = request.Query.ToDictionary(k => k.Key, v => v.Value.ToString());
|
||||
|
||||
// Determine new sort direction
|
||||
var newDirection = "asc";
|
||||
if (CurrentSort == ColumnName && CurrentDirection == "asc")
|
||||
{
|
||||
newDirection = "desc";
|
||||
}
|
||||
|
||||
// Update query params
|
||||
queryParams["sortColumn"] = ColumnName;
|
||||
queryParams["sortDirection"] = newDirection;
|
||||
queryParams["pageNumber"] = "1"; // Reset to page 1 when sorting changes
|
||||
|
||||
// Build query string
|
||||
var queryString = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={Uri.EscapeDataString(kvp.Value)}"));
|
||||
var url = $"{request.Path}?{queryString}";
|
||||
|
||||
// Determine icon
|
||||
var icon = "bi-arrow-down-up";
|
||||
if (CurrentSort == ColumnName)
|
||||
{
|
||||
icon = CurrentDirection == "asc" ? "bi-arrow-up" : "bi-arrow-down";
|
||||
}
|
||||
|
||||
// Build output — preserve any existing classes (e.g. text-end, ps-4)
|
||||
var existingClass = output.Attributes["class"]?.Value?.ToString() ?? "";
|
||||
var combinedClass = string.IsNullOrEmpty(existingClass) ? "sortable" : $"sortable {existingClass}";
|
||||
output.Attributes.SetAttribute("class", combinedClass);
|
||||
|
||||
// Use the tag's inner content as link text so display text matches the markup
|
||||
var innerContent = (await output.GetChildContentAsync()).GetContent();
|
||||
output.Content.Clear();
|
||||
output.Content.AppendHtml($"<a href=\"{url}\" class=\"text-decoration-none\" style=\"color:inherit\">");
|
||||
output.Content.AppendHtml(string.IsNullOrWhiteSpace(innerContent) ? context.AllAttributes["sortable"].Value.ToString() : innerContent);
|
||||
output.Content.AppendHtml($" <i class=\"bi {icon}\"></i>");
|
||||
output.Content.AppendHtml("</a>");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user