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
Ένα πρόγραμμα εργασίας τις προάλλες απαιτούσε την εφαρμογή των αποτελεσμάτων της φόρμας τηλεειδοποίησης. Ο βοηθός μου ήταν πάντα... Βοηθός ετικετών από τον Darrel O'Neil όπως έγραψα για Ορίστε. Παρ' όλα αυτά, για οποιονδήποτε λόγο είναι απλά... Σταμάτησε η εργασία Για μένα. Έτσι, αντί να προσπαθώ να ξεγελάσω μέσα από αυτό που μοιάζει με ένα εγκαταλελειμμένο έργο σε αυτό το σημείο αποφάσισα να φτιάξω ένα μόνος μου.
Ως συνήθως, μπορείς να βρεις την πηγή γι' αυτό. στο GitHub μου
Έχω ένα site δειγμάτων για αυτό το έργο Φιλοξένησε εδώ.
Αυτό έχει δείγματα της εξόδου όπως αυτό:
Για αυτόν τον βοηθό ετικετών είχα μερικές απαιτήσεις:
Dafault.cshtml
Θέα.Στο μέλλον θα προσθέσω την ικανότητα:
Το taghelper είναι τώρα ένα γυαλιστερό νέο πακέτο Nuget ώστε να μπορείτε να το εγκαταστήσετε με την ακόλουθη εντολή:
dotnet add package mostlylucid.pagingtaghelper
Στη συνέχεια, θα προσθέσετε τον βοηθό ετικετών στο δικό σας _ViewImports.cshtml
αρχείο όπως:
@addTagHelper *, mostlylucid.pagingtaghelper
Τότε μπορείτε απλά να αρχίσετε να το χρησιμοποιείτε? Παρέχω μερικά μαθήματα βοηθών που μπορείτε να χρησιμοποιήσετε για να το configre όπως
IPagingModel
Αυτό είναι το "βασικό υλικό" που πρέπει να ξεκινήσεις. Είναι μια απλή διεπαφή που μπορείτε να εφαρμόσετε στο μοντέλο σας για να κάνετε την κλήση να λειτουργήσει. Σημειώστε ότι ViewType
είναι προαιρετική εδώ προεπιλεγεί να TailwindANdDaisy
Αλλά μπορείτε να το ρυθμίσετε Custom
, Plain
ή Bootstrap
αν θέλετε να χρησιμοποιήσετε μια διαφορετική άποψη.
public enum ViewType
{
TailwindANdDaisy,
Custom,
Plain,
Bootstrap
}
Ή μπορείτε ακόμη και να καθορίσετε μια προσαρμοσμένη προβολή με τη χρήση του TagHelper's use-local-view
ιδιοκτησία.
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; }
}
Έχω επίσης θέσει σε εφαρμογή αυτά στο πρόγραμμα για την παροχή μιας βάσης:
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; }
}
Θα καλύψω τη λειτουργία αναζήτησης σε ένα μελλοντικό άρθρο..
Στη συνέχεια, σκέφτηκα πώς θα ήθελα το TagHelper να μοιάζει σε χρήση:
<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>
Εδώ μπορείτε να δείτε ότι ορίζω μερικές παραμέτρους HTMX και το μοντέλο που πρέπει να χρησιμοποιήσετε για την επιγραφή. Επίσης ορίζω τον αριθμό των σελίδων για να εμφανίσετε και τις κεφαλίδες για να στείλετε με το αίτημα (αυτό μου επιτρέπει να χρησιμοποιήσω HTMX για να κατοικήσει τη σελίδα).
Το συστατικό έχει επίσης ένα BUNCH από άλλα στοιχεία config τα οποία θα δουλέψω σε μελλοντικά άρθρα. Όπως μπορείτε να δείτε theerer apos? S μια Πολλή πιθανή διαμόρφωση.
<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>
Το TagHelper είναι αρκετά απλό αλλά έχει ένα μάτσο ιδιότητες που επιτρέπουν στο χρήστη να προσαρμόσετε τη συμπεριφορά (μπορείτε να δείτε αυτό παρακάτω στο προβολή@ title: window ) εκτός από τις ιδιότητες (τις οποίες δεν θα επικολλήσω εδώ για συντομία) ο κώδικας είναι αρκετά απλός:
/// <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);
}
Περιλαμβάνει τα εξής βήματα:
div
; αυτό είναι το δοχείο για τον βομβητή.IPagingModel
και να έχουν το έργο βομβητή χωρίς περαιτέρω διαμόρφωση.PagerViewModel
με τις ιδιότητες που καθορίζονται στις τιμές που έχουμε ή τις προεπιλογές αν δεν παρέχονται.Pager
ΠροβολήComponent με το PagerViewModel
και να ρυθμίσετε το περιεχόμενο εξόδου στο αποτέλεσμα.Πάλι όλα πολύ απλά.
Η θέα για το ViewComponent είναι αρκετά απλή.Είναι απλά ένας βρόχος μέσα από τις σελίδες και μερικές συνδέσεις με την πρώτη, τελευταία, επόμενη και προηγούμενες σελίδες.
@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>
}
Ένα πράγμα που μου έλειπε από τον αρχικό βοηθό ετικέτας ήταν μια πτώση μεγέθους σελίδας.
Αυτή είναι μια απλή λίστα επιλογών, μπορείτε να δείτε Ξεκινώ αρχικά καθορίζοντας fixedSteps
Τα οποία είναι μόνο μερικά σταθερά βήματα που θέλω να χρησιμοποιήσω για την πτώση. Στη συνέχεια, κάνω κύκλους μέσα από αυτά και τα προσθέτουμε στη λίστα. Μια συνήθεια που έχω πάντα είναι να έχω μια επιλογή "όλα" έτσι προσθέτω τα συνολικά στοιχεία στη λίστα αν δεν είναι ήδη εκεί.
@{
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 (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>
}
Μπορείτε να δείτε ότι χρησιμοποιώ προαιρετικά κάποια χαρακτηριστικά HTMX για να μεταβιβάσω το μέγεθος της σελίδας στον διακομιστή και να ενημερώσω τη σελίδα διατηρώντας παράλληλα την τρέχουσα σελίδα και παράμετρο αναζήτησης (αν υπάρχει).
Εpiιpiλέον, εάν αpiοφασίσετε use-htmx=false
ως παράμετρος στον βοηθό ετικετών δεν θα βγάλει αυτά, αλλά αντίθετα θα σας επιτρέψει να χρησιμοποιήσετε κάποια JS I παρέχει ως βοηθός HTML για να ενημερώσετε το μέγεθος της σελίδας.
@Html.PageSizeOnchangeSnippet()
Αυτό είναι ένα απλό σενάριο που θα ενημερώσει το μέγεθος της σελίδας και να ξαναφορτώσει τη σελίδα (σημειώστε ότι αυτό δεν λειτουργεί ακόμα για Plain CSS / Bootstrap όπως εγώ πρέπει να επεξεργαστεί τα ονόματα των ακινήτων κλπ).
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();
}
});
});
Η ενσωμάτωση HTMX είναι αρκετά απλή καθώς το HTMX υποχωρεί σε παιδικά στοιχεία μπορούμε να ορίσουμε τις παραμέτρους HTMX στο μητρικό στοιχείο και θα κληρονομηθούν.
hx-boost="true"
- Αυτό χρησιμοποιεί το ωραίο λειτουργία hx-boost να αναχαιτίσει το γεγονός κλικ και να στείλει το αίτημα μέσω HTMX.Αυτό είναι αρκετά απλό και χρησιμοποιεί DaisyUI, boxicons και Tailwind για να δημιουργήσει μια απλή μέθοδο φόρτωσης.
<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-δείκτης="#loading-modal" στη συνέχεια ορίζει ότι όταν μια αίτηση HTMX εκτελείται θα πρέπει να δείξει στη συνέχεια να κρύψει αυτή τη μέθοδο.
Αυτό είναι το πρώτο μέρος, προφανώς υπάρχουν πολλά περισσότερα να καλύψει και θα το κάνω σε μελλοντικά άρθρα; συμπεριλαμβανομένης της τοποθεσίας του δείγματος, εναλλακτικές απόψεις, τη λειτουργία αναζήτησης και το έθιμο CSS.