Skip to content

Commit

Permalink
feat: allow providing a Custom Pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Oct 11, 2024
1 parent 26fa5aa commit 4a2bfc8
Show file tree
Hide file tree
Showing 17 changed files with 625 additions and 41 deletions.
1 change: 1 addition & 0 deletions docs/TOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
* [Grid State & Presets](grid-functionalities/grid-state-preset.md)
* [Grouping & Aggregators](grid-functionalities/grouping-aggregators.md)
* [Header Menu & Header Buttons](grid-functionalities/header-menu-header-buttons.md)
* [Pagination](grid-functionalities/pagination.md)
* [Infinite Scroll](grid-functionalities/infinite-scroll.md)
* [Pinning (frozen) of Columns/Rows](grid-functionalities/frozen-columns-rows.md)
* [Row Selection](grid-functionalities/Row-Selection.md)
Expand Down
57 changes: 57 additions & 0 deletions docs/grid-functionalities/pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
### Introduction
The project has a built-in Pagination Component but in some cases, users might want to provide their own Custom Pagination Component.

### Demo
[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example30) / [Demo Component](https://github.com/ghiscoding/slickgrid-universal/blob/master/examples/webpack-demo-vanilla-bundle/src/examples/example30.ts)

#### Custom Pagination
When providing a custom pagination component as a `customPaginationComponent`, that class will be instantiated instead of the regular `SlickPaginationComponent`.

> **Note** Your Custom Pagination must `implements BasePaginationComponent` so that the internal instantiation work as intended.
##### Component

```ts
import { CustomPager } from './custom-pager';

export class GridBasicComponent {
columnDefinitions: Column[];
gridOptions: GridOption;
dataset: any[];

attached(): void {
// your columns definition
this.columnDefinitions = [];

this.gridOptions = {
// enable pagination and provide a `customPaginationComponent`
enablePagination: true,
customPaginationComponent: CustomPager,

// provide any of the regular pagination options like usual
pagination: {
pageSize: this.pageSize
},
}
}
}
```

###### Custom Pagination Component
```ts
import type { BasePaginationComponent, PaginationService, PubSubService, SlickGrid } from '@slickgrid-universal/common';

export class CustomPager implements BasePaginationComponent {
constructor(protected readonly grid: SlickGrid, protected readonly paginationService: PaginationService, protected readonly pubSubService: PubSubService) {
// ...
}

dispose() {
// ...
}

render(containerElm: HTMLElement) {
// ...
}
}
```
2 changes: 2 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/app-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import Example26 from './examples/example26';
import Example27 from './examples/example27';
import Example28 from './examples/example28';
import Example29 from './examples/example29';
import Example30 from './examples/example30';

export class AppRouting {
constructor(private config: RouterConfig) {
Expand Down Expand Up @@ -65,6 +66,7 @@ export class AppRouting {
{ route: 'example27', name: 'example27', view: './examples/example27.html', viewModel: Example27, title: 'Example27', },
{ route: 'example28', name: 'example28', view: './examples/example28.html', viewModel: Example28, title: 'Example28', },
{ route: 'example29', name: 'example29', view: './examples/example29.html', viewModel: Example29, title: 'Example29', },
{ route: 'example30', name: 'example30', view: './examples/example30.html', viewModel: Example30, title: 'Example30', },
{ route: '', redirect: 'example01' },
{ route: '**', redirect: 'example01' }
];
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ <h4 class="is-size-4 has-text-white">Slickgrid-Universal</h4>
<a class="navbar-item" onclick.delegate="loadRoute('example29')">
Example29 - Drag & Drop
</a>
<a class="navbar-item" onclick.delegate="loadRoute('example30')">
Example30 - Custom Pagination
</a>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@use 'sass:color';

.custom-pagination {
display: flex;
justify-content: flex-end;
margin: 10px;
font-size: 13px;

.custom-pagination-settings {
display: inline-flex;
align-items: center;
margin-right: 30px;
}

.custom-pagination-nav {
display: flex;
align-items: center;

.page-item {
display: flex;
width: 26px;
justify-content: center;
margin: 0;
&.disabled .page-link {
color: rgb(180, 179, 179);
}
}

.page-number {
padding: 0 5px;
.page-number {
display: inline-flex;
justify-content: center;
width: 20px;
}
}

nav {
ul {
display: flex;

.page-link {
color: rgb(0, 168, 53);
&:hover {
color: color.adjust(rgb(0, 168, 53), $lightness: -12%);
}
}
}
}
}
}
192 changes: 192 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/examples/example30-pager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { BindingEventService, BindingHelper } from '@slickgrid-universal/binding';
import type { BasePaginationComponent, PaginationService, PubSubService, PaginationMetadata, SlickGrid, Subscription } from '@slickgrid-universal/common';

import './example30-pager.scss';

export class CustomPager implements BasePaginationComponent {
protected _bindingHelper: BindingHelper;
protected _bindingEventService: BindingEventService;
protected _paginationElement!: HTMLDivElement;
protected _subscriptions: Subscription[] = [];
protected _gridContainerElm?: HTMLElement;
currentPagination: PaginationMetadata;
firstButtonClasses = 'li page-item seek-first';
prevButtonClasses = 'li page-item seek-prev';
lastButtonClasses = 'li page-item seek-end';
nextButtonClasses = 'li page-item seek-next';

constructor(protected readonly grid: SlickGrid, protected readonly paginationService: PaginationService, protected readonly pubSubService: PubSubService) {
this._bindingHelper = new BindingHelper();
this._bindingEventService = new BindingEventService();

// Anytime the pagination is initialized or has changes,
// we'll copy the data into a local object so that we can add binding to this local object
this._subscriptions.push(
this.pubSubService.subscribe<PaginationMetadata>('onPaginationRefreshed', paginationChanges => {
this.currentPagination.dataFrom = paginationChanges.dataFrom;
this.currentPagination.dataTo = paginationChanges.dataTo;
this.currentPagination.pageCount = paginationChanges.pageCount;
this.currentPagination.pageNumber = paginationChanges.pageNumber;
this.currentPagination.pageSize = paginationChanges.pageSize;
this.currentPagination.pageSizes = paginationChanges.pageSizes;
this.currentPagination.totalItems = paginationChanges.totalItems;

this.updatePageButtonsUsability();
})
);
}

dispose() {
this.pubSubService.unsubscribeAll(this._subscriptions);
this.disposeElement();
}

disposeElement() {
this._bindingEventService.unbindAll();
this._bindingHelper.dispose();
this._paginationElement.remove();
}

render(containerElm: HTMLElement, position: 'top' | 'bottom' = 'top') {
this._gridContainerElm = containerElm;
this.currentPagination = this.paginationService.getFullPagination();
this._paginationElement = document.createElement('div');
this._paginationElement.id = 'pager';
this._paginationElement.className = `pagination-container pager ${this.grid.getUID()}`;
this._paginationElement.style.width = '100%';
this._paginationElement.innerHTML =
`<div class="custom-pagination">
<span class="custom-pagination-settings">
<span class="custom-pagination-count">
<span class="page-info-from-to">
<span class="item-from" aria-label="Page Item From" data-test="item-from">
${this.currentPagination.dataFrom}
</span>-
<span class="item-to" aria-label="Page Item To" data-test="item-to">
${this.currentPagination.dataTo}
</span>
of
</span>
<span class="page-info-total-items">
<span class="total-items" aria-label="Total Items" data-test="total-items">${this.currentPagination.totalItems}</span>
<span class="text-items"> items</span>
</span>
</span>
</span>
<div class="custom-pagination-nav">
<nav aria-label="Page navigation">
<ul class="custom-pagination-ul">
<li class="${this.firstButtonClasses}">
<a class="page-link mdi mdi-page-first icon-seek-first mdi-22px" aria-label="First Page" role="button"></a>
</li>
<li class="${this.prevButtonClasses}">
<a class="page-link icon-seek-prev mdi mdi-chevron-down mdi-22px mdi-rotate-90" aria-label="Previous Page" role="button"></a>
</li>
</ul>
</nav>
<div class="page-number">
<span class="text-page">Page</span>
<span class="page-number" aria-label="Page Number" data-test="page-number-label">${this.currentPagination.pageNumber}</span>
of
<span class="page-count" data-test="page-count">${this.currentPagination.pageCount}</span>
</div>
<nav aria-label="Page navigation">
<ul class="custom-pagination-ul">
<li class="${this.nextButtonClasses}">
<a class="page-link icon-seek-next mdi mdi-chevron-down mdi-22px mdi-rotate-270" aria-label="Next Page" role="button"></a>
</li>
<li class="${this.lastButtonClasses}">
<a class="page-link icon-seek-end mdi mdi-page-last mdi-22px" aria-label="Last Page" role="button"></a>
</li>
</ul>
</nav>
</div>
</div>`;

if (position === 'top') {
// we can prepend the grid if we wish
this._paginationElement.classList.add('top');
containerElm.prepend(this._paginationElement);
} else {
// or append it at the bottom
this._paginationElement.classList.add('bottom');
containerElm.appendChild(this._paginationElement);
}

// button usabilities (which buttons are disabled/enabled)
this.updatePageButtonsUsability();

// value/classes bindings
this.addBindings();

// event listeners
this.addEventListeners(this._paginationElement);
}

/**
* Add some DOM Element bindings, typically the framework you choose will do this (i.e. Angular/React/...)
* but we're in plain JS here so let's use simply binding service available in Slickgrid-Universal
*/
addBindings(): void {
// CSS classes
this._bindingHelper.addElementBinding(this, 'firstButtonClasses', 'li.page-item.seek-first', 'className');
this._bindingHelper.addElementBinding(this, 'prevButtonClasses', 'li.page-item.seek-prev', 'className');
this._bindingHelper.addElementBinding(this, 'lastButtonClasses', 'li.page-item.seek-end', 'className');
this._bindingHelper.addElementBinding(this, 'nextButtonClasses', 'li.page-item.seek-next', 'className');

// span texts
this._bindingHelper.addElementBinding(this.currentPagination, 'dataFrom', 'span.item-from', 'textContent');
this._bindingHelper.addElementBinding(this.currentPagination, 'dataTo', 'span.item-to', 'textContent');
this._bindingHelper.addElementBinding(this.currentPagination, 'totalItems', 'span.total-items', 'textContent');
this._bindingHelper.addElementBinding(this.currentPagination, 'pageCount', 'span.page-count', 'textContent');
this._bindingHelper.addElementBinding(this.currentPagination, 'pageNumber', 'span.page-number', 'textContent');
}

/** Add some DOM Element event listeners */
addEventListeners(containerElm: HTMLElement): void {
this._bindingEventService.bind(containerElm.querySelector('.icon-seek-first')!, 'click', this.onFirstPageClicked.bind(this) as EventListener);
this._bindingEventService.bind(containerElm.querySelector('.icon-seek-prev')!, 'click', this.onPreviousPageClicked.bind(this) as EventListener);
this._bindingEventService.bind(containerElm.querySelector('.icon-seek-next')!, 'click', this.onNextPageClicked.bind(this) as EventListener);
this._bindingEventService.bind(containerElm.querySelector('.icon-seek-end')!, 'click', this.onLastPageClicked.bind(this) as EventListener);
}

onFirstPageClicked(event: MouseEvent): void {
if (!this.isLeftPaginationDisabled()) {
this.paginationService.goToFirstPage(event);
}
}

onLastPageClicked(event: MouseEvent): void {
if (!this.isRightPaginationDisabled()) {
this.paginationService.goToLastPage(event);
}
}

onNextPageClicked(event: MouseEvent): void {
if (!this.isRightPaginationDisabled()) {
this.paginationService.goToNextPage(event);
}
}

onPreviousPageClicked(event: MouseEvent): void {
if (!this.isLeftPaginationDisabled()) {
this.paginationService.goToPreviousPage(event);
}
}

isLeftPaginationDisabled(): boolean {
return this.currentPagination.pageNumber === 1 || this.currentPagination.totalItems === 0;
}

isRightPaginationDisabled(): boolean {
return this.currentPagination.pageNumber === this.currentPagination.pageCount || this.currentPagination.totalItems === 0;
}

/** button usabilities (which buttons are disabled/enabled) */
protected updatePageButtonsUsability(): void {
this.firstButtonClasses = this.isLeftPaginationDisabled() ? 'page-item seek-first disabled' : 'page-item seek-first';
this.prevButtonClasses = this.isLeftPaginationDisabled() ? 'page-item seek-prev disabled' : 'page-item seek-prev';
this.lastButtonClasses = this.isRightPaginationDisabled() ? 'page-item seek-end disabled' : 'page-item seek-end';
this.nextButtonClasses = this.isRightPaginationDisabled() ? 'page-item seek-next disabled' : 'page-item seek-next';
}
}
34 changes: 34 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/examples/example30.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<h3 class="title is-3">
Example 30 - Custom Pagination

<div class="subtitle code-link">
<span class="is-size-6">see</span>
<a class="is-size-5" target="_blank"
href="https://github.com/ghiscoding/slickgrid-universal/blob/master/examples/vite-demo-vanilla-bundle/src/examples/example30.ts">
<span class="mdi mdi-link-variant"></span> code
</a>
</div>
</h3>


<div>
<button class="button is-small" onclick.delegate="togglePaginationPosition()" data-text="toggle-pagination-btn">
<span class="mdi mdi-swap-vertical"></span>
<span>Toggle Pagination Position</span>
</button>

<span class="margin-15px">
Page Size
<input type="text" class="input is-small is-narrow" data-test="page-size-input" value.bind="pageSize" onkeyup.delegate="setPaginationSize(event.target.value)">
</span>
</div>

<br>

<h6 class="title is-6 italic content">
You can create a Custom Pagination that will be appended/prepended (bottom or top) of the grid. In fact we can put these pagination elements
anywhere on the page (for example the page size is totally separate in his own corner above).
</h6>

<div class="grid30">
</div>
Loading

0 comments on commit 4a2bfc8

Please sign in to comment.