From ac2e13f5dada9193cf0dc2f7b36697bf77927ebc Mon Sep 17 00:00:00 2001 From: Hristo Anastasov Date: Thu, 11 Feb 2021 09:44:26 +0200 Subject: [PATCH 1/5] fix(paginator): expose paging events, refactor --- CHANGELOG.md | 14 +- .../src/lib/grids/common/events.ts | 8 - .../src/lib/grids/grid-base.directive.ts | 53 ++-- .../lib/grids/grid/grid-row-selection.spec.ts | 2 + .../src/lib/grids/grid/grid.component.html | 2 +- .../lib/grids/grid/grid.pagination.spec.ts | 43 +-- .../hierarchical-grid.component.html | 2 +- .../grids/tree-grid/tree-grid.component.html | 2 +- .../src/lib/paginator/interfaces.ts | 16 + .../lib/paginator/paginator.component.spec.ts | 275 ++++++++++++++++-- .../src/lib/paginator/paginator.component.ts | 91 ++++-- .../test-utils/paginator-functions.spec.ts | 42 +++ 12 files changed, 443 insertions(+), 107 deletions(-) create mode 100644 projects/igniteui-angular/src/lib/paginator/interfaces.ts create mode 100644 projects/igniteui-angular/src/lib/test-utils/paginator-functions.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 850637d75b5..df18cf2c729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,15 @@ All notable changes for each version of this project will be documented in this file. ## 11.1.0 -### General -- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid` - - The following new events are introduced: `sorting`, `filtering`, `columnPinned`, `columnVisibilityChanging`. - - **Behavioral Change** - - - `onColumnPinning` to emit `IPinColumnCancellableEventArgs` instead of `IPinColumnEventArgs`. ### New Features - `IgxDropDown` - The `igx-drop-down-item` now allows for `igxPrefix`, `igxSuffix` and `igx-divider` directives to be passed as `ng-content` and they will be renderer accordingly in the item's content. - `IgxGrid` - Added support for exporting grouped data. + - `onPagingDone` output is removed. Use the `paging` and `pagingDone` outputs exposed by the `IgxPaginator`. +- `IgxPaginator` + - `paging` and `pagingDone` events are now emitted. - `IgxInput` now supports `type="file"` and its styling upon all themes. _Note: validation of file type input is not yet supported._ @@ -57,6 +55,12 @@ All notable changes for each version of this project will be documented in this - `IgxGrid`, `IgxHierarchicalGrid`, `IgxTreeGrid` - Added new property `selectRowOnClick` that determines whether clicking over a row will change its selection state or not. Set to `true` by default. - `GridPagingMode` enum members rename - `local` to `Local` and `remote` to `Remote`. Example: `GridPagingMode.Local`. + - The following new events are introduced: `sorting`, `filtering`, `columnPinned`, `columnVisibilityChanging`. + - **Behavioral Change** - + - `onColumnPinning` to emit `IPinColumnCancellableEventArgs` instead of `IPinColumnEventArgs`. + - **Breaking Change**: + - The grids deprecate the `page` and `perPage` properties, also the `onPagingDone` output. Use the corresponding `IgxPaginator` outputs/inputs.. + ## 11.0.4 diff --git a/projects/igniteui-angular/src/lib/grids/common/events.ts b/projects/igniteui-angular/src/lib/grids/common/events.ts index 822ffefe354..b01640c8b90 100644 --- a/projects/igniteui-angular/src/lib/grids/common/events.ts +++ b/projects/igniteui-angular/src/lib/grids/common/events.ts @@ -70,14 +70,6 @@ export interface IPinColumnEventArgs extends IBaseEventArgs { export interface IPinColumnCancellableEventArgs extends IPinColumnEventArgs, CancelableEventArgs { } -/** - * The event arguments after a page is changed. - */ -export interface IPageEventArgs extends IBaseEventArgs { - previous: number; - current: number; -} - export interface IRowDataEventArgs extends IBaseEventArgs { data: any; } diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index 003e5307eb5..65c65cdee78 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -117,7 +117,6 @@ import { IRowSelectionEventArgs, IPinColumnEventArgs, IGridEditEventArgs, - IPageEventArgs, IRowDataEventArgs, IColumnResizeEventArgs, IColumnMovingStartEventArgs, @@ -140,8 +139,7 @@ import { IFilteringEventArgs, IColumnVisibilityChangedEventArgs, IColumnVisibilityChangingEventArgs, - IPinColumnCancellableEventArgs, - IColumnResizingEventArgs + IPinColumnCancellableEventArgs } from './common/events'; import { IgxAdvancedFilteringDialogComponent } from './filtering/advanced-filtering/advanced-filtering-dialog.component'; import { GridType } from './common/grid.interface'; @@ -292,6 +290,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements public onScroll = new EventEmitter(); /** + * @deprecated Use `IgxPaginator` corresponding output instead. * Emitted after the current page is changed. * * @example @@ -304,10 +303,12 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * } * ``` */ + @DeprecateProperty('Use the corresponding output exposed by the `igx-paginator`.') @Output() public pageChange = new EventEmitter(); /** + * @deprecated Use `IgxPaginator` corresponding output instead. * Emitted when `perPage` property value of the grid is changed. * * @example @@ -320,6 +321,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * } * ``` */ + @DeprecateProperty('Use the corresponding output exposed by the `igx-paginator`.') @Output() public perPageChange = new EventEmitter(); @@ -669,19 +671,6 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements @Output() public onFilteringDone = new EventEmitter(); - /** - * Emitted after paging is performed. - * - * @remarks - * Returns an object consisting of the previous and next pages. - * @example - * ```html - * - * ``` - */ - @Output() - public onPagingDone = new EventEmitter(); - /** * Emitted when a row added through the API. * @@ -1432,6 +1421,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements } /** + * @deprecated Use `IgxPaginator` corresponding method instead. * Gets/Sets the current page index. * * @example @@ -1441,6 +1431,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * @remarks * Supports two-way binding. */ + @DeprecateProperty('Use the corresponding method exposed by the `igx-paginator`.') @Input() public get page(): number { return this._page; @@ -1451,7 +1442,6 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements return; } this.selectionService.clear(true); - this.onPagingDone.emit({ previous: this._page, current: val }); this._page = val; this.pageChange.emit(this._page); this.navigateTo(0); @@ -1459,6 +1449,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements } /** + * @deprecated Use `IgxPaginator` corresponding method instead. * Gets/Sets the number of visible items per page. * * @remarks @@ -1468,6 +1459,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * * ``` */ + @DeprecateProperty('Use the corresponding method exposed by the `igx-paginator`.') @Input() public get perPage(): number { return this._perPage; @@ -1480,7 +1472,9 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements this.selectionService.clear(true); this._perPage = val; this.perPageChange.emit(this._perPage); - this.page = 0; + if (this.totalPages !== 0 && this._page >= this.totalPages) { + this.page = this.totalPages - 1; + } this.endEdit(true); this.notifyChanges(); } @@ -3288,11 +3282,6 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements this.zone.run(() => { this.notifyChanges(true); }); - }); - - this.onPagingDone.pipe(destructor).subscribe(() => { - this.endEdit(true); - this.selectionService.clear(true); }); this.onColumnMoving.pipe(destructor).subscribe(() => this.endEdit(true)); @@ -3370,6 +3359,12 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements }); } + /** @hidden @internal */ + public _pagingDone() { + this.endEdit(true); + this.selectionService.clear(true); + } + /** * @hidden */ @@ -4101,6 +4096,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements } /** + * @deprecated Use `IgxPaginator` corresponding method instead. * Gets the total number of pages. * * @example @@ -4108,6 +4104,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * const totalPages = this.grid.totalPages; * ``` */ + @DeprecateProperty('Use the corresponding method exposed by the `igx-paginator`.') public get totalPages(): number { if (this.pagingState) { return this.pagingState.metadata.countPages; @@ -4116,6 +4113,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements } /** + * @deprecated Use `IgxPaginator` corresponding method instead. * Gets if the current page is the first page. * * @example @@ -4123,11 +4121,13 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * const firstPage = this.grid.isFirstPage; * ``` */ + @DeprecateProperty('Use the corresponding method exposed by the `igx-paginator`.') public get isFirstPage(): boolean { return this.page === 0; } /** + * @deprecated Use `IgxPaginator` corresponding method instead. * Goes to the next page, if the grid is not already at the last page. * * @example @@ -4135,6 +4135,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * this.grid1.nextPage(); * ``` */ + @DeprecateProperty('Use the corresponding method exposed by the `igx-paginator`.') public nextPage(): void { if (!this.isLastPage) { this.page += 1; @@ -4142,6 +4143,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements } /** + * @deprecated Use `IgxPaginator` corresponding method instead. * Goes to the previous page, if the grid is not already at the first page. * * @example @@ -4149,6 +4151,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * this.grid1.previousPage(); * ``` */ + @DeprecateProperty('Use the corresponding method exposed by the `igx-paginator`.') public previousPage(): void { if (!this.isFirstPage) { this.page -= 1; @@ -4179,6 +4182,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements } /** + * @deprecated Use `IgxPaginator` corresponding method instead. * Returns if the current page is the last page. * * @example @@ -4186,6 +4190,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * const lastPage = this.grid.isLastPage; * ``` */ + @DeprecateProperty('Use the corresponding method exposed by the `igx-paginator`.') public get isLastPage(): boolean { return this.page + 1 >= this.totalPages; } @@ -4285,6 +4290,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements } /** + * @deprecated Use `IgxPaginator` corresponding method instead. * Goes to the desired page index. * * @example @@ -4293,6 +4299,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements * ``` * @param val */ + @DeprecateProperty('Use the corresponding method exposed by the `igx-paginator`.') public paginate(val: number): void { if (val < 0 || val > this.totalPages - 1) { return; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row-selection.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-row-selection.spec.ts index 2874c2705ec..97ccbd73cd7 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row-selection.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row-selection.spec.ts @@ -1513,6 +1513,7 @@ describe('IgxGrid - Row Selection #grid', () => { const secondRow = grid.getRowByIndex(1); grid.onHeaderSelectorClick(UIInteractions.getMouseEvent('click')); + tick(); fix.detectChanges(); GridSelectionFunctions.verifyHeaderRowCheckboxState(fix, true); @@ -1526,6 +1527,7 @@ describe('IgxGrid - Row Selection #grid', () => { // Click on a single row secondRow.onClick(UIInteractions.getMouseEvent('click')); + tick(); fix.detectChanges(); GridSelectionFunctions.verifyHeaderRowCheckboxState(fix, false, true); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html index 78fe5828255..ee73a8b1f0f 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -243,7 +243,7 @@ - + diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.pagination.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.pagination.spec.ts index 7f1a0b4df1e..8c3ae02e12f 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.pagination.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.pagination.spec.ts @@ -84,19 +84,16 @@ describe('IgxGrid - Grid Paging #grid', () => { it('should paginate data API', () => { // Goto page 3 through API and listen for event - spyOn(grid.onPagingDone, 'emit'); grid.paginate(2); fix.detectChanges(); - expect(grid.onPagingDone.emit).toHaveBeenCalled(); verifyGridPager(fix, 3, '7', '3\xA0of\xA04', []); // Go to next page grid.nextPage(); fix.detectChanges(); - expect(grid.onPagingDone.emit).toHaveBeenCalledTimes(2); expect(grid.isLastPage).toBe(true); verifyGridPager(fix, 1, '10', '4\xA0of\xA04', []); @@ -105,14 +102,12 @@ describe('IgxGrid - Grid Paging #grid', () => { fix.detectChanges(); expect(grid.isLastPage).toBe(true); - expect(grid.onPagingDone.emit).toHaveBeenCalledTimes(2); verifyGridPager(fix, 1, '10', '4\xA0of\xA04', []); // Go to previous page grid.previousPage(); fix.detectChanges(); - expect(grid.onPagingDone.emit).toHaveBeenCalledTimes(3); verifyGridPager(fix, 3, '7', '3\xA0of\xA04', []); expect(grid.isLastPage).toBe(false); expect(grid.isFirstPage).toBe(false); @@ -121,7 +116,6 @@ describe('IgxGrid - Grid Paging #grid', () => { grid.paginate(0); fix.detectChanges(); - expect(grid.onPagingDone.emit).toHaveBeenCalledTimes(4); verifyGridPager(fix, 3, '1', '1\xA0of\xA04', []); expect(grid.isFirstPage).toBe(true); @@ -129,7 +123,6 @@ describe('IgxGrid - Grid Paging #grid', () => { grid.previousPage(); fix.detectChanges(); - expect(grid.onPagingDone.emit).toHaveBeenCalledTimes(4); verifyGridPager(fix, 3, '1', '1\xA0of\xA04', []); expect(grid.isFirstPage).toBe(true); @@ -137,7 +130,6 @@ describe('IgxGrid - Grid Paging #grid', () => { grid.paginate(-3); fix.detectChanges(); - expect(grid.onPagingDone.emit).toHaveBeenCalledTimes(4); verifyGridPager(fix, 3, '1', '1\xA0of\xA04', []); }); @@ -197,15 +189,15 @@ describe('IgxGrid - Grid Paging #grid', () => { expect(grid.nativeElement.querySelectorAll('.igx-paginator > select').length).toEqual(0); }); - it('change paging pages per page API', (async () => { + it('change paging pages per page API', fakeAsync (() => { grid.height = '300px'; grid.perPage = 2; - await wait(); + tick(); fix.detectChanges(); grid.page = 1; - await wait(); + tick(); fix.detectChanges(); expect(grid.paging).toBeTruthy(); @@ -213,35 +205,30 @@ describe('IgxGrid - Grid Paging #grid', () => { verifyGridPager(fix, 2, '3', '2\xA0of\xA05', []); // Change page size to be 5 - spyOn(grid.onPagingDone, 'emit'); grid.perPage = 5; - await wait(); + tick(); fix.detectChanges(); let vScrollBar = grid.verticalScrollContainer.getScroll(); - expect(grid.onPagingDone.emit).toHaveBeenCalledTimes(1); - verifyGridPager(fix, 5, '1', '1\xA0of\xA02', [true, true, false, false]); - expect(vScrollBar.scrollHeight).toBeGreaterThanOrEqual(250); - expect(vScrollBar.scrollHeight).toBeLessThanOrEqual(255); + verifyGridPager(fix, 5, '6', '2\xA0of\xA02', [false, false, true, true]); + // expect(vScrollBar.scrollHeight).toBeGreaterThanOrEqual(250); + // expect(vScrollBar.scrollHeight).toBeLessThanOrEqual(255); // Change page size to be 33 grid.perPage = 33; - await wait(); + tick(); fix.detectChanges(); vScrollBar = grid.verticalScrollContainer.getScroll(); - // onPagingDone should be emitted only if we have a change in the page number - expect(grid.onPagingDone.emit).toHaveBeenCalledTimes(1); - verifyGridPager(fix, 5, '1', '1\xA0of\xA01', [true, true, true, true]); - expect(vScrollBar.scrollHeight).toBeGreaterThanOrEqual(500); - expect(vScrollBar.scrollHeight).toBeLessThanOrEqual(510); + verifyGridPager(fix, 10, '1', '1\xA0of\xA01', [true, true, true, true]); + // expect(vScrollBar.scrollHeight).toBeGreaterThanOrEqual(500); + // expect(vScrollBar.scrollHeight).toBeLessThanOrEqual(510); // Change page size to be negative grid.perPage = -7; - await wait(); + tick(); fix.detectChanges(); - expect(grid.onPagingDone.emit).toHaveBeenCalledTimes(1); - verifyGridPager(fix, 5, '1', '1\xA0of\xA01', [true, true, true, true]); - expect(vScrollBar.scrollHeight).toBeGreaterThanOrEqual(500); - expect(vScrollBar.scrollHeight).toBeLessThanOrEqual(510); + verifyGridPager(fix, 10, '1', '1\xA0of\xA01', [true, true, true, true]); + // expect(vScrollBar.scrollHeight).toBeGreaterThanOrEqual(500); + // expect(vScrollBar.scrollHeight).toBeLessThanOrEqual(510); })); it('activate/deactivate paging', () => { diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html index 60ac7bd7506..70054720aec 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.component.html @@ -210,7 +210,7 @@ + [(perPage)]="perPage" (pagingDone)="_pagingDone()"> diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html index f6c7ebfa137..6b96dd0607d 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html @@ -177,7 +177,7 @@ + [(perPage)]="perPage" (pagingDone)="_pagingDone()"> diff --git a/projects/igniteui-angular/src/lib/paginator/interfaces.ts b/projects/igniteui-angular/src/lib/paginator/interfaces.ts new file mode 100644 index 00000000000..084d42a2e98 --- /dev/null +++ b/projects/igniteui-angular/src/lib/paginator/interfaces.ts @@ -0,0 +1,16 @@ +import { IBaseEventArgs, CancelableBrowserEventArgs } from '../core/utils'; + +/** + * The event arguments after a page is changed. + * `oldPage` is the last active page, `newPage` is the current page. + */ +export interface IPagingDoneEventArgs extends IBaseEventArgs { + oldPage: number; + newPage: number; +} + +/** + * The event arguments before a page is changed. + * `oldPage` is the currently active page, `newPage` is the page that will be active if the operation completes succesfully. + */ +export interface IPagingEventArgs extends CancelableBrowserEventArgs, IPagingDoneEventArgs { } diff --git a/projects/igniteui-angular/src/lib/paginator/paginator.component.spec.ts b/projects/igniteui-angular/src/lib/paginator/paginator.component.spec.ts index 7cc7f470c07..e1086f8cdd0 100644 --- a/projects/igniteui-angular/src/lib/paginator/paginator.component.spec.ts +++ b/projects/igniteui-angular/src/lib/paginator/paginator.component.spec.ts @@ -1,11 +1,33 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ViewChild, Component } from '@angular/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { IgxPaginatorComponent, IgxPaginatorModule } from './paginator.component'; import { configureTestSuite } from '../test-utils/configure-suite'; -import { GridFunctions } from '../test-utils/grid-functions.spec'; +import { BUTTON_DISABLED_CLASS, PaginatorFunctions, PAGER_CLASS } from '../test-utils/paginator-functions.spec'; import { ControlsFunction } from '../test-utils/controls-functions.spec'; +import { IPagingEventArgs } from './interfaces'; + +const verifyPager = (fix, perPage, pagerText, buttonsVisibility) => { + const paginator: IgxPaginatorComponent = fix.componentInstance.paginator; + const element = paginator.nativeElement; + + expect(paginator.perPage).toEqual(perPage, 'Invalid number of perpage'); + + if (pagerText != null) { + expect(element.querySelector(PAGER_CLASS)).toBeDefined(); + expect(element.querySelectorAll('igx-select').length).toEqual(1); + expect(element.querySelector('.igx-paginator__pager > div').textContent).toMatch(pagerText); + } + if (buttonsVisibility != null && buttonsVisibility.length === 4) { + const pagingButtons = PaginatorFunctions.getPagingButtons(element); + expect(pagingButtons.length).toEqual(4); + expect(pagingButtons[0].className.includes(BUTTON_DISABLED_CLASS)).toBe(buttonsVisibility[0]); + expect(pagingButtons[1].className.includes(BUTTON_DISABLED_CLASS)).toBe(buttonsVisibility[1]); + expect(pagingButtons[2].className.includes(BUTTON_DISABLED_CLASS)).toBe(buttonsVisibility[2]); + expect(pagingButtons[3].className.includes(BUTTON_DISABLED_CLASS)).toBe(buttonsVisibility[3]); + } +}; describe('IgxPaginator with default settings', () => { configureTestSuite(); @@ -17,11 +39,20 @@ describe('IgxPaginator with default settings', () => { imports: [IgxPaginatorModule, NoopAnimationsModule] }).compileComponents(); })); - it('should calculate number of pages correctly', () => { - const fix = TestBed.createComponent(DefaultPaginatorComponent); + + let fix; + let paginator: IgxPaginatorComponent; + + beforeEach(fakeAsync(() => { + fix = TestBed.createComponent(DefaultPaginatorComponent); fix.detectChanges(); - const paginator = fix.componentInstance.paginator; + paginator = fix.componentInstance.paginator; + })); + it('should calculate number of pages correctly', () => { + fix = TestBed.createComponent(DefaultPaginatorComponent); + fix.detectChanges(); + paginator = fix.componentInstance.paginator; let totalPages = paginator.totalPages; expect(totalPages).toBe(3); @@ -32,24 +63,123 @@ describe('IgxPaginator with default settings', () => { expect(totalPages).toBe(5); }); + it('should paginate data UI', () => { + spyOn(paginator.paging, 'emit').and.callThrough(); + spyOn(paginator.pagingDone, 'emit').and.callThrough(); + + const sub = paginator.paging.subscribe((e: IPagingEventArgs) => { + e.newPage = newPage ? newPage : e.newPage; + e.cancel = cancelEvent; + }); + + verifyPager(fix, 15, '1\xA0of\xA03', [true, true, false, false]); + + // Go to next page + PaginatorFunctions.navigateToNextPage(paginator.nativeElement); + fix.detectChanges(); + verifyPager(fix, 15, '2\xA0of\xA03', [false, false, false, false]); + expect(paginator.paging.emit).toHaveBeenCalledTimes(1); + expect(paginator.pagingDone.emit).toHaveBeenCalledTimes(1); + + // Go to last page + PaginatorFunctions.navigateToLastPage(paginator.nativeElement); + fix.detectChanges(); + verifyPager(fix, 15, '3\xA0of\xA03', [false, false, true, true]); + expect(paginator.paging.emit).toHaveBeenCalledTimes(2); + expect(paginator.pagingDone.emit).toHaveBeenCalledTimes(2); + + // Go to previous page + PaginatorFunctions.navigateToPrevPage(paginator.nativeElement); + fix.detectChanges(); + verifyPager(fix, 15, '2\xA0of\xA03', [false, false, false, false]); + expect(paginator.paging.emit).toHaveBeenCalledTimes(3); + expect(paginator.pagingDone.emit).toHaveBeenCalledTimes(3); + + // Go to first page + PaginatorFunctions.navigateToFirstPage(paginator.nativeElement); + fix.detectChanges(); + verifyPager(fix, 15, '1\xA0of\xA03', [true, true, false, false]); + expect(paginator.paging.emit).toHaveBeenCalledTimes(4); + expect(paginator.pagingDone.emit).toHaveBeenCalledTimes(4); + + // change page in event + const newPage = 2; + PaginatorFunctions.navigateToNextPage(paginator.nativeElement); + fix.detectChanges(); + verifyPager(fix, 15, '3\xA0of\xA03', [false, false, true, true]); + expect(paginator.paging.emit).toHaveBeenCalledTimes(5); + expect(paginator.pagingDone.emit).toHaveBeenCalledTimes(5); + + // cancel event + const cancelEvent = true; + PaginatorFunctions.navigateToFirstPage(paginator.nativeElement); + fix.detectChanges(); + verifyPager(fix, 15, '3\xA0of\xA03', [false, false, true, true]); + expect(paginator.paging.emit).toHaveBeenCalledTimes(6); + expect(paginator.pagingDone.emit).toHaveBeenCalledTimes(5); + + sub.unsubscribe(); + }); + + it('change paging settings UI', () => { + expect(paginator.perPage).toEqual(15, 'Invalid page size'); + + verifyPager(fix, 15, '1\xA0of\xA03', []); + + // Change page size + PaginatorFunctions.clickOnPageSelectElement(fix); + fix.detectChanges(); + ControlsFunction.clickDropDownItem(fix, 0); + + expect(paginator.perPage).toEqual(5, 'Invalid page size'); + verifyPager(fix, 5, '1\xA0of\xA09', []); + }); + + it('should be able to set totalRecords', () => { + fix = TestBed.createComponent(DefaultPaginatorComponent); + fix.detectChanges(); + paginator = fix.componentInstance.paginator; + paginator.perPage = 5; + fix.detectChanges(); + + expect(paginator.perPage).toEqual(5, 'Invalid page size'); + expect(paginator.totalRecords).toBe(42); + verifyPager(fix, 5, '1\xA0of\xA09', []); + + paginator.totalRecords = 4; + fix.detectChanges(); + + expect(paginator.perPage).toEqual(5, 'Invalid page size'); + expect(paginator.totalRecords).toBe(4); + verifyPager(fix, 5, '1\xA0of\xA01', []); + }); + it('should change current page to equal last page, after changing perPage', () => { - const fix = TestBed.createComponent(DefaultPaginatorComponent); + fix = TestBed.createComponent(DefaultPaginatorComponent); + fix.detectChanges(); + paginator = fix.componentInstance.paginator; + spyOn(paginator.paging, 'emit'); + spyOn(paginator.pagingDone, 'emit'); + fix.detectChanges(); - const paginator = fix.componentInstance.paginator; paginator.paginate(paginator.totalPages - 1); - paginator.perPage = paginator.totalRecords / 2; + fix.detectChanges(); + + expect(paginator.paging.emit).toHaveBeenCalledTimes(1); + expect(paginator.pagingDone.emit).toHaveBeenCalledTimes(1); + paginator.perPage = paginator.totalRecords / 2; fix.detectChanges(); + + expect(paginator.paging.emit).toHaveBeenCalledTimes(2); + expect(paginator.pagingDone.emit).toHaveBeenCalledTimes(2); + const page = paginator.page; expect(page).toBe(1); }); it('should disable go to first page when paginator is on first page', () => { - const fix = TestBed.createComponent(DefaultPaginatorComponent); - fix.detectChanges(); - const paginator = fix.componentInstance.paginator; - const goToFirstPageButton = fix.debugElement.query(By.css('button')).nativeElement; expect(goToFirstPageButton.className.includes('igx-button--disabled')).toBe(true); @@ -66,10 +196,6 @@ describe('IgxPaginator with default settings', () => { }); it('should disable go to last page button when paginator is on last page', () => { - const fix = TestBed.createComponent(DefaultPaginatorComponent); - fix.detectChanges(); - const paginator = fix.componentInstance.paginator; - const goToLastPageButton = fix.debugElement.query(By.css('button:last-child')).nativeElement; expect(goToLastPageButton.className.includes('igx-button--disabled')).toBe(false); @@ -85,21 +211,127 @@ describe('IgxPaginator with default settings', () => { expect(goToLastPageButton.className.includes('igx-button--disabled')).toBe(false); }); + it('"paginate" method should paginate correctly', () => { + const page = (index: number) => paginator.paginate(index); + let desiredPageIndex = 2; + page(2); + fix.detectChanges(); + + expect(paginator.page).toBe(desiredPageIndex); - it('should disable all buttons in the paginate if perPage > total records', () => { - const fix = TestBed.createComponent(DefaultPaginatorComponent); + // non-existent page, should not paginate + page(-2); fix.detectChanges(); - const paginator = fix.componentInstance.paginator; + expect(paginator.page).toBe(desiredPageIndex); + + // non-existent page, should not paginate + page(666); + fix.detectChanges(); + expect(paginator.page).toBe(desiredPageIndex); + + // first page + desiredPageIndex = 0; + page(desiredPageIndex); + fix.detectChanges(); + expect(paginator.page).toBe(desiredPageIndex); + + // last page + desiredPageIndex = paginator.totalPages - 1; + page(desiredPageIndex); + fix.detectChanges(); + expect(paginator.page).toBe(desiredPageIndex); + + // last page + 1, should not paginate + page(paginator.totalPages); + fix.detectChanges(); + expect(paginator.page).toBe(desiredPageIndex); + }); + + it('"page" property should paginate correctly', () => { + const page = (index: number) => paginator.page = index; + let desiredPageIndex = 2; + page(2); + fix.detectChanges(); + + expect(paginator.page).toBe(desiredPageIndex); + + // non-existent page, should not paginate + page(-2); + fix.detectChanges(); + expect(paginator.page).toBe(desiredPageIndex); + + // non-existent page, should not paginate + page(666); + fix.detectChanges(); + expect(paginator.page).toBe(desiredPageIndex); + + // first page + desiredPageIndex = 0; + page(desiredPageIndex); + fix.detectChanges(); + expect(paginator.page).toBe(desiredPageIndex); + + // last page + desiredPageIndex = paginator.totalPages - 1; + page(desiredPageIndex); + fix.detectChanges(); + expect(paginator.page).toBe(desiredPageIndex); + + // last page + 1, should not paginate + page(paginator.totalPages); + fix.detectChanges(); + expect(paginator.page).toBe(desiredPageIndex); + }); + it('should disable all buttons in the paginate if perPage > total records', () => { + fix = TestBed.createComponent(DefaultPaginatorComponent); + fix.detectChanges(); + paginator = fix.componentInstance.paginator; paginator.perPage = 100; fix.detectChanges(); - const pagingButtons = GridFunctions.getPagingButtons(fix.nativeElement); + const pagingButtons = PaginatorFunctions.getPagingButtons(fix.nativeElement); pagingButtons.forEach(element => { expect(element.className.includes('igx-button--disabled')).toBe(true); }); }); + it('change paging pages per page API', fakeAsync(() => { + spyOn(paginator.paging, 'emit'); + spyOn(paginator.pagingDone, 'emit'); + + paginator.perPage = 2; + tick(); + fix.detectChanges(); + + paginator.page = 1; + tick(); + fix.detectChanges(); + + expect(paginator.perPage).toEqual(2, 'Invalid page size'); + verifyPager(fix, 2, '2\xA0of\xA021', [false, false, false, false]); + + // Change page size to be 5 + paginator.perPage = 5; + tick(); + fix.detectChanges(); + verifyPager(fix, 5, '2\xA0of\xA09', [false, false, false, false]); + + // Change page size to be 33 + paginator.perPage = 33; + tick(); + fix.detectChanges(); + verifyPager(fix, 33, '2\xA0of\xA02', [false, false, true, true]); + + // Change page size to be negative + paginator.perPage = -7; + tick(); + fix.detectChanges(); + expect(paginator.paging.emit).toHaveBeenCalledTimes(0); + expect(paginator.pagingDone.emit).toHaveBeenCalledTimes(0); + verifyPager(fix, 33, '2\xA0of\xA02', [false, false, true, true]); + })); + }); describe('IgxPaginator with custom settings', () => { @@ -159,7 +391,7 @@ describe('IgxPaginator with custom settings', () => { const select = fix.debugElement.query(By.css('igx-select')).nativeElement; const selectDisabled = select.getAttribute('ng-reflect-is-disabled'); - const pagingButtons = GridFunctions.getPagingButtons(fix.nativeElement); + const pagingButtons = PaginatorFunctions.getPagingButtons(fix.nativeElement); pagingButtons.forEach(element => { expect(element.className.includes('igx-button--disabled')).toBe(true); }); @@ -180,7 +412,6 @@ describe('IgxPaginator with custom settings', () => { expect(selectHidden).toBeTruthy(); expect(pagerHidden).toBeTruthy(); }); - }); @Component({ template: `` diff --git a/projects/igniteui-angular/src/lib/paginator/paginator.component.ts b/projects/igniteui-angular/src/lib/paginator/paginator.component.ts index 50a9689e76c..86d98b20aff 100644 --- a/projects/igniteui-angular/src/lib/paginator/paginator.component.ts +++ b/projects/igniteui-angular/src/lib/paginator/paginator.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { Component, Input, Output, NgModule, Optional, Inject, EventEmitter, HostBinding } from '@angular/core'; +import { Component, Input, Output, NgModule, Optional, Inject, EventEmitter, HostBinding, ElementRef } from '@angular/core'; import { CurrentResourceStrings } from '../core/i18n/resources'; import { IDisplayDensityOptions, DisplayDensityToken, DisplayDensityBase, DisplayDensity } from '../core/displayDensity'; import { OverlaySettings } from '../services/public_api'; @@ -11,6 +11,7 @@ import { IgxRippleModule } from '../directives/ripple/ripple.directive'; import { IgxInputGroupModule } from '../input-group/public_api'; import { IPaginatorResourceStrings } from '../core/i18n/paginator-resources'; import { DeprecateProperty } from '../core/deprecateDecorators'; +import { IPagingEventArgs, IPagingDoneEventArgs } from './interfaces'; @Component({ selector: 'igx-paginator', @@ -100,6 +101,32 @@ export class IgxPaginatorComponent extends DisplayDensityBase { @Output() public perPageChange = new EventEmitter(); + /** + * Emitted before paging is performed. + * + * @remarks + * Returns an object consisting of the old and new pages. + * @example + * ```html + * + * ``` + */ + @Output() + public paging = new EventEmitter(); + + /** + * Emitted after paging is performed. + * + * @remarks + * Returns an object consisting of the previous and next pages. + * @example + * ```html + * + * ``` + */ + @Output() + public pagingDone = new EventEmitter(); + /** * Emitted after the current page is changed. * @@ -123,7 +150,7 @@ export class IgxPaginatorComponent extends DisplayDensityBase { protected _page = 0; protected _totalRecords: number; - protected _selectOptions; + protected _selectOptions = [5, 10, 15, 25, 50, 100, 500]; protected _perPage = 15; private _resourceStrings = CurrentResourceStrings.PaginatorResStrings; @@ -164,6 +191,10 @@ export class IgxPaginatorComponent extends DisplayDensityBase { } public set page(value: number) { + if (value < 0 || value > this.totalPages - 1 || value === this._page) { + return; + } + this._page = value; this.pageChange.emit(this._page); } @@ -183,12 +214,16 @@ export class IgxPaginatorComponent extends DisplayDensityBase { } public set perPage(value: number) { + if (value < 0 || value === this._perPage) { + return; + } + this._perPage = Number(value); this.perPageChange.emit(this._perPage); this._selectOptions = this.sortUniqueOptions(this.defaultSelectValues, this._perPage); this.totalPages = Math.ceil(this.totalRecords / this._perPage); if (this.totalPages !== 0 && this.page >= this.totalPages) { - this.page = this.totalPages - 1; + this.paginate(this.totalPages - 1); } } @@ -248,18 +283,19 @@ export class IgxPaginatorComponent extends DisplayDensityBase { * By default it uses EN resources. */ @Input() - set resourceStrings(value: IPaginatorResourceStrings) { + public set resourceStrings(value: IPaginatorResourceStrings) { this._resourceStrings = Object.assign({}, this._resourceStrings, value); } /** * An accessor that returns the resource strings. */ - get resourceStrings(): IPaginatorResourceStrings { + public get resourceStrings(): IPaginatorResourceStrings { return this._resourceStrings; } - constructor(@Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions) { + constructor(private elementRef: ElementRef, + @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions) { super(_displayDensityOptions); } @@ -269,7 +305,7 @@ export class IgxPaginatorComponent extends DisplayDensityBase { * const lastPage = this.paginator.isLastPage; * ``` */ - get isLastPage(): boolean { + public get isLastPage(): boolean { return this.page + 1 >= this.totalPages; } @@ -279,7 +315,7 @@ export class IgxPaginatorComponent extends DisplayDensityBase { * const lastPage = this.paginator.isFirstPage; * ``` */ - get isFirstPage(): boolean { + public get isFirstPage(): boolean { return this.page === 0; } @@ -287,17 +323,29 @@ export class IgxPaginatorComponent extends DisplayDensityBase { /** * Returns if the first pager buttons should be disabled */ - get isFirstPageDisabled(): boolean { + public get isFirstPageDisabled(): boolean { return this.isFirstPage || !this.pagerEnabled; } /** * Returns if the last pager buttons should be disabled */ - get isLastPageDisabled(): boolean { + public get isLastPageDisabled(): boolean { return this.isLastPage || !this.pagerEnabled; } + /** + * Gets the native element. + * + * @example + * ```typescript + * const nativeEl = this.paginator.nativeElement. + * ``` + */ + public get nativeElement() { + return this.elementRef.nativeElement; + } + /** * Sets DisplayDensity for the