NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Tuesday, 11 March 2025
//Less than a minute
Een werkproject de andere dag vereist het implementeren van paging formulier resultaten. Mijn ga-naar paging tag helper is altijd de pagination Tag Helper door Darrel O'Neill zoals ik schreef over Hier. Maar om welke reden dan ook... het is gewoon... werkt niet meer Voor mij. Dus in plaats van te proberen te puzzelen door wat eruit ziet als een verlaten project op dit moment besloot ik er zelf een te bouwen.
Zoals gewoonlijk kunt u de bron voor deze op mijn GitHub
Ik heb een voorbeeld site voor dit project Hier gehost
Dit heeft samples van de output als volgt:
Voor deze tag helper had ik een paar eisen:
Dafault.cshtml
uitzicht.In de toekomst voeg ik de mogelijkheid toe aan:
De taghelper is nu een glanzend nieuw Nuget pakket, zodat u het kunt installeren met het volgende commando:
dotnet add package mostlylucid.pagingtaghelper
Je zou dan de tag helper toevoegen aan je _ViewImports.cshtml
bestand als volgt:
@addTagHelper *, mostlylucid.pagingtaghelper
Dan kunt u gewoon beginnen met het te gebruiken; Ik geef een aantal helper klassen die u kunt gebruiken om het te configureren zoals
IPagingModel
Dit is het 'basismateriaal' dat je nodig hebt om aan de slag te gaan. Het is een eenvoudige interface die u kunt implementeren op uw model om de paging werken. Merk op dat ViewType
is optioneel hier is het standaard om TailwindANdDaisy
maar je kunt het instellen op Custom
, Plain
of Bootstrap
als u een andere weergave wilt gebruiken.
public enum ViewType
{
TailwindANdDaisy,
Custom,
Plain,
Bootstrap
}
OF u kunt zelfs een aangepaste weergave opgeven met behulp van de TagHelper's use-local-view
eigendom.
namespace mostlylucid.pagingtaghelper.Models;
public interface IPagingModel
{
public int Page { get; set; }
public int TotalItems { get; set; }
public int PageSize { get; set; }
public ViewType ViewType { get; set; }
public string LinkUrl { get; set; }
}
namespace mostlylucid.pagingtaghelper.Models;
public interface IPagingSearchModel : IPagingModel
{
public string? SearchTerm { get; set; }
}
Ik heb deze ook in het project geïmplementeerd om een basis te bieden:
public abstract class BasePagerModel : IPagingModel
{
public int Page { get; set; } = 1;
public int TotalItems { get; set; } = 0;
public int PageSize { get; set; } = 10;
public ViewType ViewType { get; set; } = ViewType.TailwindANdDaisy;
public string LinkUrl { get; set; } = "";
}
public abstract class BasePagerSearchMdodel : BasePagerModel, IPagingSearchModel
{
public string? SearchTerm { get; set; }
}
Ik zal de zoekfunctie in een volgend artikel behandelen..
Toen bedacht ik hoe ik zou willen dat de TagHelper eruit zou zien als in gebruik:
<paging
x-ref="pager"
hx-boost="true"
hx-indicator="#loading-modal"
hx-target="#user-list "
hx-swap="show:none"
model="Model"
pages-to-display="10"
hx-headers='{"pagerequest": "true"}'>
</paging>
Hier zie je dat ik een paar HTMX parameters en het model voor de paging heb ingesteld. Ik heb ook het aantal pagina's ingesteld om te tonen en de headers om te verzenden met het verzoek (dit stelt me in staat om HTMX te gebruiken om de pagina te vullen).
Het onderdeel heeft ook een BUNCH van andere config elementen die ik zal doorwerken in toekomstige artikelen. Zoals je kunt zien is er een hoop mogelijke configuratie.
<paging css-class=""
first-last-navigation=""
first-page-text=""
next-page-aria-label=""
next-page-text=""
page=""
pages-to-display=""
page-size=""
previous-page-text=""
search-term=""
skip-forward-back-navigation=""
skip-back-text=""
skip-forward-text="true"
total-items=""
link-url=""
last-page-text=""
show-pagesize=""
use-htmx=""
use-local-view=""
view-type="Bootstrap"
htmx-target=""
id=""
></paging>
De TagHelper is vrij eenvoudig maar heeft een heleboel eigenschappen die de gebruiker in staat stellen om het gedrag aan te passen (dit zie je hieronder in de weergave ) afgezien van de eigenschappen (die ik hier niet voor kortheid zal plakken) is de code vrij eenvoudig:
/// <summary>
/// Processes the tag helper to generate the pagination component.
/// </summary>
/// <param name="context">The tag helper context.</param>
/// <param name="output">The tag helper output.</param>
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
//Remove all the properties that are not needed for the rendered content.
output.Attributes.RemoveAll("page");
output.Attributes.RemoveAll("link-url");
output.Attributes.RemoveAll("page-size");
output.Attributes.RemoveAll("total-items");
output.Attributes.RemoveAll("pages-to-display");
output.Attributes.RemoveAll("css-class");
output.Attributes.RemoveAll("first-page-text");
output.Attributes.RemoveAll("previous-page-text");
output.Attributes.RemoveAll("skip-back-text");
output.Attributes.RemoveAll("skip-forward-text");
output.Attributes.RemoveAll("next-page-text");
output.Attributes.RemoveAll("next-page-aria-label");
output.Attributes.RemoveAll("last-page-text");
output.Attributes.RemoveAll("first-last-navigation");
output.Attributes.RemoveAll("skip-forward-back-navigation");
output.Attributes.RemoveAll("model");
output.Attributes.RemoveAll("show-pagesize");
output.Attributes.RemoveAll("pagingmodel");
output.Attributes.RemoveAll("use-local-view");
var pagerId = PagerId ?? $"pager-{Guid.NewGuid():N}";
var linkUrl = LinkUrl ?? ViewContext.HttpContext.Request.Path;
PageSize = Model?.PageSize ?? PageSize ?? 10;
Page = Model?.Page ?? Page ?? 1;
ViewType = Model?.ViewType ?? ViewType;
TotalItems = Model?.TotalItems ?? TotalItems ?? 0;
if(Model is IPagingSearchModel searchModel)
SearchTerm = searchModel?.SearchTerm ?? SearchTerm ?? "";
output.Attributes.SetAttribute("id", pagerId);
var viewComponentHelper = (IViewComponentHelper)ViewContext.HttpContext.RequestServices.GetService(typeof(IViewComponentHelper))!;
((IViewContextAware)viewComponentHelper).Contextualize(ViewContext);
var pagerModel = PagerModel ?? new PagerViewModel()
{
ViewType = ViewType,
UseLocalView = UseLocalView,
UseHtmx = UseHtmx,
PagerId = pagerId,
SearchTerm = SearchTerm,
ShowPageSize = ShowPageSize,
Model = Model,
LinkUrl = linkUrl,
Page = Page,
PageSize = PageSize,
TotalItems = TotalItems,
PagesToDisplay = PagesToDisplay,
CssClass = CssClass,
FirstPageText = FirstPageText,
PreviousPageText = PreviousPageText,
SkipBackText = SkipBackText,
SkipForwardText = SkipForwardText,
NextPageText = NextPageText,
NextPageAriaLabel = NextPageAriaLabel,
LastPageText = LastPageText,
FirstLastNavigation = FirstLastNavigation,
SkipForwardBackNavigation = SkipForwardBackNavigation,
HtmxTarget = HtmxTarget,
};
var result = await viewComponentHelper.InvokeAsync("Pager", pagerModel);
output.Content.SetHtmlContent(result);
}
Het omvat de volgende stappen:
div
; dit is de container voor de pager.IPagingModel
en laat de pager werken zonder verdere configuratie.PagerViewModel
met de eigenschappen die zijn ingesteld op de waarden die we hebben of de standaardwaarden indien niet verstrekt.Pager
BeeldComponent met de PagerViewModel
en stel de output inhoud in op het resultaat.Alweer vrij eenvoudig.
Het uitzicht voor de ViewComponent is vrij eenvoudig; het is gewoon een lus door de pagina's en een paar links naar de eerste, laatste, volgende en vorige pagina's.
@model mostlylucid.pagingtaghelper.Components.PagerViewModel
@{
var totalPages = (int)Math.Ceiling((double)Model.TotalItems! / (double)Model.PageSize!);
var pageSizes = new List<int>();
if (Model.ShowPageSize)
{
// Build a dynamic list of page sizes.
// Fixed steps as a starting point.
int[] fixedSteps = { 10, 25, 50, 75, 100, 125, 150, 200, 250, 500, 1000 };
// Add only those fixed steps that are less than or equal to TotalItems.
foreach (var step in fixedSteps)
{
if (step <= Model.TotalItems)
{
pageSizes.Add(step);
}
}
// If TotalItems is greater than the largest fixed step,
// add additional steps by doubling until reaching TotalItems.
if (Model.TotalItems > fixedSteps.Last())
{
int next = fixedSteps.Last();
while (next < Model.TotalItems)
{
next *= 2;
// Only add if it doesn't exceed TotalItems.
if (next < Model.TotalItems)
{
pageSizes.Add(next);
}
}
// Always include the actual TotalItems as the maximum option.
if (!pageSizes.Contains(Model.TotalItems.Value))
{
pageSizes.Add(Model.TotalItems.Value);
}
}
}
}
@if (totalPages > 1)
{
<div class="@Model.CssClass flex items-center justify-center" id="pager-container">
@if (Model.ShowPageSize)
{
var pagerId = Model.PagerId;
var htmxAttributes = Model.UseHtmx
? $"hx-get=\"{Model.LinkUrl}\" hx-trigger=\"change\" hx-include=\"#{pagerId} [name='page'], #{pagerId} [name='search']\" hx-push-url=\"true\""
: "";
<!-- Preserve current page -->
<input type="hidden" name="page" value="@Model.Page"/>
<input type="hidden" name="search" value="@Model.SearchTerm"/>
<input type="hidden" class="useHtmx" value="@Model.UseHtmx.ToString().ToLowerInvariant()"/>
if (!Model.UseHtmx)
{
<input type="hidden" class="linkUrl" value="@Model.LinkUrl"/>
}
<!-- Page size select with label -->
<div class="flex items-center mr-8">
<label for="pageSize-@pagerId" class="text-sm text-gray-600 mr-2">Page size:</label>
<select id="pageSize-@pagerId"
name="pageSize"
class="border rounded select select-primary select-sm pt-0 mt-0 min-w-[80px] pr-4"
@Html.Raw(htmxAttributes)>
@foreach (var option in pageSizes.ToList())
{
var optionString = option.ToString();
if (option == Model.PageSize)
{
<option value="@optionString" selected="selected">@optionString</option>
}
else
{
<option value="@optionString">@optionString</option>
}
}
</select>
</div>
}
@* "First" page link *@
@if (Model.FirstLastNavigation && Model.Page > 1)
{
var href = $"{Model.LinkUrl}?page=1&pageSize={Model.PageSize}";
if (!string.IsNullOrEmpty(Model.SearchTerm))
{
href += $"&search={Model.SearchTerm}";
}
<a class="btn btn-sm"
href="@href">
@Model.FirstPageText
</a>
}
@* "Previous" page link *@
@if (Model.Page > 1)
{
var href = $"{Model.LinkUrl}?page={Model.Page - 1}&pageSize={Model.PageSize}";
if (!string.IsNullOrEmpty(Model.SearchTerm))
{
href += $"&search={Model.SearchTerm}";
}
<a class="btn btn-sm"
href="@href">
@Model.PreviousPageText
</a>
}
@* Optional skip back indicator *@
@if (Model.SkipForwardBackNavigation && Model.Page > Model.PagesToDisplay)
{
<a class="btn btn-sm btn-disabled">
@Model.SkipBackText
</a>
}
@* Determine visible page range *@
@{
int halfDisplay = Model.PagesToDisplay / 2;
int startPage = Math.Max(1, Model.Page.Value - halfDisplay);
int endPage = Math.Min(totalPages, startPage + Model.PagesToDisplay - 1);
startPage = Math.Max(1, endPage - Model.PagesToDisplay + 1);
}
@for (int i = startPage; i <= endPage; i++)
{
var href = $"{Model.LinkUrl}?page={i}&pageSize={Model.PageSize}";
if (!string.IsNullOrEmpty(Model.SearchTerm))
{
href += $"&search={Model.SearchTerm}";
}
<a data-page="@i" class="btn btn-sm mr-2 @(i == Model.Page ? "btn-active" : "")"
href="@href">
@i
</a>
}
@* Optional skip forward indicator *@
@if (Model.SkipForwardBackNavigation && Model.Page < totalPages - Model.PagesToDisplay + 1)
{
<a class="btn btn-sm btn-disabled mr-2">
@Model.SkipForwardText
</a>
}
@* "Next" page link *@
@if (Model.Page < totalPages)
{
var href = $"{Model.LinkUrl}?page={Model.Page + 1}&pageSize={Model.PageSize}";
if (!string.IsNullOrEmpty(Model.SearchTerm))
{
href += $"&search={Model.SearchTerm}";
}
<a class="btn btn-sm mr-2"
href="@href"
aria-label="@Model.NextPageAriaLabel">
@Model.NextPageText
</a>
}
@* "Last" page link *@
@if (Model.FirstLastNavigation && Model.Page < totalPages)
{
var href = $"{Model.LinkUrl}?page={totalPages}&pageSize={Model.PageSize}";
if (!string.IsNullOrEmpty(Model.SearchTerm))
{
href += $"&search={Model.SearchTerm}";
}
<a class="btn btn-sm"
href="@href">
@Model.LastPageText
</a>
}
<!-- Page info text with left margin for separation -->
<div class="text-sm text-neutral-500 ml-8">
Page @Model.Page of @totalPages (Total items: @Model.TotalItems)
</div>
</div>
}
Een ding wat ik miste in de originele tag helper was een paginagrootte dropdown.
Dit is een eenvoudige selectielijst, je kunt zien dat ik eerst begin door te definiëren fixedSteps
Dat zijn slechts een paar vaste stappen die ik wil gebruiken voor de dropdown. Ik loop ze dan door en voeg ze toe aan de lijst. Een gewoonte die ik altijd heb is het hebben van een 'all' optie, dus ik voeg de totale items aan de lijst als het er nog niet is.
@{
var totalPages = (int)Math.Ceiling((double)Model.TotalItems! / (double)Model.PageSize!);
var pageSizes = new List<int>();
if (Model.ShowPageSize)
{
// Build a dynamic list of page sizes.
// Fixed steps as a starting point.
int[] fixedSteps = { 10, 25, 50, 75, 100, 125, 150, 200, 250, 500, 1000 };
// Add only those fixed steps that are less than or equal to TotalItems.
foreach (var step in fixedSteps)
{
if (step <= Model.TotalItems)
{
pageSizes.Add(step);
}
}
// If TotalItems is greater than the largest fixed step,
// add additional steps by doubling until reaching TotalItems.
if (Model.TotalItems > fixedSteps.Last())
{
int next = fixedSteps.Last();
while (next < Model.TotalItems)
{
next *= 2;
// Only add if it doesn't exceed TotalItems.
if (next < Model.TotalItems)
{
pageSizes.Add(next);
}
}
// Always include the actual TotalItems as the maximum option.
if (!pageSizes.Contains(Model.TotalItems.Value))
{
pageSizes.Add(Model.TotalItems.Value);
}
}
}
}
Ik maak dit dan weer terug naar de pagina
@if (Model.ShowPageSize)
{
var pagerId = Model.PagerId;
var htmxAttributes = Model.UseHtmx
? $"hx-get=\"{Model.LinkUrl}\" hx-trigger=\"change\" hx-include=\"#{pagerId} [name='page'], #{pagerId} [name='search']\" hx-push-url=\"true\""
: "";
<!-- Preserve current page -->
<input type="hidden" name="page" value="@Model.Page"/>
<input type="hidden" name="search" value="@Model.SearchTerm"/>
<input type="hidden" class="useHtmx" value="@Model.UseHtmx.ToString().ToLowerInvariant()"/>
if (!Model.UseHtmx)
{
<input type="hidden" class="linkUrl" value="@Model.LinkUrl"/>
}
<!-- Page size select with label -->
<div class="flex items-center mr-8">
<label for="pageSize-@pagerId" class="text-sm text-gray-600 mr-2">Page size:</label>
<select id="pageSize-@pagerId"
name="pageSize"
class="border rounded select select-primary select-sm pt-0 mt-0 min-w-[80px] pr-4"
@Html.Raw(htmxAttributes)>
@foreach (var option in pageSizes.ToList())
{
var optionString = option.ToString();
if (option == Model.PageSize)
{
<option value="@optionString" selected="selected">@optionString</option>
}
else
{
<option value="@optionString">@optionString</option>
}
}
</select>
</div>
}
U kunt zien dat ik optioneel enkele HTMX-attributen gebruik om de paginagrootte door te geven aan de server en de pagina bij te werken met behoud van de huidige pagina en zoekparameter (indien aanwezig).
Bovendien als u specif use-htmx=false
als een parameter op de tag helper zal het deze niet uitvoeren, maar in plaats daarvan zal u toestaan om een aantal JS die ik als een HTML Helper te gebruiken om de paginagrootte bij te werken.
@Html.PageSizeOnchangeSnippet()
Dit is een eenvoudig script dat de paginagrootte zal bijwerken en de pagina opnieuw zal laden (let op: dit werkt nog niet voor Plain CSS / Bootstrap omdat ik de namen van de eigendommen moet uitwerken etc).
document.addEventListener("DOMContentLoaded", function () {
document.body.addEventListener("change", function (event) {
const selectElement = event.target.closest("#pager-container select[name='pageSize']");
if (!selectElement) return;
const pagerContainer = selectElement.closest("#pager-container");
const useHtmxInput = pagerContainer.querySelector("input.useHtmx");
const useHtmx = useHtmxInput ? useHtmxInput.value === "true" : true; // default to true
if (!useHtmx) {
const pageInput = pagerContainer.querySelector("[name='page']");
const searchInput = pagerContainer.querySelector("[name='search']");
const page = pageInput ? pageInput.value : "1";
const search = searchInput ? searchInput.value : "";
const pageSize = selectElement.value;
const linkUrl = pagerContainer.querySelector("input.linkUrl").value ?? "";
const url = new URL(linkUrl, window.location.origin);
url.searchParams.set("page", page);
url.searchParams.set("pageSize", pageSize);
if (search) {
url.searchParams.set("search", search);
}
window.location.href = url.toString();
}
});
});
De HTMX integratie is vrij eenvoudig omdat HTMX cascading tot kind elementen kunnen we de HTMX parameters op het ouder element en ze zullen worden geërfd.
hx-boost="true"
- dit maakt gebruik van de handige hx-boost functie om het klik-evenement te onderscheppen en het verzoek te verzenden via HTMX.Dit is vrij eenvoudig en maakt gebruik van DaisyUI, boxiconen en Tailwind om een eenvoudige laadmodus te creëren.
<div id="loading-modal" class="modal htmx-indicator">
<div class="modal-box flex flex-col items-center justify-center">
<h2 class="text-lg font-semibold">Loading...</h2>
<i class="bx bx-loader bx-spin text-3xl mt-2"></i>
</div>
</div>
hx-indicator="#loading-modal" geeft vervolgens aan dat wanneer een HTMX-verzoek wordt uitgevoerd het shoould tonen dan deze modale verbergen.
Dus dat is deel 1, er is duidelijk een LOT meer te behandelen en ik zal in toekomstige artikelen; met inbegrip van de sample site, alternatieve weergaven, de zoekfunctie en de aangepaste CSS.