-
Notifications
You must be signed in to change notification settings - Fork 156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Advanced pagination #245
Comments
Hi, I needed something like this, and copied the implementation from ngx-pagination and used spartan UI components. And it looks something like this: CleanShot.2024-07-06.at.21.33.27.mp4We can also add it as an example in the docs for other devs to use ofc (but I haven't had any free time) import {
ChangeDetectionStrategy,
Component,
input,
model,
computed,
untracked,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
HlmPaginationContentDirective,
HlmPaginationDirective,
HlmPaginationEllipsisComponent,
HlmPaginationItemDirective,
HlmPaginationLinkDirective,
HlmPaginationNextComponent,
HlmPaginationPreviousComponent,
} from '@spartan-ng/ui-pagination-helm';
import { BrnSelectImports } from '@spartan-ng/ui-select-brain';
import { HlmSelectImports } from '@spartan-ng/ui-select-helm';
@Component({
selector: 'app-numbered-pagination',
template: `
<div class="flex items-center justify-between gap-2 px-4 py-2">
<div class="flex items-center text-nowrap gap-1 text-sm text-gray-600">
<b>{{ totalItems() }}</b> total items | <b>{{ pages().length }}</b>
pages
</div>
<nav hlmPagination>
<ul hlmPaginationContent>
@if (showEdges() && !isFirstPageActive()) {
<li hlmPaginationItem (click)="goToPrevious()">
<hlm-pagination-previous />
</li>
}
@for (page of pages(); track page) {
<li hlmPaginationItem>
@if (page === '...') {
<hlm-pagination-ellipsis />
} @else {
<a
hlmPaginationLink
[isActive]="currentPage() === page"
(click)="currentPage.set(page)">
{{ page }}
</a>
}
</li>
}
@if (showEdges() && !isLastPageActive()) {
<li hlmPaginationItem (click)="goToNext()">
<hlm-pagination-next />
</li>
}
</ul>
</nav>
<!-- Show Page Size selector -->
<brn-select
[(ngModel)]="itemsPerPage"
class="ml-auto"
placeholder="Page size">
<hlm-select-trigger class="w-fit">
<hlm-select-value />
</hlm-select-trigger>
<hlm-select-content>
@for (pageSize of pageSizesWithCurrent(); track pageSize) {
<hlm-option [value]="pageSize">{{ pageSize }} / page</hlm-option>
}
</hlm-select-content>
</brn-select>
</div>
`,
standalone: true,
imports: [
FormsModule,
HlmPaginationDirective,
HlmPaginationContentDirective,
HlmPaginationItemDirective,
HlmPaginationPreviousComponent,
HlmPaginationNextComponent,
HlmPaginationLinkDirective,
HlmPaginationEllipsisComponent,
BrnSelectImports,
HlmSelectImports,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NumberedPaginationComponent {
/**
* The current (active) page.
*/
currentPage = model.required<number>();
/**
* The number of items per paginated page.
*/
itemsPerPage = model.required<number>();
/**
* The total number of items in the collection. Only useful when
* doing server-side paging, where the collection size is limited
* to a single page returned by the server API.
*/
totalItems = input.required<number>();
/**
* The number of page links to show.
*/
maxSize = input(7);
/**
* Show the first and last page buttons.
*/
showEdges = input(true);
/**
* The page sizes to show.
* Defaults to [10, 20, 50, 100]
*/
pageSizes = input([10, 20, 50, 100]);
protected pageSizesWithCurrent = computed(() => {
const pageSizes = this.pageSizes();
return pageSizes.includes(this.itemsPerPage())
? pageSizes // if current page size is included, return the same array
: [...pageSizes, this.itemsPerPage()].sort((a, b) => a - b); // otherwise, add current page size and sort the array
});
protected isFirstPageActive = computed(() => this.currentPage() === 1);
protected isLastPageActive = computed(
() => this.currentPage() === this.lastPageNumber()
);
protected goToPrevious() {
this.currentPage.set(this.currentPage() - 1);
}
protected goToNext() {
this.currentPage.set(this.currentPage() + 1);
}
protected goToFirst() {
this.currentPage.set(1);
}
protected goToLast() {
this.currentPage.set(this.lastPageNumber());
}
protected lastPageNumber = computed(() => {
if (this.totalItems() < 1) {
// when there are 0 or fewer (an error case) items, there are no "pages" as such,
// but it makes sense to consider a single, empty page as the last page.
return 1;
}
return Math.ceil(this.totalItems() / this.itemsPerPage());
});
protected pages = computed(() => {
const correctedCurrentPage = outOfBoundCorrection(
this.totalItems(),
this.itemsPerPage(),
this.currentPage()
);
if (correctedCurrentPage !== this.currentPage()) {
// update the current page
untracked(() => this.currentPage.set(correctedCurrentPage));
}
return createPageArray(
correctedCurrentPage,
this.itemsPerPage(),
this.totalItems(),
this.maxSize()
);
});
}
type Page = number | '...';
/**
* Checks that the instance.currentPage property is within bounds for the current page range.
* If not, return a correct value for currentPage, or the current value if OK.
*
* Copied from 'ngx-pagination' package
*/
function outOfBoundCorrection(
totalItems: number,
itemsPerPage: number,
currentPage: number
): number {
const totalPages = Math.ceil(totalItems / itemsPerPage);
if (totalPages < currentPage && 0 < totalPages) {
return totalPages;
} else if (currentPage < 1) {
return 1;
}
return currentPage;
}
/**
* Returns an array of Page objects to use in the pagination controls.
*
* Copied from 'ngx-pagination' package
*/
function createPageArray(
currentPage: number,
itemsPerPage: number,
totalItems: number,
paginationRange: number
): Page[] {
// paginationRange could be a string if passed from attribute, so cast to number.
paginationRange = +paginationRange;
const pages: Page[] = [];
// Return 1 as default page number
// Make sense to show 1 instead of empty when there are no items
const totalPages = Math.max(Math.ceil(totalItems / itemsPerPage), 1);
const halfWay = Math.ceil(paginationRange / 2);
const isStart = currentPage <= halfWay;
const isEnd = totalPages - halfWay < currentPage;
const isMiddle = !isStart && !isEnd;
const ellipsesNeeded = paginationRange < totalPages;
let i = 1;
while (i <= totalPages && i <= paginationRange) {
let label: number | '...';
const pageNumber = calculatePageNumber(
i,
currentPage,
paginationRange,
totalPages
);
const openingEllipsesNeeded = i === 2 && (isMiddle || isEnd);
const closingEllipsesNeeded =
i === paginationRange - 1 && (isMiddle || isStart);
if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
label = '...';
} else {
label = pageNumber;
}
pages.push(label);
i++;
}
return pages;
}
/**
* Given the position in the sequence of pagination links [i],
* figure out what page number corresponds to that position.
*
* Copied from 'ngx-pagination' package
*/
function calculatePageNumber(
i: number,
currentPage: number,
paginationRange: number,
totalPages: number
) {
const halfWay = Math.ceil(paginationRange / 2);
if (i === paginationRange) {
return totalPages;
} else if (i === 1) {
return i;
} else if (paginationRange < totalPages) {
if (totalPages - halfWay < currentPage) {
return totalPages - paginationRange + i;
} else if (halfWay < currentPage) {
return currentPage - halfWay + i;
} else {
return i;
}
} else {
return i;
}
} |
I think adding it to the docs would be a great idea, I could add it myself (the example above) if you guys are fine with it. Also should we have just one example, or we could expand to having multiple examples? |
@DevWedeloper @eneajaho We can also add it as part of hlm so people can copy it into their project using the CLI. Thoughts? |
Which scope/s are relevant/related to the feature request?
pagination
Information
I believe it would be beneficial to have a pagination component similar to that of ng-zorros.
Describe any alternatives/workarounds you're currently using
Implementing a similar pagination logic manually.
I would be willing to submit a PR to fix this issue
The text was updated successfully, but these errors were encountered: