From fa1179312295b7c16f450ac5a2079b368a693356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D0=BC=D0=B0=D0=B3=D1=83=D0=BB=D0=BE=D0=B2=20=D0=90?= =?UTF-8?q?=D0=B7=D0=B0=D1=82=20=D0=90=D0=B9=D1=80=D0=B0=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Sun, 2 Feb 2025 22:58:10 +0300 Subject: [PATCH 1/3] feat: wr-pagination component --- projects/lib/pagination/index.ts | 8 + projects/lib/pagination/ng-package.json | 5 + .../lib/pagination/pagination.component.html | 46 ++++ .../lib/pagination/pagination.component.ts | 130 ++++++++++ projects/lib/pagination/pagination.types.ts | 8 + projects/lib/pagination/public-api.ts | 9 + projects/lib/pagination/styles/_index.scss | 60 +++++ projects/lib/pagination/styles/vars.scss | 14 ++ projects/lib/styles.scss | 1 + .../components/sidebar/sidebar.component.ts | 1 + .../app/docs/components/components.routing.ts | 4 + .../pagination/pagination.component.html | 224 ++++++++++++++++++ .../pagination/pagination.component.scss | 12 + .../pagination/pagination.component.ts | 118 +++++++++ projects/showcase/app/routing.ts | 1 + 15 files changed, 641 insertions(+) create mode 100644 projects/lib/pagination/index.ts create mode 100644 projects/lib/pagination/ng-package.json create mode 100644 projects/lib/pagination/pagination.component.html create mode 100644 projects/lib/pagination/pagination.component.ts create mode 100644 projects/lib/pagination/pagination.types.ts create mode 100644 projects/lib/pagination/public-api.ts create mode 100644 projects/lib/pagination/styles/_index.scss create mode 100644 projects/lib/pagination/styles/vars.scss create mode 100644 projects/showcase/app/docs/components/pagination/pagination.component.html create mode 100644 projects/showcase/app/docs/components/pagination/pagination.component.scss create mode 100644 projects/showcase/app/docs/components/pagination/pagination.component.ts diff --git a/projects/lib/pagination/index.ts b/projects/lib/pagination/index.ts new file mode 100644 index 0000000..253b52a --- /dev/null +++ b/projects/lib/pagination/index.ts @@ -0,0 +1,8 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE + */ + +export * from './public-api'; diff --git a/projects/lib/pagination/ng-package.json b/projects/lib/pagination/ng-package.json new file mode 100644 index 0000000..789c95e --- /dev/null +++ b/projects/lib/pagination/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/projects/lib/pagination/pagination.component.html b/projects/lib/pagination/pagination.component.html new file mode 100644 index 0000000..ace486e --- /dev/null +++ b/projects/lib/pagination/pagination.component.html @@ -0,0 +1,46 @@ +
+ @if (showTotal()) { + + {{ currentRange() }} + + } + +
+ + + @for (page of pages(); track page) { + @if (page === '...') { + ... + } @else { + + {{ page }} + + } + } + + +
+ + @if (showSizeChanger()) { + + @for (option of pageSizeOptions(); track option) { + + } + + } +
diff --git a/projects/lib/pagination/pagination.component.ts b/projects/lib/pagination/pagination.component.ts new file mode 100644 index 0000000..1893685 --- /dev/null +++ b/projects/lib/pagination/pagination.component.ts @@ -0,0 +1,130 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE + */ + +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, + booleanAttribute, + computed, + input, + model, + numberAttribute, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { WrButtonComponent } from 'ngwr/button'; +import { provideWrIcons, arrowBack, arrowForward } from 'ngwr/icon'; +import { WrSelectComponent } from 'ngwr/select'; +import { WrOptionComponent } from 'ngwr/select/select-option.component'; + +import { WrPaginationPosition } from './pagination.types'; + +@Component({ + selector: 'wr-pagination', + standalone: true, + imports: [WrButtonComponent, WrSelectComponent, WrOptionComponent, FormsModule], + templateUrl: './pagination.component.html', + styleUrl: './pagination.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + providers: [provideWrIcons([arrowBack, arrowForward])], + host: { + class: 'wr-pagination', + '[class.wr-pagination--disabled]': 'disabled()', + }, +}) +export class WrPaginationComponent { + currentPage = model(1); + total = input(0, { transform: numberAttribute }); + pageSize = model(10); + pageSizeOptions = input([10, 20, 50, 100]); + showSizeChanger = input(false, { transform: booleanAttribute }); + showTotal = input(false, { transform: booleanAttribute }); + position = input('start'); + disabled = input(false, { transform: booleanAttribute }); + ofLabel = input('of'); + + protected readonly totalPages = computed(() => Math.ceil(this.total() / this.pageSize())); + + protected readonly currentRange = computed(() => { + const start = (this.currentPage() - 1) * this.pageSize() + 1; + const end = Math.min(this.currentPage() * this.pageSize(), this.total()); + return `${start}-${end} ${this.ofLabel()} ${this.total()}`; + }); + + protected readonly pages = computed(() => { + const total = this.totalPages(); + const current = this.currentPage(); + const items: (number | '...')[] = []; + + if (total <= 7) { + return Array.from({ length: total }, (_, i) => i + 1); + } + + items.push(1); + + if (current > 4) { + items.push('...'); + } + + let start: number; + let end: number; + + if (current <= 4) { + start = 2; + end = 5; + } else if (current >= total - 3) { + start = total - 4; + end = total - 1; + } else { + start = current - 2; + end = current + 2; + } + + for (let i = start; i <= end; i++) { + items.push(i); + } + + if (current < total - 3) { + items.push('...'); + } + + if (end !== total) { + items.push(total); + } + + return items; + }); + + protected isCurrentPage(page: number): boolean { + return this.currentPage() === page; + } + + onPageChange(page: number): void { + if (this.disabled() || page === this.currentPage() || page < 1 || page > this.totalPages()) { + return; + } + + this.currentPage.set(page); + } + + onPageSizeChange(size: number): void { + if (this.disabled() || size === this.pageSize()) { + return; + } + + this.pageSize.set(size); + + const newTotalPages = Math.ceil(this.total() / size); + if (this.currentPage() > newTotalPages) { + this.onPageChange(newTotalPages); + } + } + + protected readonly String = String; +} diff --git a/projects/lib/pagination/pagination.types.ts b/projects/lib/pagination/pagination.types.ts new file mode 100644 index 0000000..9994e1e --- /dev/null +++ b/projects/lib/pagination/pagination.types.ts @@ -0,0 +1,8 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE + */ + +export type WrPaginationPosition = 'start' | 'center' | 'end'; diff --git a/projects/lib/pagination/public-api.ts b/projects/lib/pagination/public-api.ts new file mode 100644 index 0000000..fe6d42c --- /dev/null +++ b/projects/lib/pagination/public-api.ts @@ -0,0 +1,9 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE + */ + +export * from './pagination.types'; +export * from './pagination.component'; diff --git a/projects/lib/pagination/styles/_index.scss b/projects/lib/pagination/styles/_index.scss new file mode 100644 index 0000000..5dc26a8 --- /dev/null +++ b/projects/lib/pagination/styles/_index.scss @@ -0,0 +1,60 @@ +@forward "./vars"; + +.wr-pagination { + --wr-select-option-height: var(--wr-pagination-select-option-height); + --wr-select-padding-y: var(--wr-pagination-select-padding-y); + --wr-select-padding-x: var(--wr-pagination-select-padding-x); + + display: block; + + &-container { + display: flex; + align-items: center; + gap: var(--wr-pagination-gap); + } + + &--start { + justify-content: flex-start; + } + + &--center { + justify-content: center; + } + + &--end { + justify-content: flex-end; + } + + &--disabled { + opacity: 0.6; + pointer-events: none; + } + + &-total { + color: var(--wr-pagination-total-color); + font-size: var(--wr-pagination-font-size); + } + + &-items { + display: flex; + align-items: center; + gap: var(--wr-pagination-items-gap); + } + + &-ellipsis { + color: var(--wr-pagination-ellipsis-color); + padding: 0 var(--wr-pagination-item-padding); + user-select: none; + } + + wr-btn { + display: flex; + align-items: center; + justify-content: center; + min-width: var(--wr-pagination-item-size); + height: var(--wr-pagination-item-size); + padding: 0; + gap: 0; + text-align: center; + } +} diff --git a/projects/lib/pagination/styles/vars.scss b/projects/lib/pagination/styles/vars.scss new file mode 100644 index 0000000..68d0ce5 --- /dev/null +++ b/projects/lib/pagination/styles/vars.scss @@ -0,0 +1,14 @@ +:root { + --wr-pagination-gap: 1rem; + --wr-pagination-items-gap: 0.5rem; + --wr-pagination-font-size: 0.875rem; + --wr-pagination-total-color: var(--wr-color-medium); + --wr-pagination-ellipsis-color: var(--wr-color-medium); + + --wr-pagination-item-size: 1.625rem; + --wr-pagination-item-padding: 0.5rem; + + --wr-pagination-select-option-height: var(--wr-pagination-item-size); + --wr-pagination-select-padding-y: 0; + --wr-pagination-select-padding-x: 0.825rem; +} diff --git a/projects/lib/styles.scss b/projects/lib/styles.scss index 2bb8f30..79af693 100644 --- a/projects/lib/styles.scss +++ b/projects/lib/styles.scss @@ -22,4 +22,5 @@ @forward './tag/styles'; @forward './textarea/styles'; @forward './select/styles'; +@forward './pagination/styles'; //@forward './tooltip'; diff --git a/projects/showcase/app/_core/components/sidebar/sidebar.component.ts b/projects/showcase/app/_core/components/sidebar/sidebar.component.ts index 7bcc9de..6ed401d 100644 --- a/projects/showcase/app/_core/components/sidebar/sidebar.component.ts +++ b/projects/showcase/app/_core/components/sidebar/sidebar.component.ts @@ -74,6 +74,7 @@ export class SidebarComponent extends WrAbstractBase implements OnInit { { title: 'Tag', url: [routes.docs.components.index, routes.docs.components.tag] }, { title: 'Textarea', url: [routes.docs.components.index, routes.docs.components.textarea] }, { title: 'Select', url: [routes.docs.components.index, routes.docs.components.select] }, + { title: 'Pagination', url: [routes.docs.components.index, routes.docs.components.pagination] }, ], }, ]; diff --git a/projects/showcase/app/docs/components/components.routing.ts b/projects/showcase/app/docs/components/components.routing.ts index 667e49d..f3ac515 100644 --- a/projects/showcase/app/docs/components/components.routing.ts +++ b/projects/showcase/app/docs/components/components.routing.ts @@ -78,4 +78,8 @@ export default [ path: components.select, loadComponent: () => import('./select/select.component').then(c => c.SelectComponent), }, + { + path: components.pagination, + loadComponent: () => import('./pagination/pagination.component').then(c => c.PaginationComponent), + }, ] satisfies Routes; diff --git a/projects/showcase/app/docs/components/pagination/pagination.component.html b/projects/showcase/app/docs/components/pagination/pagination.component.html new file mode 100644 index 0000000..fc04d8e --- /dev/null +++ b/projects/showcase/app/docs/components/pagination/pagination.component.html @@ -0,0 +1,224 @@ +
+
+ Component + Standalone +
+

{{ title }}

+

{{ description }}

+
+ +
+

How to install

+

+ Import WrPaginationComponent into a component where you want to use. +

+
+ + +
+
+ +
+

Basic usage

+

Basic usage of pagination component

+ +
+ + + + + + +
+
+ +
+

Position

+

Different positions of pagination

+ +
+ + +
+ + + + + +
+
+ +
+
+
+ +
+

With Size Changer

+

Pagination with page size selection

+ +
+ + + + + + +
+
+ +
+

Show Total

+

Show total number of items

+ +
+ + + + + + +
+
+ +
+

More Pages

+

Pagination with more pages shows ellipsis for better navigation

+ +
+ + + + + + +
+
+ +
+

Disabled

+

Disabled state of pagination

+ +
+ + + + + + +
+
+ +
+

Styling

+

You can customize pagination styles using CSS variables.

+ +
+ +
+
+ +
+

API

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDescriptionTypeDefault
currentPageCurrent page numbernumber1
totalTotal number of itemsnumber0
pageSizeNumber of items per pagenumber10
pageSizeOptionsAvailable page sizesnumber[][10, 20, 50, 100]
showSizeChangerWhether to show page size selectbooleanfalse
showTotalWhether to show total numberbooleanfalse
ofLabelTotal number "of" labelstring"of"
positionPosition of pagination"start" | "center" | "end""start"
disabledWhether pagination is disabledbooleanfalse
+
+
+
diff --git a/projects/showcase/app/docs/components/pagination/pagination.component.scss b/projects/showcase/app/docs/components/pagination/pagination.component.scss new file mode 100644 index 0000000..0f550cf --- /dev/null +++ b/projects/showcase/app/docs/components/pagination/pagination.component.scss @@ -0,0 +1,12 @@ +.sizes-demo { + display: flex; + flex-direction: column; + gap: 16px; +} + +.position-demo { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +} diff --git a/projects/showcase/app/docs/components/pagination/pagination.component.ts b/projects/showcase/app/docs/components/pagination/pagination.component.ts new file mode 100644 index 0000000..0ae6388 --- /dev/null +++ b/projects/showcase/app/docs/components/pagination/pagination.component.ts @@ -0,0 +1,118 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE + */ + +import { ChangeDetectionStrategy, Component, HostBinding, inject, OnInit, ViewEncapsulation } from '@angular/core'; + +import { WrPaginationComponent } from 'ngwr/pagination'; +import { WrTagComponent } from 'ngwr/tag'; + +import { CodeComponent, SnippetComponent } from '#core/components'; +import { SeoService } from '#core/services'; + +@Component({ + standalone: true, + selector: 'ngwr-pagination', + templateUrl: './pagination.component.html', + styleUrl: './pagination.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + imports: [CodeComponent, SnippetComponent, WrPaginationComponent, WrTagComponent], +}) +export class PaginationComponent implements OnInit { + @HostBinding() + class = 'ngwr-page'; + + private readonly seoService = inject(SeoService); + + title = 'Pagination'; + description = 'A long list can be divided into several pages using Pagination'; + + current = 1; + pageSize = 10; + total = 50; + currentLarge = 1; + pageSizeLarge = 10; + totalLarge = 200; + + code = { + import: `import { WrPaginationComponent } from "ngwr/pagination";`, + component: `@Component({ + //..., + imports: [WrPaginationComponent], + }) + export class MyComponent {}`, + basic: ``, + position: ` + + + + `, + withSizeChanger: ``, + withTotal: ``, + morePages: ``, + disabled: ``, + styling: `:root { + --wr-pagination-gap: 1rem; + --wr-pagination-items-gap: 0.5rem; + --wr-pagination-font-size: 0.875rem; + --wr-pagination-total-color: var(--wr-color-medium); + --wr-pagination-ellipsis-color: var(--wr-color-medium); + + --wr-pagination-item-size: 1.625rem; + --wr-pagination-item-padding: 0.5rem; + + --wr-pagination-select-option-height: var(--wr-pagination-item-size); + --wr-pagination-select-padding-y: 0; + --wr-pagination-select-padding-x: 0.825rem; + }`, + }; + + ngOnInit(): void { + this.seoService.setCanonicalURL(); + this.seoService.setTitle(this.title); + this.seoService.setDescription(this.description); + this.seoService.setKeywords(['pagination', 'wr-pagination']); + } +} diff --git a/projects/showcase/app/routing.ts b/projects/showcase/app/routing.ts index 35cdb67..374d1d9 100644 --- a/projects/showcase/app/routing.ts +++ b/projects/showcase/app/routing.ts @@ -27,6 +27,7 @@ export const routes = { tag: 'tag', textarea: 'textarea', select: 'select', + pagination: 'pagination', }, core: { index: 'core', From 34225cbbedda938c8916f0668e7762067d1c9d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D0=BC=D0=B0=D0=B3=D1=83=D0=BB=D0=BE=D0=B2=20=D0=90?= =?UTF-8?q?=D0=B7=D0=B0=D1=82=20=D0=90=D0=B9=D1=80=D0=B0=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Mon, 3 Feb 2025 11:05:07 +0300 Subject: [PATCH 2/3] fix: pagination styles in package.json --- projects/lib/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/lib/package.json b/projects/lib/package.json index 1c4be6c..da4316e 100644 --- a/projects/lib/package.json +++ b/projects/lib/package.json @@ -84,6 +84,9 @@ }, "./select/styles": { "sass": "./select/styles/_index.scss" + }, + "./pagination/styles": { + "sass": "./pagination/styles/_index.scss" } } } From 949853bf5e8d0a9d04c9fac1c15948b54184df41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D0=BC=D0=B0=D0=B3=D1=83=D0=BB=D0=BE=D0=B2=20=D0=90?= =?UTF-8?q?=D0=B7=D0=B0=D1=82=20=D0=90=D0=B9=D1=80=D0=B0=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=87?= Date: Mon, 3 Feb 2025 11:32:35 +0300 Subject: [PATCH 3/3] fix: hostbinding --- .../lib/pagination/pagination.component.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/projects/lib/pagination/pagination.component.ts b/projects/lib/pagination/pagination.component.ts index 1893685..7e52f02 100644 --- a/projects/lib/pagination/pagination.component.ts +++ b/projects/lib/pagination/pagination.component.ts @@ -14,6 +14,7 @@ import { input, model, numberAttribute, + HostBinding, } from '@angular/core'; import { FormsModule } from '@angular/forms'; @@ -33,10 +34,6 @@ import { WrPaginationPosition } from './pagination.types'; changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, providers: [provideWrIcons([arrowBack, arrowForward])], - host: { - class: 'wr-pagination', - '[class.wr-pagination--disabled]': 'disabled()', - }, }) export class WrPaginationComponent { currentPage = model(1); @@ -49,6 +46,16 @@ export class WrPaginationComponent { disabled = input(false, { transform: booleanAttribute }); ofLabel = input('of'); + @HostBinding('class') + get hostClasses(): Record { + return { + 'wr-pagination': true, + 'wr-pagination--disabled': this.disabled(), + }; + } + + protected readonly String = String; + protected readonly totalPages = computed(() => Math.ceil(this.total() / this.pageSize())); protected readonly currentRange = computed(() => { @@ -105,7 +112,7 @@ export class WrPaginationComponent { return this.currentPage() === page; } - onPageChange(page: number): void { + protected onPageChange(page: number): void { if (this.disabled() || page === this.currentPage() || page < 1 || page > this.totalPages()) { return; } @@ -113,7 +120,7 @@ export class WrPaginationComponent { this.currentPage.set(page); } - onPageSizeChange(size: number): void { + protected onPageSizeChange(size: number): void { if (this.disabled() || size === this.pageSize()) { return; } @@ -125,6 +132,4 @@ export class WrPaginationComponent { this.onPageChange(newTotalPages); } } - - protected readonly String = String; }