diff --git a/packages/stark-ui/src/modules.ts b/packages/stark-ui/src/modules.ts
index 80a64343c3..b85c266949 100644
--- a/packages/stark-ui/src/modules.ts
+++ b/packages/stark-ui/src/modules.ts
@@ -1,15 +1,15 @@
export * from "./modules/action-bar";
export * from "./modules/app-logo";
export * from "./modules/app-logout";
+export * from "./modules/app-sidebar";
export * from "./modules/breadcrumb";
export * from "./modules/collapsible";
-export * from "./modules/app-sidebar";
-export * from "./modules/keyboard-directives";
export * from "./modules/date-picker";
export * from "./modules/date-range-picker";
export * from "./modules/dropdown";
export * from "./modules/keyboard-directives";
export * from "./modules/language-selector";
+export * from "./modules/pagination";
export * from "./modules/pretty-print";
export * from "./modules/slider";
export * from "./modules/svg-view-box";
diff --git a/packages/stark-ui/src/modules/pagination.ts b/packages/stark-ui/src/modules/pagination.ts
new file mode 100644
index 0000000000..be06e59348
--- /dev/null
+++ b/packages/stark-ui/src/modules/pagination.ts
@@ -0,0 +1,2 @@
+export * from "./pagination/pagination.module";
+export * from "./pagination/components";
diff --git a/packages/stark-ui/src/modules/pagination/components.ts b/packages/stark-ui/src/modules/pagination/components.ts
new file mode 100644
index 0000000000..82d047bd70
--- /dev/null
+++ b/packages/stark-ui/src/modules/pagination/components.ts
@@ -0,0 +1,2 @@
+export * from "./components/pagination.component";
+export * from "./components/pagination-config.intf";
diff --git a/packages/stark-ui/src/modules/pagination/components/_pagination-theme.scss b/packages/stark-ui/src/modules/pagination/components/_pagination-theme.scss
new file mode 100644
index 0000000000..fc03d33dc1
--- /dev/null
+++ b/packages/stark-ui/src/modules/pagination/components/_pagination-theme.scss
@@ -0,0 +1,23 @@
+/********** PAGINATION THEME **********/
+/* stark: src/modules/pagination/components/_pagination-theme.scss */
+
+@media #{$tablet-query} {
+ .stark-pagination > div {
+ .pagination-enter-page {
+ & input {
+ border: solid 1px $divider-color;
+ }
+ }
+
+ .page-numbers {
+ &.active {
+ a {
+ color: mat-color(map-get($base-theme, primary-palette));
+ opacity: 1;
+ }
+ }
+ }
+ }
+}
+
+/* END stark: src/modules/pagination/components/_pagination-theme.scss */
diff --git a/packages/stark-ui/src/modules/pagination/components/_pagination.component.scss b/packages/stark-ui/src/modules/pagination/components/_pagination.component.scss
new file mode 100644
index 0000000000..22464a8846
--- /dev/null
+++ b/packages/stark-ui/src/modules/pagination/components/_pagination.component.scss
@@ -0,0 +1,68 @@
+/********** PAGINATION **********/
+/* stark: src/modules/pagination/components/_pagination.component.scss */
+.stark-pagination > div {
+ align-items: center;
+ display: flex;
+ justify-content: flex-start;
+ ul {
+ align-items: center;
+ display: flex;
+ list-style: none;
+ margin: 0;
+ margin-right: 18px;
+ padding: 0;
+ }
+ .pagination-enter-page {
+ display: none;
+ }
+
+ .pagination-items-per-page {
+ .stark-dropdown .mat-form-field {
+ width: 50px;
+ }
+ }
+
+ .page-numbers {
+ display: none;
+ }
+}
+
+@media #{$tablet-query} {
+ .stark-pagination > div {
+ font-size: mat-font-size($typography-config, caption);
+ font-weight: mat-font-weight($typography-config, caption);
+ line-height: mat-line-height($typography-config, caption);
+
+ .pagination-enter-page {
+ align-items: center;
+ display: flex;
+ margin-right: 40px;
+ & input {
+ margin-right: 4px;
+ width: 26px;
+ height: 24px;
+ padding: 4px;
+ border-radius: 2px;
+ text-align: center;
+ }
+ }
+
+ .page-numbers {
+ cursor: pointer;
+ display: flex;
+ a {
+ color: inherit;
+ padding: 15px;
+ opacity: 0.5;
+ }
+ }
+
+ &.compact {
+ .pagination-enter-page {
+ margin-right: 5px;
+ }
+ }
+ }
+}
+
+/* END stark: src/modules/pagination/components/_pagination.component.scss */
diff --git a/packages/stark-ui/src/modules/pagination/components/pagination-config.intf.ts b/packages/stark-ui/src/modules/pagination/components/pagination-config.intf.ts
new file mode 100644
index 0000000000..baf518eaf3
--- /dev/null
+++ b/packages/stark-ui/src/modules/pagination/components/pagination-config.intf.ts
@@ -0,0 +1,52 @@
+/**
+ * Defines the config object to be used with the Pagination component.
+ */
+export interface StarkPaginationConfig {
+ /**
+ * If true, the component will be displayed in "extended" mode (an "extended-pagination" class is added to the element).
+ * Default: false
+ */
+ isExtended?: boolean;
+
+ /**
+ * Number of items displayed on each page. The number of available pages for pagination is calculated based on this number.
+ * Default: itemsPerPageOptions[0]
+ */
+ itemsPerPage?: number;
+
+ /**
+ * Available options for items per page dropdown.
+ * Default : [5,10,15]
+ */
+ itemsPerPageOptions?: number[];
+
+ /**
+ * If false, then itemsPerPage dropdown will not be present.
+ * Default: true
+ */
+ itemsPerPageIsPresent?: boolean;
+
+ /**
+ * Current page index.
+ * Default: 0
+ */
+ page?: number;
+
+ /**
+ * If false, then page nav bar will not be present.
+ * Default: true
+ */
+ pageNavIsPresent?: boolean;
+
+ /**
+ * f false, then input box for page selection will not be present.
+ * Default: true
+ */
+ pageInputIsPresent?: boolean;
+
+ /**
+ * Number of items being paged in order to calculate number of pages for pagination.
+ * Default: 0
+ */
+ totalItems?: number;
+}
diff --git a/packages/stark-ui/src/modules/pagination/components/pagination.component.html b/packages/stark-ui/src/modules/pagination/components/pagination.component.html
new file mode 100644
index 0000000000..0ca1974aed
--- /dev/null
+++ b/packages/stark-ui/src/modules/pagination/components/pagination.component.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ pageNumber }}
+ {{ pageNumber }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/stark-ui/src/modules/pagination/components/pagination.component.spec.ts b/packages/stark-ui/src/modules/pagination/components/pagination.component.spec.ts
new file mode 100644
index 0000000000..1ea3e3d386
--- /dev/null
+++ b/packages/stark-ui/src/modules/pagination/components/pagination.component.spec.ts
@@ -0,0 +1,1084 @@
+import { async, ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing";
+import { FormsModule } from "@angular/forms";
+import { MatInputModule } from "@angular/material/input";
+import { MatMenuModule } from "@angular/material/menu";
+import { MatPaginatorModule } from "@angular/material/paginator";
+import { MatTooltipModule } from "@angular/material/tooltip";
+import { MatButtonModule } from "@angular/material/button";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { Component, DebugElement, NO_ERRORS_SCHEMA, ViewChild } from "@angular/core";
+import { By } from "@angular/platform-browser";
+import { TranslateModule } from "@ngx-translate/core";
+import { STARK_LOGGING_SERVICE } from "@nationalbankbelgium/stark-core";
+import { MockStarkLoggingService } from "@nationalbankbelgium/stark-core/testing";
+import { Observer } from "rxjs";
+import { StarkPaginateEvent, StarkPaginationComponent } from "./pagination.component";
+import { StarkPaginationConfig } from "./pagination-config.intf";
+import { StarkDropdownComponent, StarkDropdownModule } from "../../dropdown";
+import { StarkKeyboardDirectivesModule } from "../../keyboard-directives";
+import SpyObj = jasmine.SpyObj;
+import createSpyObj = jasmine.createSpyObj;
+
+@Component({
+ selector: `host-component`,
+ template: `
+
+ `
+})
+class TestHostComponent {
+ @ViewChild(StarkPaginationComponent)
+ public paginationComponent: StarkPaginationComponent;
+
+ public htmlSuffixId: string;
+ public paginationConfig: StarkPaginationConfig;
+}
+
+fdescribe("PaginationComponent", () => {
+ let hostComponent: TestHostComponent;
+ let hostFixture: ComponentFixture;
+ let component: StarkPaginationComponent;
+
+ const paginationConfig: StarkPaginationConfig = {
+ page: 2,
+ itemsPerPage: 4,
+ itemsPerPageOptions: [4, 8, 12],
+ totalItems: 6,
+ isExtended: true
+ };
+
+ const assertPageNavSelection: Function = (paginationElement: DebugElement, selectedOption: string) => {
+ const pageNavElement: DebugElement = paginationElement.query(By.css("ul"));
+ const pageNavOptionElements: DebugElement[] = pageNavElement.queryAll(By.css("li"));
+
+ for (const pageNavOption of pageNavOptionElements) {
+ if (pageNavOption.properties["value"] === selectedOption) {
+ expect(pageNavOption.classes["active"]).toBe(true);
+ } else {
+ expect(pageNavOption.classes["active"]).toBeFalsy(); // can be undefined or false
+ }
+ }
+ };
+
+ const changeInputValueAndPressEnter: Function = (rootElement: DebugElement, value: string) => {
+ const querySelector: string = "div.pagination-enter-page input";
+ const pageSelectorInput: DebugElement = rootElement.query(By.css(querySelector));
+
+ (pageSelectorInput.nativeElement).value = value;
+ (pageSelectorInput.nativeElement).dispatchEvent(new Event("input"));
+
+ const changeEvent: Event = document.createEvent("Event");
+ changeEvent.initEvent("change", true, true);
+ pageSelectorInput.triggerEventHandler("change", changeEvent);
+
+ const keypressEvent: Event = document.createEvent("Event");
+ keypressEvent.initEvent("keypress", true, true);
+ keypressEvent["which"] = 13;
+ pageSelectorInput.triggerEventHandler("keypress", keypressEvent);
+ };
+
+ const assertPageInputSelection: Function = (rootElement: DebugElement, selectedOption: string) => {
+ const querySelector: string = "div.pagination-enter-page input";
+ const pageSelectorInput: DebugElement = rootElement.query(By.css(querySelector));
+ expect(pageSelectorInput.properties["value"].toString()).toBe(selectedOption);
+ };
+
+ beforeEach(async(() => {
+ return TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MatButtonModule,
+ MatInputModule,
+ MatMenuModule,
+ MatPaginatorModule,
+ MatTooltipModule,
+ NoopAnimationsModule,
+ TranslateModule.forRoot(),
+ StarkDropdownModule,
+ StarkKeyboardDirectivesModule
+ ],
+ declarations: [StarkPaginationComponent, TestHostComponent],
+ providers: [{ provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() }],
+ schemas: [NO_ERRORS_SCHEMA] // to avoid errors due to "mat-icon" directive not known (which we don't want to add in these tests)
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ hostFixture = TestBed.createComponent(TestHostComponent);
+ hostComponent = hostFixture.componentInstance;
+ hostFixture.detectChanges();
+
+ component = hostComponent.paginationComponent;
+ });
+
+ describe("on initialization", () => {
+ it("should set internal component properties", () => {
+ expect(hostFixture).toBeDefined();
+ expect(component).toBeDefined();
+
+ expect(component.logger).not.toBeNull();
+ expect(component.logger).toBeDefined();
+ });
+
+ it("should NOT have any inputs set", () => {
+ expect(component.htmlSuffixId).toBe("pagination");
+ expect(component.mode).toBeUndefined();
+ expect(component.paginationConfig).toBeDefined();
+ });
+
+ it("should render the appropriate content in normal mode", () => {
+ hostComponent.paginationConfig = {
+ page: 2,
+ itemsPerPage: 4,
+ itemsPerPageOptions: [4, 8, 12],
+ totalItems: 10,
+ isExtended: false,
+ pageNavIsPresent: true,
+ pageInputIsPresent: true,
+ itemsPerPageIsPresent: true
+ };
+ hostFixture.detectChanges();
+
+ const pageNavElement: DebugElement = hostFixture.debugElement.query(By.css("ul"));
+ expect(pageNavElement).toBeDefined();
+ expect(pageNavElement.nativeElement.innerHTML).toContain('component.paginationConfig.itemsPerPageOptions).join(",")
+ );
+ expect(itemsPerPageSelector.attributes["ng-reflect-value"]).toBe((component.paginationConfig.itemsPerPage).toString());
+ expect(itemsPerPageSelector.attributes["ng-reflect-dropdown-id"]).toBe("items-per-page-" + "pagination");
+ expect(itemsPerPageSelector.attributes["ng-reflect-dropdown-name"]).toBe("items-per-page-" + "pagination");
+ // expect(itemsPerPageSelector.attr("header")).toBe("STARK.PAGINATION.ITEMS_PER_PAGE"); // TODO add a header to the itemsPerPage dropdown
+ });
+
+ it("should render the appropriate content in extended mode", () => {
+ hostComponent.paginationConfig = {
+ page: 2,
+ itemsPerPage: 4,
+ itemsPerPageOptions: [4, 8, 12],
+ totalItems: 10,
+ isExtended: true,
+ pageNavIsPresent: true,
+ pageInputIsPresent: true,
+ itemsPerPageIsPresent: true
+ };
+ hostFixture.detectChanges();
+
+ const pageNavElement: DebugElement = hostFixture.debugElement.query(By.css("ul"));
+ expect(pageNavElement).toBeDefined();
+ expect(pageNavElement.nativeElement.innerHTML).toContain('component.paginationConfig.itemsPerPageOptions).join(",")
+ );
+ expect(itemsPerPageSelector.attributes["ng-reflect-value"]).toBe((component.paginationConfig.itemsPerPage).toString());
+ expect(itemsPerPageSelector.attributes["ng-reflect-dropdown-id"]).toBe("items-per-page-" + "pagination");
+ expect(itemsPerPageSelector.attributes["ng-reflect-dropdown-name"]).toBe("items-per-page-" + "pagination");
+ // expect(itemsPerPageSelector.attr("header")).toBe("STARK.PAGINATION.ITEMS_PER_PAGE"); // TODO add a header to the itemsPerPage dropdown
+ });
+ });
+
+ describe("isZero", () => {
+ it("should return TRUE if passed number is zero (number)", () => {
+ const isZero: boolean = component.isZero(0);
+ expect(isZero).toBe(true);
+ });
+
+ it("should return TRUE if passed number is zero (string)", () => {
+ const isZero: boolean = component.isZero("0");
+ expect(isZero).toBe(true);
+ });
+
+ it("should return FALSE if passed number is not zero (number)", () => {
+ const isZero: boolean = component.isZero(1);
+ expect(isZero).toBe(false);
+ });
+
+ it("should return FALSE if passed number is not zero (string)", () => {
+ const isZero: boolean = component.isZero("1");
+ expect(isZero).toBe(false);
+ });
+ });
+
+ describe("hasNext", () => {
+ beforeEach(() => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ totalItems: 6
+ };
+ });
+
+ it("should return TRUE if there's a page after the current", () => {
+ component.paginationConfig.page = 1;
+
+ const hasNext: boolean = component.hasNext();
+ expect(hasNext).toBe(true);
+ });
+
+ it("should return FALSE if there's not a page after the current", () => {
+ component.paginationConfig.page = 2;
+
+ const hasNext: boolean = component.hasNext();
+ expect(hasNext).toBe(false);
+ });
+ });
+
+ describe("hasPrevious", () => {
+ beforeEach(() => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ totalItems: 6
+ };
+ });
+
+ it("should return TRUE if there's a page before the current", () => {
+ component.paginationConfig.page = 2;
+
+ const hasPrevious: boolean = component.hasPrevious();
+ expect(hasPrevious).toBe(true);
+ });
+
+ it("should return FALSE if there's not a page before the current", () => {
+ component.paginationConfig.page = 1;
+
+ const hasPrevious: boolean = component.hasPrevious();
+ expect(hasPrevious).toBe(false);
+ });
+ });
+
+ describe("getTotalPages", () => {
+ beforeEach(() => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ totalItems: 6
+ };
+ });
+
+ it("should return the number of pages based on number of items per page", () => {
+ component.paginationConfig.itemsPerPage = 4;
+ component.paginationConfig.totalItems = 10;
+
+ const pages: number = component.getTotalPages();
+ expect(pages).toBe(3);
+ });
+
+ it("should return the number of pages when itemsPerPages equals zero", () => {
+ component.paginationConfig.itemsPerPage = 0;
+ component.paginationConfig.totalItems = 10;
+
+ const pages: number = component.getTotalPages();
+ expect(pages).toBe(10);
+ });
+
+ it("should return NaN if there's not totalItems value", () => {
+ component.paginationConfig.itemsPerPage = 4;
+ component.paginationConfig.totalItems = undefined;
+
+ const pages: number = component.getTotalPages();
+ expect(pages).toBeNaN();
+ });
+ });
+
+ describe("goToLast", () => {
+ beforeEach(() => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ totalItems: 9
+ };
+ });
+
+ it("should not increment the page if the current page is the last", () => {
+ component.paginationConfig.page = 3;
+
+ component.goToNext();
+ expect(component.paginationConfig.page).toBe(3);
+ });
+
+ it("should go to the last page if the current page is not the last", () => {
+ component.paginationConfig.page = 1;
+
+ component.goToLast();
+ expect(component.paginationConfig.page).toBe(3);
+ });
+
+ it("should call onChangePagination if the current page is not the last", () => {
+ component.paginationConfig.page = 1;
+
+ spyOn(component, "onChangePagination");
+ component.goToLast();
+ expect(component.onChangePagination).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("goToNext", () => {
+ beforeEach(() => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ totalItems: 9
+ };
+ });
+
+ it("should not increment the page if the current page is the last", () => {
+ component.paginationConfig.page = 3;
+
+ component.goToNext();
+ expect(component.paginationConfig.page).toBe(3);
+ });
+
+ it("should increment the page if the current page is not the last", () => {
+ component.paginationConfig.page = 1;
+
+ component.goToNext();
+ expect(component.paginationConfig.page).toBe(2);
+ });
+
+ it("should call onChangePagination if the current page is not the last", () => {
+ component.paginationConfig.page = 1;
+
+ spyOn(component, "onChangePagination");
+ component.goToNext();
+ expect(component.onChangePagination).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("goToPrevious", () => {
+ beforeEach(() => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ totalItems: 9
+ };
+ });
+
+ it("should not decrement the page if the current page is the first", () => {
+ component.paginationConfig.page = 1;
+
+ component.goToPrevious();
+ expect(component.paginationConfig.page).toBe(1);
+ });
+
+ it("should decrement the page if the current page is not the first", () => {
+ component.paginationConfig.page = 2;
+
+ component.goToPrevious();
+ expect(component.paginationConfig.page).toBe(1);
+ });
+
+ it("should call onChangePagination if the current page is not the first", () => {
+ component.paginationConfig.page = 2;
+
+ spyOn(component, "onChangePagination");
+ component.goToPrevious();
+ expect(component.onChangePagination).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("goToFirst", () => {
+ beforeEach(() => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ totalItems: 9
+ };
+ });
+
+ it("should not decrement the page if the current page is the first", () => {
+ component.paginationConfig.page = 1;
+
+ component.goToFirst();
+ expect(component.paginationConfig.page).toBe(1);
+ });
+
+ it("should go to the first page if the current page is not the first", () => {
+ component.paginationConfig.page = 3;
+
+ component.goToFirst();
+ expect(component.paginationConfig.page).toBe(1);
+ });
+
+ it("should call onChangePagination if the current page is not the first", () => {
+ component.paginationConfig.page = 2;
+
+ spyOn(component, "onChangePagination");
+ component.goToFirst();
+ expect(component.onChangePagination).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("setPageNumbers", () => {
+ beforeEach(() => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ itemsPerPageOptions: [4, 8, 12]
+ };
+ });
+
+ it("should generate pageNumbers with 1-2-3", () => {
+ component.paginationConfig.page = 2;
+ component.paginationConfig.totalItems = 10;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, 2, 3]);
+ });
+
+ it("should generate pageNumbers with 1-2-3-4-5", () => {
+ component.paginationConfig.page = 2;
+ component.paginationConfig.totalItems = 20;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, 2, 3, 4, 5]);
+ });
+
+ it("should generate pageNumbers with 1-2-3-...-6", () => {
+ component.paginationConfig.page = 1;
+ component.paginationConfig.totalItems = 21;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, 2, 3, "...", 6]);
+ });
+
+ it("should generate pageNumbers with 1-2-3-...-6 when the current page is the second", () => {
+ component.paginationConfig.page = 2;
+ component.paginationConfig.totalItems = 21;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, 2, 3, "...", 6]);
+ });
+
+ it("should generate pageNumbers with 1-...-4-5-6 when the current page is the before last", () => {
+ component.paginationConfig.page = 5;
+ component.paginationConfig.totalItems = 21;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, "...", 4, 5, 6]);
+ });
+
+ it("should generate pageNumbers with 1-...-5-6 when the current page is the last", () => {
+ component.paginationConfig.page = 6;
+ component.paginationConfig.totalItems = 21;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, 2, 3, "...", 6]);
+ });
+
+ it("should generate pageNumbers with 1-...-4-5-6 when the current page is lastPage-2", () => {
+ component.paginationConfig.page = 4;
+ component.paginationConfig.totalItems = 21;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, "...", 4, 5, 6]);
+ });
+
+ it("should generate pageNumbers with 1-...-4-...-7 when the current page is the first", () => {
+ component.paginationConfig.page = 1;
+ component.paginationConfig.totalItems = 28;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, "...", 4, "...", 7]);
+ });
+
+ it("should generate pageNumbers with 1-...-4-...-7 when the current page is in the middle", () => {
+ component.paginationConfig.page = 4;
+ component.paginationConfig.totalItems = 28;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, "...", 4, "...", 7]);
+ });
+
+ it("should generate pageNumbers with 1-...-5-6-7 if the current page is the before last", () => {
+ component.paginationConfig.page = 6;
+ component.paginationConfig.totalItems = 28;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, "...", 5, 6, 7]);
+ });
+
+ it("should generate pageNumbers with 1-...-4-...-7 if the current page is the last", () => {
+ component.paginationConfig.page = 7;
+ component.paginationConfig.totalItems = 28;
+
+ component.setPageNumbers();
+ expect(component.pageNumbers).toEqual([1, "...", 4, "...", 7]);
+ });
+ });
+
+ describe("goToPage", () => {
+ beforeEach(() => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ page: 1,
+ totalItems: 10
+ };
+ });
+
+ it("should not change page when page is ...", () => {
+ component.goToPage("...");
+ expect(component.paginationConfig.page).toBe(1);
+ });
+
+ it("should not call onChangePagination function when page is ...", () => {
+ spyOn(component, "onChangePagination");
+ component.goToPage("...");
+ expect(component.onChangePagination).not.toHaveBeenCalled();
+ });
+
+ it("should change page if page is 2", () => {
+ component.goToPage("...");
+ expect(component.paginationConfig.page).toBe(1);
+ });
+
+ it("should call onChangePagination function when page is 2", () => {
+ spyOn(component, "onChangePagination");
+ component.goToPage(2);
+ expect(component.onChangePagination).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("getPageInputMaxDigits", () => {
+ it("should return 3 when total of pages number is composed of 3 digits", () => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ page: 1,
+ totalItems: 680
+ };
+
+ const getPageInputMaxDigits: number = component.getPageInputMaxDigits();
+ expect(getPageInputMaxDigits).toBe(3);
+ });
+
+ it("should return 1 when total of pages number is composed of 1 digit", () => {
+ component.paginationConfig = {
+ itemsPerPage: 4,
+ page: 1,
+ totalItems: 20
+ };
+
+ const getPageInputMaxDigits: number = component.getPageInputMaxDigits();
+ expect(getPageInputMaxDigits).toBe(1);
+ });
+ });
+
+ describe("normalizePaginationConfig", () => {
+ it("should set paginationConfig.totalItems to zero if paginationConfig is not defined", () => {
+ const normalizedConfig: StarkPaginationConfig = component.normalizePaginationConfig(undefined);
+ expect(normalizedConfig).toEqual({ totalItems: 0 });
+ });
+
+ it("should set paginationConfig with default values if paginationConfig object is empty", () => {
+ const paginationConfigDefault: StarkPaginationConfig = {
+ page: 1,
+ itemsPerPage: 5,
+ itemsPerPageOptions: [5, 10, 15],
+ isExtended: false,
+ totalItems: 0,
+ itemsPerPageIsPresent: true,
+ pageNavIsPresent: true,
+ pageInputIsPresent: true
+ };
+
+ const normalizedConfig: StarkPaginationConfig = component.normalizePaginationConfig({});
+ expect(normalizedConfig).toEqual(paginationConfigDefault);
+ });
+
+ it("should NOT change paginationConfig if all properties of paginationConfig are already set", () => {
+ const fullPaginationConfig: StarkPaginationConfig = {
+ ...paginationConfig,
+ itemsPerPageIsPresent: true,
+ pageNavIsPresent: true,
+ pageInputIsPresent: true
+ };
+ const normalizedConfig: StarkPaginationConfig = component.normalizePaginationConfig(fullPaginationConfig);
+ expect(normalizedConfig).toEqual(fullPaginationConfig);
+ });
+ });
+
+ describe("onChangeItemsPerPage", () => {
+ beforeEach(() => {
+ component.paginationConfig = { ...paginationConfig };
+ });
+
+ it("should change page to 1", () => {
+ component.onChangeItemsPerPage((component.paginationConfig.itemsPerPageOptions)[1]);
+ expect(component.paginationConfig.page).toBe(1);
+ });
+
+ it("should call setPageNumbers 1 time", () => {
+ spyOn(component, "setPageNumbers");
+ component.onChangeItemsPerPage((component.paginationConfig.itemsPerPageOptions)[1]);
+ expect(component.setPageNumbers).toHaveBeenCalledTimes(1);
+ });
+
+ it("should call onChangePagination 1 time", () => {
+ spyOn(component, "onChangePagination");
+ component.onChangeItemsPerPage((component.paginationConfig.itemsPerPageOptions)[1]);
+ expect(component.onChangePagination).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("onChangePagination", () => {
+ beforeEach(() => {
+ component.paginationConfig = { ...paginationConfig };
+ });
+
+ it("should emit the StarkPaginateEvent 1 time", () => {
+ const mockObserver: SpyObj> = createSpyObj>("observerSpy", ["next", "error", "complete"]);
+
+ component.paginated.subscribe(mockObserver);
+ component.onChangePagination();
+
+ expect(mockObserver.next).toHaveBeenCalledTimes(1);
+ const event: StarkPaginateEvent = mockObserver.next.calls.argsFor(0)[0];
+ expect(event).toBeDefined();
+ expect(event.itemsPerPage).toBe(component.paginationConfig.itemsPerPage);
+ expect(event.page).toBe(component.paginationConfig.page);
+
+ expect(mockObserver.error).not.toHaveBeenCalled();
+ expect(mockObserver.complete).not.toHaveBeenCalled();
+ });
+
+ it("should call setPageNumbers function 1 time", () => {
+ spyOn(component, "setPageNumbers");
+ component.onChangePagination();
+ expect(component.setPageNumbers).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("page nav", () => {
+ const selectorPageNavElement: string = ".stark-pagination ul";
+
+ it("should be rendered if pageNavIsPresent is true or undefined", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, pageNavIsPresent: true };
+ hostFixture.detectChanges();
+
+ const pageNavElement: DebugElement = hostFixture.debugElement.query(By.css(selectorPageNavElement));
+ expect(pageNavElement).toBeDefined();
+ expect(pageNavElement).not.toBeNull();
+ });
+
+ it("should be rendered if pageNavIsPresent is undefined", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, pageNavIsPresent: undefined };
+ hostFixture.detectChanges();
+
+ const pageNavElement: DebugElement = hostFixture.debugElement.query(By.css(selectorPageNavElement));
+ expect(pageNavElement).toBeDefined();
+ expect(pageNavElement).not.toBeNull();
+ });
+
+ it("should NOT be rendered if pageNavIsPresent is false", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, pageNavIsPresent: false };
+ hostFixture.detectChanges();
+
+ const pageNavElement: DebugElement = hostFixture.debugElement.query(By.css(selectorPageNavElement));
+ expect(pageNavElement).toBeNull();
+ });
+ });
+
+ describe("page input", () => {
+ const selectorPageSelector: string = ".stark-pagination div.pagination-enter-page";
+
+ it("should be rendered if pageInputIsPresent is undefined", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, pageInputIsPresent: undefined };
+ hostFixture.detectChanges();
+
+ const pageSelector: DebugElement = hostFixture.debugElement.query(By.css(selectorPageSelector));
+
+ const pageSelectorInput: DebugElement = pageSelector.query(By.css("input"));
+ expect(pageSelectorInput.properties["id"]).toBe("current-page-" + "pagination");
+ expect(pageSelectorInput.attributes["ng-reflect-model"]).toBe("2");
+ const pageSelectorTotalPages: DebugElement = pageSelector.query(By.css("span.total-pages"));
+ expect(pageSelectorTotalPages.nativeElement.innerHTML).toBe("2");
+ });
+
+ it("should be rendered if pageInputIsPresent is true", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, pageInputIsPresent: true };
+ hostFixture.detectChanges();
+
+ const pageSelector: DebugElement = hostFixture.debugElement.query(By.css(selectorPageSelector));
+
+ const pageSelectorInput: DebugElement = pageSelector.query(By.css("input"));
+ expect(pageSelectorInput.properties["id"]).toBe("current-page-" + "pagination");
+ expect(pageSelectorInput.attributes["ng-reflect-model"]).toBe("2");
+ const pageSelectorTotalPages: DebugElement = pageSelector.query(By.css("span.total-pages"));
+ expect(pageSelectorTotalPages.nativeElement.innerHTML).toBe("2");
+ });
+
+ it("should NOT be rendered if pageInputIsPresent is false", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, pageInputIsPresent: false };
+ hostFixture.detectChanges();
+
+ const pageSelector: DebugElement = hostFixture.debugElement.query(By.css(selectorPageSelector));
+ expect(pageSelector).toBeNull();
+ });
+
+ it("should trigger the pagination when a valid page is typed and the Enter key is pressed", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, pageInputIsPresent: true };
+ hostFixture.detectChanges();
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+
+ changeInputValueAndPressEnter(hostFixture.debugElement.childNodes[0], "1");
+
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ }));
+
+ it("should NOT trigger the pagination when an invalid page is typed and the Enter key is pressed", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, pageInputIsPresent: true };
+ hostFixture.detectChanges();
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+
+ changeInputValueAndPressEnter(hostFixture.debugElement.childNodes[0], "4");
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2"); // the input value is reverted to the last valid value
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ }));
+ });
+
+ describe("itemsPerPage dropdown", () => {
+ it("should be rendered if itemsPerPageIsPresent is undefined", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, itemsPerPageIsPresent: undefined };
+ hostFixture.detectChanges();
+
+ const itemsPerPageSelector: DebugElement = hostFixture.debugElement.query(By.directive(StarkDropdownComponent));
+ expect(itemsPerPageSelector).not.toBeNull();
+ expect(itemsPerPageSelector).toBeDefined();
+ expect(itemsPerPageSelector.attributes["ng-reflect-dropdown-id"]).toBe("items-per-page-" + "pagination");
+ // expect(itemsPerPageSelector.attr("header")).toBe("STARK.PAGINATION.ITEMS_PER_PAGE"); // TODO add a header to the itemsPerPage dropdown
+ });
+
+ it("should be rendered if itemsPerPageIsPresent is true", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, itemsPerPageIsPresent: true };
+ hostFixture.detectChanges();
+
+ const itemsPerPageSelector: DebugElement = hostFixture.debugElement.query(By.directive(StarkDropdownComponent));
+ expect(itemsPerPageSelector).not.toBeNull();
+ expect(itemsPerPageSelector).toBeDefined();
+ expect(itemsPerPageSelector.attributes["ng-reflect-dropdown-id"]).toBe("items-per-page-" + "pagination");
+ // expect(itemsPerPageSelector.attr("header")).toBe("STARK.PAGINATION.ITEMS_PER_PAGE"); // TODO add a header to the itemsPerPage dropdown
+ });
+
+ it("should NOT be rendered if pageInputIsPresent is false", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, itemsPerPageIsPresent: false };
+ hostFixture.detectChanges();
+
+ const itemsPerPageSelector: DebugElement = hostFixture.debugElement.query(By.directive(StarkDropdownComponent));
+ expect(itemsPerPageSelector).toBeNull();
+ });
+ });
+
+ describe("navigation buttons", () => {
+ describe("goToFirst", () => {
+ let firstButtonElement: DebugElement | null;
+ const selectorFirstButtonElement: string = "li.first-page button";
+
+ it("button should not be rendered in extended mode", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 2, isExtended: true };
+ hostFixture.detectChanges();
+
+ firstButtonElement = hostFixture.debugElement.query(By.css(selectorFirstButtonElement));
+ expect(firstButtonElement).toBeNull();
+ });
+
+ it("should change the page when the page is not already the first one", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 2, isExtended: false };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+
+ firstButtonElement = hostFixture.debugElement.query(By.css(selectorFirstButtonElement));
+ expect(firstButtonElement.properties["disabled"]).toBe(false);
+ firstButtonElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ firstButtonElement = hostFixture.debugElement.query(By.css(selectorFirstButtonElement));
+ expect(firstButtonElement.properties["disabled"]).toBe(true);
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+ }));
+
+ it("should not change the page if the page is already the first one", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 1, isExtended: false };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+
+ firstButtonElement = hostFixture.debugElement.query(By.css(selectorFirstButtonElement));
+ expect(firstButtonElement.properties["disabled"]).toBe(true);
+ firstButtonElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ firstButtonElement = hostFixture.debugElement.query(By.css(selectorFirstButtonElement));
+ expect(firstButtonElement.properties["disabled"]).toBe(true);
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+ }));
+ });
+
+ describe("goToPrevious", () => {
+ let previousButtonElement: DebugElement;
+ const selectorPreviousButtonElement: string = "li.previous button";
+
+ it("should change the page when the page is not already the first one", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 2 };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+
+ previousButtonElement = hostFixture.debugElement.query(By.css(selectorPreviousButtonElement));
+ expect(previousButtonElement.properties["disabled"]).toBe(false);
+ previousButtonElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ previousButtonElement = hostFixture.debugElement.query(By.css(selectorPreviousButtonElement));
+ expect(previousButtonElement.properties["disabled"]).toBe(true);
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+ }));
+
+ it("should not change the page if the page is already the first one", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 1 };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+
+ previousButtonElement = hostFixture.debugElement.query(By.css(selectorPreviousButtonElement));
+ expect(previousButtonElement.properties["disabled"]).toBe(true);
+ previousButtonElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ previousButtonElement = hostFixture.debugElement.query(By.css(selectorPreviousButtonElement));
+ expect(previousButtonElement.properties["disabled"]).toBe(true);
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+ }));
+ });
+
+ describe("goToNext", () => {
+ let nextButtonElement: DebugElement;
+ const selectorNextButtonElement: string = "li.next button";
+
+ it("should change the page if the page is not already the last one", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 1 };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+
+ nextButtonElement = hostFixture.debugElement.query(By.css(selectorNextButtonElement));
+ expect(nextButtonElement.properties["disabled"]).toBe(false);
+ nextButtonElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ nextButtonElement = hostFixture.debugElement.query(By.css(selectorNextButtonElement));
+ expect(nextButtonElement.properties["disabled"]).toBe(true);
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+ }));
+
+ it("should not change the page if the page is already the last one", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 2 };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+
+ nextButtonElement = hostFixture.debugElement.query(By.css(selectorNextButtonElement));
+ expect(nextButtonElement.properties["disabled"]).toBe(true);
+ nextButtonElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ nextButtonElement = hostFixture.debugElement.query(By.css(selectorNextButtonElement));
+ expect(nextButtonElement.properties["disabled"]).toBe(true);
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+ }));
+ });
+
+ describe("goToLast", () => {
+ let lastButtonElement: DebugElement | null;
+ const selectorLastButtonElement: string = "li.last-page button";
+
+ it("button should not be rendered in extended mode", () => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 2, isExtended: true };
+ hostFixture.detectChanges();
+
+ lastButtonElement = hostFixture.debugElement.query(By.css("li.first-page button"));
+ expect(lastButtonElement).toBeNull();
+ });
+
+ it("should change the page if the page is not already the last one", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 1, isExtended: false };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+
+ lastButtonElement = hostFixture.debugElement.query(By.css(selectorLastButtonElement));
+ expect(lastButtonElement.properties["disabled"]).toBe(false);
+ lastButtonElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ lastButtonElement = hostFixture.debugElement.query(By.css(selectorLastButtonElement));
+ expect(lastButtonElement.properties["disabled"]).toBe(true);
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+ }));
+
+ it("should not change the page if the page is already the last one", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 2, isExtended: false };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+
+ lastButtonElement = hostFixture.debugElement.query(By.css(selectorLastButtonElement));
+ expect(lastButtonElement.properties["disabled"]).toBe(true);
+ lastButtonElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ lastButtonElement = hostFixture.debugElement.query(By.css(selectorLastButtonElement));
+ expect(lastButtonElement.properties["disabled"]).toBe(true);
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+ }));
+ });
+ });
+
+ describe("pageNumbers", () => {
+ it("should change page if click on page number", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 2, totalItems: 10 };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+
+ const pageTwoElement: DebugElement = hostFixture.debugElement.query(By.css("li[value='3'] a"));
+ pageTwoElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "3");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "3");
+ }));
+
+ it("should not change page if click on '...'", fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, page: 1, totalItems: 50 };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+
+ // Angular sets the 'value' attribute of elements to "0" instead of "..."
+ const morePagesElement: DebugElement = hostFixture.debugElement.query(By.css("li[value='0']"));
+ morePagesElement.triggerEventHandler("click", {});
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+ }));
+ });
+
+ describe("on paginationConfig change", () => {
+ beforeEach(fakeAsync(() => {
+ hostComponent.paginationConfig = { ...paginationConfig, totalItems: 10 };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+ }));
+
+ it("should change pageNumbers if totalItems has changed", () => {
+ const previousPageNumbersLength: number = hostFixture.debugElement.queryAll(By.css("li.page-numbers")).length;
+ hostComponent.paginationConfig = { ...paginationConfig, totalItems: 13 };
+ hostFixture.detectChanges();
+ const currentPageNumbersElement: number = hostFixture.debugElement.queryAll(By.css("li.page-numbers")).length;
+ expect(currentPageNumbersElement).not.toEqual(previousPageNumbersLength);
+ });
+
+ it("should not change pageNumbers if totalItems has not changed", () => {
+ const previousPageNumbersLength: number = hostFixture.debugElement.queryAll(By.css("li.page-numbers")).length;
+ hostComponent.paginationConfig = { ...paginationConfig, totalItems: 10 };
+ hostFixture.detectChanges();
+ const currentPageNumbersElement: number = hostFixture.debugElement.queryAll(By.css("li.page-numbers")).length;
+ expect(currentPageNumbersElement).toEqual(previousPageNumbersLength);
+ });
+
+ it("should set current page to 1 when it is undefined in config", fakeAsync(() => {
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "2");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "2");
+
+ hostComponent.paginationConfig = { ...paginationConfig, page: undefined };
+ hostFixture.detectChanges();
+ tick(); // since values are set on ngModel asynchronously (see https://github.com/angular/angular/issues/22606)
+
+ assertPageNavSelection(hostFixture.debugElement.childNodes[0], "1");
+ assertPageInputSelection(hostFixture.debugElement.childNodes[0], "1");
+ }));
+ });
+});
diff --git a/packages/stark-ui/src/modules/pagination/components/pagination.component.ts b/packages/stark-ui/src/modules/pagination/components/pagination.component.ts
new file mode 100644
index 0000000000..35e4c3d104
--- /dev/null
+++ b/packages/stark-ui/src/modules/pagination/components/pagination.component.ts
@@ -0,0 +1,430 @@
+"use strict";
+
+import {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ EventEmitter,
+ HostBinding,
+ Inject,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ Renderer2,
+ SimpleChanges,
+ ViewEncapsulation
+} from "@angular/core";
+import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core";
+import { StarkPaginationConfig } from "./pagination-config.intf";
+import { MatPaginator, MatPaginatorIntl, PageEvent } from "@angular/material/paginator";
+
+const _isEqual: Function = require("lodash/isEqual");
+
+const componentName: string = "stark-pagination";
+
+export type StarkPaginationComponentMode = "compact";
+
+export interface StarkPaginateEvent {
+ /**
+ * Current page after pagination
+ */
+ page: number;
+
+ /**
+ * Current number of items displayed per page
+ */
+ itemsPerPage: number;
+}
+
+/**
+ * Component to display pagination bar to be used with a collection of items.
+ * It extends the MatPaginator class from Angular Material so it can be integrated as well with the MatTable.
+ * {@link https://material.angular.io/components/paginator/api|MatPaginator}
+ */
+@Component({
+ selector: "stark-pagination",
+ templateUrl: "./pagination.component.html",
+ encapsulation: ViewEncapsulation.None
+})
+export class StarkPaginationComponent extends MatPaginator implements OnInit, OnChanges, AfterViewInit {
+ /**
+ * Adds class="stark-pagination" attribute on the host component
+ */
+ @HostBinding("class")
+ public class: string = componentName;
+
+ /**
+ * Suffix id given to items per page dropdown
+ * (items-per-page-) and pageSelector dropdown (page-selector-)
+ * Default: "pagination"
+ */
+ @Input()
+ public htmlSuffixId?: string;
+
+ /**
+ * Desired layout or flavour:
+ * - compact: Displayed in a compact mode.
+ * - default: basic implementation with everything
+ */
+ @Input()
+ public mode?: StarkPaginationComponentMode;
+
+ /**
+ * StarkPaginationConfig object containing main information for the pagination.
+ */
+ @Input()
+ public paginationConfig: StarkPaginationConfig;
+
+ /**
+ * Output event emitter that will emit the paginate event when the pagination changed.
+ */
+ @Output()
+ public paginated: EventEmitter = new EventEmitter();
+
+ public get paginationInput(): number {
+ return this._paginationInput;
+ }
+
+ public set paginationInput(newValue: number) {
+ // store the previous pagination input value in case the new one is not valid
+ // so it can be reverted to the previous value when that happens
+ if (this._paginationInput && (newValue > this.getTotalPages() || newValue === 0)) {
+ this.previousPaginationInput = this._paginationInput;
+ }
+ this._paginationInput = newValue;
+ }
+
+ public _paginationInput: number;
+ public previousPaginationInput: number;
+ public previousPageIndex: number;
+ public pageNumbers: (string | number)[];
+
+ public constructor(
+ @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService,
+ public element: ElementRef,
+ public renderer: Renderer2,
+ public cdRef: ChangeDetectorRef
+ ) {
+ // we don't use the MatPaginatorIntl service to translate the labels but it is needed for the MatPaginator base class
+ // see https://material.angular.io/components/paginator/api#services
+ super(new MatPaginatorIntl(), cdRef);
+ }
+
+ /**
+ * Component lifecycle hook
+ */
+ public ngOnInit(): void {
+ this.paginationConfig = this.normalizePaginationConfig(this.paginationConfig);
+ this.setMatPaginatorProperties(this.paginationConfig);
+ this.previousPageIndex = 0;
+
+ this.htmlSuffixId = this.htmlSuffixId || "pagination";
+
+ this.setPageNumbers();
+
+ super.ngOnInit();
+ this.logger.debug(componentName + ": controller initialized");
+ }
+
+ /**
+ * Component lifecycle hook
+ */
+ public ngAfterViewInit(): void {
+ if (this.paginationConfig.isExtended) {
+ this.renderer.addClass(this.element.nativeElement, "extended-pagination");
+ }
+ }
+
+ /**
+ * Component lifecycle hook
+ */
+ public ngOnChanges(changesObj: SimpleChanges): void {
+ if (changesObj["paginationConfig"]) {
+ // Set local variable to prevent shadow changes
+ const paginationConfigOriginalChange: StarkPaginationConfig = { ...this.paginationConfig };
+ this.paginationConfig = this.normalizePaginationConfig(this.paginationConfig);
+ this.logger.debug(componentName + ": paginationConfig changed...", this.paginationConfig);
+
+ // If normalization has changed the page or itemsPerPage, that means the paginationConfig is not the same in the pagination controller
+ // and in the parent controller. So pagination hast to trigger onPaginate callback to pass the new values.
+ if (
+ typeof paginationConfigOriginalChange === "undefined" ||
+ paginationConfigOriginalChange.page !== this.paginationConfig.page ||
+ paginationConfigOriginalChange.itemsPerPage !== this.paginationConfig.itemsPerPage
+ ) {
+ this.onChangePagination();
+ } else if (
+ !_isEqual(paginationConfigOriginalChange, changesObj["paginationConfig"].previousValue) ||
+ paginationConfigOriginalChange.totalItems !== this.paginationConfig.totalItems ||
+ paginationConfigOriginalChange.itemsPerPageOptions !== this.paginationConfig.itemsPerPageOptions
+ ) {
+ this.setPageNumbers();
+ }
+ this.paginationInput = this.paginationConfig.page;
+ }
+ }
+
+ /**
+ * Creates a normalized paginationConfig to be used by this component.
+ * If the given config is undefined it will set totalItems only, otherwise it sets default values for the missing properties
+ */
+ // FIXME: refactor this function to reduce its cognitive complexity
+ public normalizePaginationConfig(config: StarkPaginationConfig | undefined): StarkPaginationConfig {
+ let normalizedConfig: StarkPaginationConfig;
+ if (!config) {
+ // initialize paginationConfig to prevent errors in other functions depending on this config
+ normalizedConfig = {
+ totalItems: 0
+ };
+ this.logger.warn(componentName + ": No configuration defined. TotalItems set to 0 by default");
+ } else {
+ normalizedConfig = {
+ itemsPerPageOptions: config.itemsPerPageOptions || [5, 10, 15],
+ itemsPerPage: config.itemsPerPage || (config.itemsPerPageOptions ? config.itemsPerPageOptions[0] : 5),
+ page: config.page || 1,
+ isExtended: config.isExtended !== undefined ? config.isExtended : false,
+ itemsPerPageIsPresent: config.itemsPerPageIsPresent !== undefined ? config.itemsPerPageIsPresent : true,
+ pageNavIsPresent: config.pageNavIsPresent !== undefined ? config.pageNavIsPresent : true,
+ pageInputIsPresent: config.pageInputIsPresent !== undefined ? config.pageInputIsPresent : true,
+ totalItems: config.totalItems !== undefined ? config.totalItems : 0
+ };
+ this.logger.debug(componentName + ": normalized pagination config: ", normalizedConfig);
+ }
+
+ return normalizedConfig;
+ }
+
+ /**
+ * Set the properties needed for the MatPaginator base class based on the given pagination configuration
+ * {@link https://material.angular.io/components/paginator/api#MatPaginator|MatPaginator API}
+ * @param config - The config object which be used to set the MatPaginator properties
+ */
+ public setMatPaginatorProperties(config: StarkPaginationConfig): void {
+ // The set of provided page size options to display to the user.
+ this.pageSizeOptions = config.itemsPerPageOptions;
+ // Number of items to display on a page. By default set to 50.
+ this.pageSize = config.itemsPerPage;
+ // The zero-based page index of the displayed list of items. Defaulted to 0.
+ this.pageIndex = config.page - 1; // zero-based
+ // The length of the total number of items that are being paginated. Defaulted to 0.
+ this.length = config.totalItems;
+ }
+
+ /**
+ * Check whether the given value is equal to zero (as number 0 or as string "0").
+ */
+ public isZero(numberToCheck: string | number): boolean {
+ return numberToCheck === 0 || numberToCheck === "0";
+ }
+
+ /**
+ * Check whether there is a page after the current one.
+ */
+ public hasNext(): boolean {
+ return this.paginationConfig && this.paginationConfig.page < this.getTotalPages();
+ }
+
+ /**
+ * Check whether there is a page before the current one.
+ */
+ public hasPrevious(): boolean {
+ return this.paginationConfig && this.paginationConfig.page > 1;
+ }
+
+ /**
+ * Change page to first one.
+ */
+ public goToFirst(): void {
+ if (this.hasPrevious()) {
+ this.goToPage(1);
+ }
+ }
+
+ /**
+ * Change page to previous one.
+ */
+ public goToPrevious(): void {
+ if (this.hasPrevious()) {
+ this.goToPage(this.paginationConfig.page - 1);
+ }
+ }
+
+ /**
+ * Change page to next one.
+ */
+ public goToNext(): void {
+ if (this.hasNext()) {
+ this.goToPage(this.paginationConfig.page + 1);
+ }
+ }
+
+ /**
+ * Change page to last one.
+ */
+ public goToLast(): void {
+ if (this.hasNext()) {
+ this.goToPage(this.getTotalPages());
+ }
+ }
+
+ /**
+ * Emit the stark paginate event and the MatPagination event.
+ * Then reload pageNumbers variable.
+ */
+ public onChangePagination(): void {
+ if (
+ this.paginationConfig &&
+ // Check the types of page & itemsPerPage to be sure they are not undefined
+ typeof this.paginationConfig.page === "number" &&
+ typeof this.paginationConfig.itemsPerPage === "number"
+ ) {
+ this.paginated.emit({
+ page: this.paginationConfig.page,
+ itemsPerPage: this.paginationConfig.itemsPerPage
+ });
+
+ this.setMatPaginatorProperties(this.paginationConfig);
+ this.emitMatPaginationEvent();
+ }
+ this.setPageNumbers();
+ this.paginationInput = this.paginationConfig.page;
+ }
+
+ /**
+ * Get total number of pages available based on itemsPerPage and totalItems.
+ */
+ public getTotalPages(): number {
+ let calculatedTotalPages: number = 0;
+ if (this.paginationConfig) {
+ const itemsPerPage: number = this.isZero(this.paginationConfig.itemsPerPage)
+ ? 1
+ : this.paginationConfig.itemsPerPage;
+ calculatedTotalPages = Math.ceil(this.paginationConfig.totalItems / itemsPerPage);
+ }
+
+ if (calculatedTotalPages === 0) {
+ return 1;
+ }
+ return calculatedTotalPages;
+ }
+
+ /**
+ * Set page to first then call onChangePagination function.
+ */
+ public onChangeItemsPerPage(itemsPerPage: number): void {
+ this.paginationConfig.page = 1;
+ this.paginationConfig.itemsPerPage = itemsPerPage;
+ this.onChangePagination();
+ }
+
+ /**
+ * Set pageNumbers variable.
+ */
+ // FIXME: refactor this function to reduce its cognitive complexity
+ public setPageNumbers(): void {
+ let min: number;
+ let max: number;
+ let i: number;
+ let j: number;
+
+ const input: (string | number)[] = [];
+
+ if (this.isCompactMode()) {
+ min = this.paginationConfig.page > 1 ? this.paginationConfig.page - 1 : 1;
+ max = min + 2;
+
+ for (j = 0, i = min; i <= max && i <= this.getTotalPages(); i++, j++) {
+ input[j] = i;
+ }
+ } else {
+ // default mode: stark
+ min = 1;
+ max = this.getTotalPages();
+
+ if (max < 6) {
+ for (j = 0, i = min; i <= max; i++, j++) {
+ input[j] = i;
+ }
+ } else {
+ input[0] = min;
+ input[4] = max;
+
+ if (this.paginationConfig.page === min + 2 || this.paginationConfig.page === min + 1) {
+ input[2] = min + 2;
+ } else if (this.paginationConfig.page === max - 2 || this.paginationConfig.page === max - 1) {
+ input[2] = max - 2;
+ } else if (this.paginationConfig.page === max || this.paginationConfig.page === min) {
+ input[2] = Math.ceil(max / 2);
+ } else {
+ input[2] = this.paginationConfig.page;
+ }
+
+ if (input[2] - 1 === min + 1) {
+ input[1] = min + 1;
+ } else {
+ input[1] = "...";
+ }
+
+ if (input[2] + 1 === max - 1) {
+ input[3] = max - 1;
+ } else {
+ input[3] = "...";
+ }
+ }
+ }
+
+ this.pageNumbers = input;
+ }
+
+ /**
+ * Change to the given page if it is different than "...". It calls onChangePagination afterwards.
+ */
+ public goToPage(page: number | "..."): void {
+ if (page !== "...") {
+ this.previousPageIndex = this.paginationConfig.page;
+ this.paginationConfig.page = page;
+ this.onChangePagination();
+ }
+ }
+
+ public changePageOnEnter(): void {
+ const newPage: number = typeof this.paginationInput === "string" ? parseInt(this.paginationInput, 10) : this.paginationInput;
+ if (newPage <= this.getTotalPages() && newPage > 0) {
+ this.goToPage(newPage);
+ } else {
+ this.logger.warn(componentName + ": the page ", newPage, " does not exist");
+ this.paginationInput = this.previousPaginationInput; // revert the pagination input value
+ }
+ }
+
+ public getPageInputMaxDigits(): number {
+ return this.getTotalPages().toString().length;
+ }
+
+ public isCompactMode(): boolean {
+ return typeof this.mode !== "undefined" && this.mode === "compact";
+ }
+
+ /**
+ * Emit the PageEvent according to the MatPaginator API
+ * {@link https://material.angular.io/components/paginator/api#PageEvent|MatPaginator PageEvent}
+ */
+ public emitMatPaginationEvent(): void {
+ const pageEvent: PageEvent = {
+ pageIndex: this.pageIndex,
+ pageSize: this.pageSize,
+ length: this.length,
+ previousPageIndex: this.previousPageIndex
+ };
+ this.page.emit(pageEvent);
+ }
+
+ /**
+ * @ignore
+ */
+ public trackPageNumberFn(_index: number): number {
+ return _index;
+ }
+}
diff --git a/packages/stark-ui/src/modules/pagination/pagination.module.ts b/packages/stark-ui/src/modules/pagination/pagination.module.ts
new file mode 100644
index 0000000000..21b71c08dc
--- /dev/null
+++ b/packages/stark-ui/src/modules/pagination/pagination.module.ts
@@ -0,0 +1,30 @@
+import { NgModule } from "@angular/core";
+import { FormsModule } from "@angular/forms";
+import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
+import { MatIconModule } from "@angular/material/icon";
+import { MatInputModule } from "@angular/material/input";
+import { MatButtonModule } from "@angular/material/button";
+import { MatTooltipModule } from "@angular/material/tooltip";
+import { MatPaginatorModule } from "@angular/material/paginator";
+import { StarkPaginationComponent } from "./components";
+import { StarkSvgViewBoxModule } from "../svg-view-box/svg-view-box.module";
+import { StarkKeyboardDirectivesModule } from "../keyboard-directives/keyboard-directives.module";
+import { StarkDropdownModule } from "../dropdown/dropdown.module";
+
+@NgModule({
+ declarations: [StarkPaginationComponent],
+ exports: [StarkPaginationComponent],
+ imports: [
+ BrowserAnimationsModule,
+ FormsModule,
+ MatButtonModule,
+ MatIconModule,
+ MatInputModule,
+ MatPaginatorModule,
+ MatTooltipModule,
+ StarkKeyboardDirectivesModule,
+ StarkSvgViewBoxModule,
+ StarkDropdownModule
+ ]
+})
+export class StarkPaginationModule {}
diff --git a/packages/stark-ui/src/modules/table/components/column.component.html b/packages/stark-ui/src/modules/table/components/column.component.html
index 52c6118778..8e906f6078 100644
--- a/packages/stark-ui/src/modules/table/components/column.component.html
+++ b/packages/stark-ui/src/modules/table/components/column.component.html
@@ -1,5 +1,6 @@
+