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
وقد استلزم مشروع عمل في اليوم الآخر تنفيذ نتائج استمارة الاستدعاء. دائماً ما كان مساعدي دائماً مُرفق مُلْخَزَجَجَزًا مُحَدَّد المُسَلِف كما كتبت عن هنا هنا لأي سبب من الأسباب أنه فقط متوقف عن العمل من أجلي من أجلي فبدلاً من محاولة فهم ما يبدو كمشروع مهجور في هذه المرحلة قررت أن أبني واحداً بنفسي.
كالعادة يمكنك الحصول على المصدر لهذا على حسابي
لدي موقع عينة لهذا المشروع يستضيف هنا
هذا يحتوي على عينات من مخرجات مثل هذا:
لهذا مساعد بطاقة كان لدي بعض المتطلبات:
Dafault.cshtml
وجهة نظر.في المستقبل سأضيف القدرة إلى:
الـ شارة هو a جديد nuget حزمة IF الإيطالية مع التالي أمر:
dotnet add package mostlylucid.pagingtaghelper
ثم تقوم بعد ذلك بإضافة مساعد بطاقة إلى _ViewImports.cshtml
ملفّ مثل:
@addTagHelper *, mostlylucid.pagingtaghelper
ثم يمكنك فقط البدء باستخدامه، انا اوفر بعض فئات المساعدين التي يمكنك استخدامها لضبطها مثل
IPagingModel
هذه هي "الأشياء الأساسية" التي تحتاجها للبدء. إنّها واجهة بسيطة يمكنك تنفيذها على نموذجك لتُشغّل النشوء. ملاحظة ملاحظة ViewType
هو اختياري هنا هو افتراضي إلى TailwindANdDaisy
« ولكن يمكنك أن تجعلها » أي خلقها « إلى صرها » أي خلقها. Custom
, Plain
أو Bootstrap
إذا كنت تريد استخدام وجهة نظر مختلفة.
public enum ViewType
{
TailwindANdDaisy,
Custom,
Plain,
Bootstrap
}
لا يمكنك ذلك في هذه الحالة تحديد مخصص مُرفق باستخدام مُعلام 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; }
}
سأغطي وظيفة البحث في مقال مستقبلي
بعد ذلك عملت على ما أريد أن يبدو الـ Tagfilder كما لو كان مستخدماً:
<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 لاستبدال الصفحة).
كما أن المكوّن يحتوي على مجموعة من العناصر الأخرى التي سأعمل عليها في المقالات المقبلة. كما ترون فإن هناك الكثير من الإعدادات الممكنة.
<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>
الـ مُوازِل هذا هو بسيط جداً لكن لديه مجموعة من الخصائص التي تمكّن المستخدم من تخصيص هذا السلوك (يُمْكِنك أن ترى هذا أسفله في الـ الرأي بعيداً عن الخصائص (التي لن ألصقها هنا للإيجاز) الشفرة واضحة إلى حد ما:
/// <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
مع خصائص set إلى القيم لدينا أو افتراضي IF ليس.Pager
مع PagerViewModel
ضبط مخرجات المحتوى إلى النتيجة.مرة أخرى كل شيء بسيط جدا.
وجهة النظر لمكون العرض بسيطة جداً، انها مجرد حلقة من خلال الصفحات و القليل من الوصلات للصفحات الأولى والأخيرة والتالية والسابقة.
@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 بشكل اختياري لتمرير حجم الصفحة إلى الخادم وتحديث الصفحة مع الاحتفاظ بالصفحة الحالية ومعامل البحث (إن كان هناك).
إذا كنت مُنَظِف use-htmx=false
معلمة على مساعد بطاقة هو لَنْ يُخرجَ هذه لكن بدلاً مِنْ ذلك سَيَسْمحُ لك بإستعمال البعضِ JS I يُزوّدُ كمساعد HTML لتحديث حجم الصفحةِ.
@Html.PageSizeOnchangeSnippet()
هذا نص بسيط سيقوم بتحديث حجم الصفحة وإعادة تحميل الصفحة (لاحظ أن هذا لا يعمل حتى الآن لـ بسيط CSS/ Botstrap كما أحتاج إلى العمل على أسماء الممتلكات وما إلى ذلك).
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"
- هذا استخدام النفعية (حجج)- (ميزة) لاعتراض الحدث النقري وإرسال الطلب عبر HTMX.هذا بسيط جداً و يستخدم ديزي أويل و الكوبيكونات و تايل ويند لخلق نمط تحميل بسيط
<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="# التحميل- واسطة" ثم تحدد أنه عندما يتم تنفيذ طلب HTMX فإنه يظهر shoold ثم إخفاء هذا الواسطة.
اذاً هذا هو الجزء 1، من الواضح ان هناك الكثير مما يمكن تغطيته وسوف اقوم به في المقالات المقبلة، بما في ذلك موقع العينة، وجهات النظر البديلة،