Add pagination component

This commit is contained in:
Giuseppe Digilio
2017-05-19 18:22:41 +02:00
parent e36ebcd72e
commit 412a3a3970
21 changed files with 783 additions and 27 deletions

View File

@@ -78,7 +78,7 @@
"@angular/upgrade": "2.2.3", "@angular/upgrade": "2.2.3",
"@angularclass/bootloader": "1.0.1", "@angularclass/bootloader": "1.0.1",
"@angularclass/idle-preload": "1.0.4", "@angularclass/idle-preload": "1.0.4",
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.15", "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.24",
"@ngrx/core": "^1.2.0", "@ngrx/core": "^1.2.0",
"@ngrx/effects": "^2.0.0", "@ngrx/effects": "^2.0.0",
"@ngrx/router-store": "^1.2.5", "@ngrx/router-store": "^1.2.5",
@@ -90,7 +90,7 @@
"angular2-universal": "2.1.0-rc.1", "angular2-universal": "2.1.0-rc.1",
"angular2-universal-polyfills": "2.1.0-rc.1", "angular2-universal-polyfills": "2.1.0-rc.1",
"body-parser": "1.15.2", "body-parser": "1.15.2",
"bootstrap": "4.0.0-alpha.5", "bootstrap": "4.0.0-alpha.6",
"cerialize": "^0.1.13", "cerialize": "^0.1.13",
"compression": "1.6.2", "compression": "1.6.2",
"express": "4.14.0", "express": "4.14.0",
@@ -100,6 +100,7 @@
"jsonschema": "^1.1.1", "jsonschema": "^1.1.1",
"methods": "1.1.2", "methods": "1.1.2",
"morgan": "1.7.0", "morgan": "1.7.0",
"ng2-pagination": "^2.0.0",
"ng2-translate": "4.2.0", "ng2-translate": "4.2.0",
"preboot": "4.5.2", "preboot": "4.5.2",
"rxjs": "5.0.0-beta.12", "rxjs": "5.0.0-beta.12",

View File

@@ -15,6 +15,14 @@
"home": "Home" "home": "Home"
}, },
"pagination": {
"results-per-page": "Results Per Page",
"showing": {
"label" : "Now showing items ",
"detail": "{{ range }} of {{ total }}"
}
},
"title": "DSpace", "title": "DSpace",
"404": { "404": {

View File

@@ -10,6 +10,19 @@
<span *ngIf="!EnvConfig.production">development</span> <span *ngIf="!EnvConfig.production">development</span>
<span *ngIf="EnvConfig.production">production</span> <span *ngIf="EnvConfig.production">production</span>
</h2> </h2>
{{options.id}}
<!--ds-pagination [paginationOptions]="options"
[collectionSize]="100"
(pageChange)="options.currentPage = $event"
(pageSizeChange)="options.pageSize = $event">
<ul>
<li *ngFor="let item of collection | paginate: { itemsPerPage: options.pageSize, currentPage: options.currentPage, totalItems: 100 }"> {{item}} </li>
</ul>
</ds-pagination-->
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
</main> </main>

View File

@@ -11,6 +11,8 @@ import { HostWindowState } from "./shared/host-window.reducer";
import { Store } from "@ngrx/store"; import { Store } from "@ngrx/store";
import { HostWindowResizeAction } from "./shared/host-window.actions"; import { HostWindowResizeAction } from "./shared/host-window.actions";
import { PaginationOptions } from './core/shared/pagination-options.model';
import { GLOBAL_CONFIG, GlobalConfig } from '../config'; import { GLOBAL_CONFIG, GlobalConfig } from '../config';
@Component({ @Component({
@@ -23,8 +25,9 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../config';
export class AppComponent implements OnDestroy, OnInit { export class AppComponent implements OnDestroy, OnInit {
private translateSubscription: any; private translateSubscription: any;
collection = [];
example: string; example: string;
options: PaginationOptions = new PaginationOptions();
data: any = { data: any = {
greeting: 'Hello', greeting: 'Hello',
recipient: 'World' recipient: 'World'
@@ -39,12 +42,20 @@ export class AppComponent implements OnDestroy, OnInit {
translate.setDefaultLang('en'); translate.setDefaultLang('en');
// the lang to use, if the lang isn't available, it will use the current loader to get them // the lang to use, if the lang isn't available, it will use the current loader to get them
translate.use('en'); translate.use('en');
for (let i = 1; i <= 100; i++) {
this.collection.push(`item ${i}`);
}
} }
ngOnInit() { ngOnInit() {
this.translateSubscription = this.translate.get('example.with.data', { greeting: 'Hello', recipient: 'DSpace' }).subscribe((translation: string) => { this.translateSubscription = this.translate.get('example.with.data', { greeting: 'Hello', recipient: 'DSpace' }).subscribe((translation: string) => {
this.example = translation; this.example = translation;
}); });
this.onLoad();
this.options.id = 'app';
//this.options.currentPage = 1;
this.options.pageSize = 15;
this.options.size = 'sm';
} }
ngOnDestroy() { ngOnDestroy() {
@@ -60,4 +71,9 @@ export class AppComponent implements OnDestroy, OnInit {
); );
} }
private onLoad() {
this.store.dispatch(
new HostWindowResizeAction(window.innerWidth, window.innerHeight)
);
}
} }

View File

@@ -20,7 +20,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
SharedModule, SharedModule,
HomeModule, HomeModule,
CoreModule.forRoot(), CoreModule.forRoot(),
AppRoutingModule, AppRoutingModule
], ],
providers: [ providers: [
] ]

View File

@@ -6,6 +6,7 @@ import {
} from "./request-cache.actions"; } from "./request-cache.actions";
import deepFreeze = require("deep-freeze"); import deepFreeze = require("deep-freeze");
import { OpaqueToken } from "@angular/core"; import { OpaqueToken } from "@angular/core";
import { PaginationOptions } from "../shared/pagination-options.model";
class NullAction extends RequestCacheRemoveAction { class NullAction extends RequestCacheRemoveAction {
type = null; type = null;
@@ -26,7 +27,19 @@ describe("requestCacheReducer", () => {
"be8325f7-243b-49f4-8a4b-df2b793ff3b5" "be8325f7-243b-49f4-8a4b-df2b793ff3b5"
]; ];
const resourceID = "9978"; const resourceID = "9978";
const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 }; const paginationOptions: PaginationOptions = {
"id": "test",
"currentPage": 1,
"pageSizeOptions": [5, 10, 20, 40, 60, 80, 100],
"disabled": false,
"boundaryLinks": false,
"directionLinks": true,
"ellipses": true,
"maxSize": 0,
"pageSize": 10,
"rotate": false,
"size": 'sm'
};
const sortOptions = { "field": "id", "direction": 0 }; const sortOptions = { "field": "id", "direction": 0 };
const testState = { const testState = {
[keys[0]]: { [keys[0]]: {

View File

@@ -4,6 +4,7 @@ import { RequestCacheState, RequestCacheEntry } from "./request-cache.reducer";
import { OpaqueToken } from "@angular/core"; import { OpaqueToken } from "@angular/core";
import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "./request-cache.actions"; import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "./request-cache.actions";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { PaginationOptions } from "../shared/pagination-options.model";
describe("RequestCacheService", () => { describe("RequestCacheService", () => {
let service: RequestCacheService; let service: RequestCacheService;
@@ -12,7 +13,19 @@ describe("RequestCacheService", () => {
const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"]; const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')]; const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')];
const resourceID = "9978"; const resourceID = "9978";
const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 }; const paginationOptions: PaginationOptions = {
"id": "test",
"currentPage": 1,
"pageSizeOptions": [5, 10, 20, 40, 60, 80, 100],
"disabled": false,
"boundaryLinks": false,
"directionLinks": true,
"ellipses": true,
"maxSize": 0,
"pageSize": 10,
"rotate": false,
"size": 'sm'
};
const sortOptions = { "field": "id", "direction": 0 }; const sortOptions = { "field": "id", "direction": 0 };
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
const validCacheEntry = (key) => { const validCacheEntry = (key) => {

View File

@@ -1,5 +1,6 @@
import { NgModule, Optional, SkipSelf, ModuleWithProviders } from '@angular/core'; import { NgModule, Optional, SkipSelf, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SharedModule } from "../shared/shared.module"; import { SharedModule } from "../shared/shared.module";
import { isNotEmpty } from "../shared/empty.util"; import { isNotEmpty } from "../shared/empty.util";
import { FooterComponent } from "./footer/footer.component"; import { FooterComponent } from "./footer/footer.component";
@@ -8,6 +9,7 @@ import { ObjectCacheService } from "./cache/object-cache.service";
import { RequestCacheService } from "./cache/request-cache.service"; import { RequestCacheService } from "./cache/request-cache.service";
import { CollectionDataService } from "./data-services/collection-data.service"; import { CollectionDataService } from "./data-services/collection-data.service";
import { ItemDataService } from "./data-services/item-data.service"; import { ItemDataService } from "./data-services/item-data.service";
import { PaginationOptions } from "./shared/pagination-options.model";
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -27,6 +29,7 @@ const PROVIDERS = [
ItemDataService, ItemDataService,
DSpaceRESTv2Service, DSpaceRESTv2Service,
ObjectCacheService, ObjectCacheService,
PaginationOptions,
RequestCacheService RequestCacheService
]; ];

View File

@@ -1,12 +1,20 @@
export class PaginationOptions { import { NgbPaginationConfig } from '@ng-bootstrap/ng-bootstrap';
export class PaginationOptions extends NgbPaginationConfig {
/** /**
* The number of results per page. * ID for the pagination instance. Only useful if you wish to
* have more than once instance at a time in a given component.
*/ */
resultsPerPage: number = 10; id: string;
/** /**
* The active page. * The active page.
*/ */
currentPage: number = 1; currentPage: number = 1;
/**
* A number array that represents options for a context pagination limit.
*/
pageSizeOptions: Array<number> = [ 5, 10, 20, 40, 60, 80, 100 ];
} }

View File

@@ -1,14 +1,14 @@
<header> <header>
<nav class="navbar navbar-dark bg-inverse"> <nav class="navbar navbar-toggleable-sm navbar-inverse bg-inverse">
<button class="navbar-toggler hidden-sm-up" type="button" (click)="toggle()" aria-controls="collapsingNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler navbar-toggler-right" type="button" (click)="toggle()" aria-controls="collapsingNav" aria-expanded="false" aria-label="Toggle navigation">
<i class="fa fa-bars fa-fw" aria-hidden="true"></i> <span class="navbar-toggler-icon fa fa-bars fa-fw" aria-hidden="true"></span>
</button> </button>
<div [ngClass]="{'clearfix': !(isNavBarCollapsed | async)}"> <div [ngClass]="{'clearfix': !(isNavBarCollapsed | async)}">
<a class="navbar-brand" routerLink="/home"> <a class="navbar-brand" routerLink="/home">
<!-- TODO: add logo here -->{{ 'title' | translate }}</a> <!-- TODO: add logo here -->{{ 'title' | translate }}</a>
</div> </div>
<div [ngbCollapse]="(isNavBarCollapsed | async)" class="collapse navbar-toggleable-xs" id="collapsingNav"> <div [ngbCollapse]="(isNavBarCollapsed | async)" class="collapse navbar-collapse" id="collapsingNav">
<ul class="nav navbar-nav"> <ul class="navbar-nav mr-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="/home" routerLinkActive="active"><i class="fa fa-home fa-fw" aria-hidden="true"></i> {{ 'nav.home' | translate }}<span class="sr-only">(current)</span></a> <a class="nav-link" routerLink="/home" routerLinkActive="active"><i class="fa fa-home fa-fw" aria-hidden="true"></i> {{ 'nav.home' | translate }}<span class="sr-only">(current)</span></a>
</li> </li>

View File

@@ -4,10 +4,11 @@ header nav.navbar {
border-radius: 0rem; border-radius: 0rem;
} }
header nav.navbar .navbar-toggler {
float: right;
}
header nav.navbar .navbar-toggler:hover { header nav.navbar .navbar-toggler:hover {
cursor: pointer; cursor: pointer;
} }
header nav.navbar .navbar-toggler .navbar-toggler-icon {
background-image: none !important;
line-height: 1.5;
}

View File

@@ -58,7 +58,7 @@ describe('HeaderComponent', () => {
}); });
it("should close the menu", () => { it("should close the menu", () => {
expect(menu.classList).not.toContain('in'); expect(menu.classList).not.toContain('show');
}); });
}); });
@@ -73,7 +73,7 @@ describe('HeaderComponent', () => {
}); });
it("should open the menu", () => { it("should open the menu", () => {
expect(menu.classList).toContain('in'); expect(menu.classList).toContain('show');
}); });
}); });

View File

@@ -10,10 +10,26 @@ export class HostWindowResizeAction implements Action {
payload: { payload: {
width: number; width: number;
height: number; height: number;
breakPoint: string;
}; };
constructor(width: number, height: number) { constructor(width: number, height: number) {
this.payload = { width, height } let breakPoint = this.getBreakpoint(width);
this.payload = { width, height, breakPoint }
}
getBreakpoint(windowWidth: number) {
if(windowWidth < 575) {
return 'xs';
} else if (windowWidth >= 576 && windowWidth < 767) {
return 'sm';
} else if (windowWidth >= 768 && windowWidth < 991) {
return 'md';
} else if (windowWidth >= 992 && windowWidth < 1199) {
return 'lg';
} else if (windowWidth >= 1200) {
return 'xl';
}
} }
} }

View File

@@ -13,32 +13,34 @@ class NullAction extends HostWindowResizeAction {
describe('hostWindowReducer', () => { describe('hostWindowReducer', () => {
it("should return the current state when no valid actions have been made", () => { it("should return the current state when no valid actions have been made", () => {
const state = { width: 800, height: 600 }; const state = { width: 800, height: 600, breakPoint: 'md' };
const action = new NullAction(); const action = new NullAction();
const newState = hostWindowReducer(state, action); const newState = hostWindowReducer(state, action);
expect(newState).toEqual(state); expect(newState).toEqual(state);
}); });
it("should start with width = null and height = null", () => { it("should start with width = null, height = null and breakPoint = null", () => {
const action = new NullAction(); const action = new NullAction();
const initialState = hostWindowReducer(undefined, action); const initialState = hostWindowReducer(undefined, action);
expect(initialState.width).toEqual(null); expect(initialState.width).toEqual(null);
expect(initialState.height).toEqual(null); expect(initialState.height).toEqual(null);
expect(initialState.breakPoint).toEqual(null);
}); });
it("should update the width and height in the state in response to a RESIZE action", () => { it("should update the width, height and breakPoint in the state in response to a RESIZE action", () => {
const state = { width: 800, height: 600 }; const state = { width: 800, height: 600, breakPoint: 'md' };
const action = new HostWindowResizeAction(1024, 768); const action = new HostWindowResizeAction(1024, 768);
const newState = hostWindowReducer(state, action); const newState = hostWindowReducer(state, action);
expect(newState.width).toEqual(1024); expect(newState.width).toEqual(1024);
expect(newState.height).toEqual(768); expect(newState.height).toEqual(768);
expect(newState.breakPoint).toEqual('lg');
}); });
it("should perform the RESIZE action without affecting the previous state", () => { it("should perform the RESIZE action without affecting the previous state", () => {
const state = { width: 800, height: 600 }; const state = { width: 800, height: 600, breakPoint: 'md' };
deepFreeze(state); deepFreeze(state);
const action = new HostWindowResizeAction(1024, 768); const action = new HostWindowResizeAction(1024, 768);

View File

@@ -3,11 +3,13 @@ import { HostWindowAction, HostWindowActionTypes } from "./host-window.actions";
export interface HostWindowState { export interface HostWindowState {
width: number; width: number;
height: number; height: number;
breakPoint: string;
} }
const initialState: HostWindowState = { const initialState: HostWindowState = {
width: null, width: null,
height: null height: null,
breakPoint: null
}; };
export const hostWindowReducer = (state = initialState, action: HostWindowAction): HostWindowState => { export const hostWindowReducer = (state = initialState, action: HostWindowAction): HostWindowState => {

View File

@@ -0,0 +1,32 @@
<div class="pagination-masked clearfix top">
<div class="row">
<div class="col pagination-info">
<span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span>
<span class="align-middle">{{ 'pagination.showing.detail' | translate:showingDetail }}</span>
</div>
<div class="col">
<div ngbDropdown #paginationControls="ngbDropdown" class="d-inline-block float-right">
<button class="btn btn-outline-primary" id="paginationControls" (click)="$event.stopPropagation(); (paginationControls.isOpen())?paginationControls.close():paginationControls.open();"><i class="fa fa-cog" aria-hidden="true"></i></button>
<div class="dropdown-menu dropdown-menu-right" id="paginationControlsDropdownMenu" aria-labelledby="paginationControls">
<h6 class="dropdown-header">{{ 'pagination.results-per-page' | translate}}</h6>
<button class="dropdown-item" style="padding-left: 20px" *ngFor="let item of pageSizeOptions " (click)="setPageSize(item)"><i class="fa fa-check {{(item != paginationOptions.pageSize) ? 'invisible' : ''}}" aria-hidden="true"></i> {{item}} </button>
</div>
</div>
</div>
</div>
</div>
<ng-content></ng-content>
<div class="pagination justify-content-center clearfix bottom">
<ngb-pagination [boundaryLinks]="paginationOptions.boundaryLinks"
[collectionSize]="collectionSize"
[disabled]="paginationOptions.disabled"
[ellipses]="paginationOptions.ellipses"
[maxSize]="(windowBreakPoint.breakPoint == 'xs')?5:paginationOptions.maxSize"
[(page)]="currentPage"
(pageChange)="doPageChange($event)"
[pageSize]="pageSize"
[rotate]="paginationOptions.rotate"
[size]="(windowBreakPoint.breakPoint == 'xs')?'sm':paginationOptions.size"></ngb-pagination>
</div>

View File

@@ -0,0 +1,321 @@
// ... test imports
import {
async,
ComponentFixture,
inject,
TestBed, fakeAsync, tick, getTestBed
} from '@angular/core/testing';
import {
Component,
CUSTOM_ELEMENTS_SCHEMA,
DebugElement
} from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { RouterTestingModule } from '@angular/router/testing';
import { By } from '@angular/platform-browser';
import Spy = jasmine.Spy;
import { TranslateModule, TranslateLoader } from "ng2-translate";
import { Store, StoreModule } from "@ngrx/store";
// Load the implementations that should be tested
import { CommonModule } from '@angular/common';
import { Ng2PaginationModule } from 'ng2-pagination';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { PaginationComponent } from './pagination.component';
import { PaginationOptions } from '../../core/shared/pagination-options.model';
import { MockTranslateLoader } from "../testing/mock-translate-loader";
import { GLOBAL_CONFIG, EnvConfig } from '../../../config';
import { MockStore, MockAction } from "../testing/mock-store";
import { ActivatedRouteStub, RouterStub } from "../testing/router-stubs";
function createTestComponent<T>(html: string, type: {new (...args: any[]): T}): ComponentFixture<T> {
TestBed.overrideComponent(type, {
set: { template: html }
});
let fixture = TestBed.createComponent(type);
fixture.detectChanges();
return fixture as ComponentFixture<T>;
}
function expectPages(fixture: ComponentFixture<any>, pagesDef: string[]): void {
let de = fixture.debugElement.query(By.css('.pagination'));
let pages = de.nativeElement.querySelectorAll('li');
expect(pages.length).toEqual(pagesDef.length);
for (let i = 0; i < pagesDef.length; i++) {
let pageDef = pagesDef[i];
let classIndicator = pageDef.charAt(0);
if (classIndicator === '+') {
expect(pages[i].classList.contains("active")).toBeTruthy();
expect(pages[i].classList.contains("disabled")).toBeFalsy();
expect(normalizeText(pages[i].textContent)).toEqual(pageDef.substr(1) + ' (current)');
} else if (classIndicator === '-') {
expect(pages[i].classList.contains("active")).toBeFalsy();
expect(pages[i].classList.contains("disabled")).toBeTruthy();
expect(normalizeText(pages[i].textContent)).toEqual(pageDef.substr(1));
if (normalizeText(pages[i].textContent) !== '...') {
expect(pages[i].querySelector('a').getAttribute('tabindex')).toEqual('-1');
}
} else {
expect(pages[i].classList.contains("active")).toBeFalsy();
expect(pages[i].classList.contains("disabled")).toBeFalsy();
expect(normalizeText(pages[i].textContent)).toEqual(pageDef);
if (normalizeText(pages[i].textContent) !== '...') {
expect(pages[i].querySelector('a').hasAttribute('tabindex')).toBeFalsy();
}
}
}
}
function changePageSize(fixture: ComponentFixture<any>, pageSize: string): void {
let buttonEl = fixture.nativeElement.querySelector('#paginationControls');
let activatedRouteStub: ActivatedRouteStub;
let routerStub: RouterStub;
buttonEl.click();
let dropdownMenu = fixture.debugElement.query(By.css('#paginationControlsDropdownMenu'));
let buttons = dropdownMenu.nativeElement.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
if (buttons[i].textContent.trim() == pageSize) {
buttons[i].click();
fixture.detectChanges();
break;
}
}
}
function changePage(fixture: ComponentFixture<any>, idx: number): void {
let de = fixture.debugElement.query(By.css('.pagination'));
let buttons = de.nativeElement.querySelectorAll('li');
buttons[idx].querySelector('a').click();
fixture.detectChanges();
}
function normalizeText(txt: string): string {
return txt.trim().replace(/\s+/g, ' ');
}
describe('Pagination component', () => {
let fixture: ComponentFixture<PaginationComponent>;
let comp: PaginationComponent;
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
let de: DebugElement;
let html;
let mockStore: any;
let activatedRouteStub: ActivatedRouteStub;
let routerStub: RouterStub;
//Define initial state and test state
let _initialState = { width: 1600, height: 770, breakPoint: 'xl' };
// async beforeEach
beforeEach(async(() => {
activatedRouteStub = new ActivatedRouteStub();
routerStub = new RouterStub();
mockStore = new MockStore(_initialState);
TestBed.configureTestingModule({
imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({
provide: TranslateLoader,
useClass: MockTranslateLoader
}), Ng2PaginationModule, NgbModule.forRoot(),
RouterTestingModule.withRoutes([
{path: 'home', component: TestComponent}
])],
declarations: [PaginationComponent, TestComponent], // declare the test component
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: GLOBAL_CONFIG, useValue: EnvConfig },
{ provide: Router, useValue: routerStub },
{ provide: Store, useValue: mockStore },
PaginationComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-pagination #p="paginationComponent"
[paginationOptions]="paginationOptions"
[collectionSize]="collectionSize"
(pageChange)="pageChanged($event)"
(pageSizeChange)="pageSizeChanged($event)">
<ul>
<li *ngFor="let item of collection | paginate: { itemsPerPage: paginationOptions.pageSize,
currentPage: paginationOptions.currentPage, totalItems: collectionSize }"> {{item}} </li>
</ul>
</ds-pagination>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create Pagination Component', inject([PaginationComponent], (app: PaginationComponent) => {
expect(app).toBeDefined();
}));
it('should render', () => {
expect(testComp.paginationOptions.id).toEqual('test');
expect(testComp.paginationOptions.currentPage).toEqual(1);
expect(testComp.paginationOptions.pageSize).toEqual(10);
expectPages(testFixture, ['-«', '+1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '»']);
});
it('should render and respond to page change', () => {
testComp.collectionSize = 30;
changePage(testFixture, 3);
expectPages(testFixture, ['«', '1', '2', '+3', '-»']);
changePage(testFixture, 0);
expectPages(testFixture, ['«', '1', '+2', '3', '»']);
});
it('should render and respond to collectionSize change', () => {
testComp.collectionSize = 30;
testFixture.detectChanges();
expectPages(testFixture, ['-«', '+1', '2', '3', '»']);
testComp.collectionSize = 40;
testFixture.detectChanges();
expectPages(testFixture, ['-«', '+1', '2', '3', '4', '»']);
});
it('should render and respond to pageSize change', () => {
testComp.collectionSize = 30;
testFixture.detectChanges();
expectPages(testFixture, ['-«', '+1', '2', '3', '»']);
changePageSize(testFixture, '5');
expectPages(testFixture, ['-«', '+1', '2', '3', '4', '5', '6', '»']);
changePageSize(testFixture, '10');
expectPages(testFixture, ['-«', '+1', '2', '3', '»']);
changePageSize(testFixture, '20');
expectPages(testFixture, ['-«', '+1', '2', '»']);
});
it('should emit pageChange event with correct value', fakeAsync(() => {
spyOn(testComp, 'pageChanged');
changePage(testFixture, 3);
tick();
expect(testComp.pageChanged).toHaveBeenCalledWith(3);
}));
it('should emit pageSizeChange event with correct value', fakeAsync(() => {
spyOn(testComp, 'pageSizeChanged');
changePageSize(testFixture, '5');
tick();
expect(testComp.pageSizeChanged).toHaveBeenCalledWith(5);
}));
it('should set correct route parameters', fakeAsync(() => {
let paginationComponent: PaginationComponent = testFixture
.debugElement.query(By.css('ds-pagination')).references['p'];
routerStub = testFixture.debugElement.injector.get(Router);
testComp.collectionSize = 60;
changePage(testFixture, 3);
tick();
expect(routerStub.navigate).toHaveBeenCalledWith([{pageId: 'test', page: 3, pageSize: 10}]);
expect(paginationComponent.currentPage).toEqual(3);
changePageSize(testFixture, '20');
tick();
expect(routerStub.navigate).toHaveBeenCalledWith([{pageId: 'test', page: 3, pageSize: 20}]);
expect(paginationComponent.pageSize).toEqual(20);
}));
it('should get parameters from route', () => {
activatedRouteStub = testFixture.debugElement.injector.get(ActivatedRoute);
activatedRouteStub.testParams = {
pageId: 'test',
page: 2,
pageSize: 20
};
testFixture.detectChanges();
expectPages(testFixture, ['«', '1', '+2', '3', '4', '5', '»']);
expect(testComp.paginationOptions.currentPage).toEqual(2);
expect(testComp.paginationOptions.pageSize).toEqual(20);
activatedRouteStub.testParams = {
pageId: 'test',
page: 3,
pageSize: 40
};
testFixture.detectChanges();
expectPages(testFixture, ['«', '1', '2', '+3', '-»']);
expect(testComp.paginationOptions.currentPage).toEqual(3);
expect(testComp.paginationOptions.pageSize).toEqual(40);
});
it('should respond to windows resize', () => {
let paginationComponent: PaginationComponent = testFixture
.debugElement.query(By.css('ds-pagination')).references['p'];
mockStore = testFixture.debugElement.injector.get(Store);
mockStore.nextState({ width: 400, height: 770, breakPoint: 'xs' });
mockStore.select('hostWindow').subscribe((state) => {
paginationComponent.windowBreakPoint = state;
testFixture.detectChanges();
expectPages(testFixture, ['-«', '+1', '2', '3', '4', '5', '-...', '10', '»']);
de = testFixture.debugElement.query(By.css('ul.pagination'));
expect(de.nativeElement.classList.contains("pagination-sm")).toBeTruthy();
});
});
});
// declare a test component
@Component({selector: 'test-cmp', template: ''})
class TestComponent {
collection: string[] = [];
collectionSize: number;
paginationOptions = new PaginationOptions();
constructor() {
this.collection = Array.from(new Array(100), (x, i) => `item ${i + 1}`);
this.collectionSize = 100;
this.paginationOptions.id = 'test';
}
pageChanged(page) {
this.paginationOptions.currentPage = page;
}
pageSizeChanged(pageSize) {
this.paginationOptions.pageSize = pageSize;
}
}

View File

@@ -0,0 +1,239 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewEncapsulation
} from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router';
import { isNumeric } from "rxjs/util/isNumeric";
import 'rxjs/add/operator/switchMap';
import { Store } from "@ngrx/store";
import { Observable } from "rxjs";
import { HostWindowState } from "../host-window.reducer";
import { PaginationOptions } from '../../core/shared/pagination-options.model';
/**
* The default pagination controls component.
*/
@Component({
exportAs: 'paginationComponent',
selector: 'ds-pagination',
templateUrl: 'pagination.component.html',
changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.Emulated
})
export class PaginationComponent implements OnDestroy, OnInit {
/**
* Number of items in collection.
*/
@Input() collectionSize: number;
/**
* Configuration for the NgbPagination component.
*/
@Input() paginationOptions: PaginationOptions;
/**
* An event fired when the page is changed.
* Event's payload equals to the newly selected page.
*/
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
/**
* An event fired when the page size is changed.
* Event's payload equals to the newly selected page size.
*/
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
/**
* Current page.
*/
public currentPage = 1;
/**
* An observable of HostWindowState type
*/
public hostWindow: Observable<HostWindowState>;
/**
* ID for the pagination instance. Only useful if you wish to
* have more than once instance at a time in a given component.
*/
private id: string;
/**
* Number of items per page.
*/
public pageSize: number = 10;
/**
* A number array that represents options for a context pagination limit.
*/
private pageSizeOptions: Array<number>;
/**
* Local variable, which can be used in the template to access the paginate controls ngbDropdown methods and properties
*/
public paginationControls;
/**
* Subscriber to observable.
*/
private routeSubscription: any;
/**
* An object that represents pagination details of the current viewed page
*/
public showingDetail: any = {
range: null,
total: null
};
/**
* Subscriber to observable.
*/
private stateSubscription: any;
/**
* Contains current HostWindowState
*/
public windowBreakPoint: HostWindowState;
/**
* Method provided by Angular. Invoked after the constructor.
*/
ngOnInit() {
this.stateSubscription = this.hostWindow.subscribe((state: HostWindowState) => {
this.windowBreakPoint = state;
});
this.checkConfig(this.paginationOptions);
this.id = this.paginationOptions.id || null;
this.currentPage = this.paginationOptions.currentPage;
this.pageSize = this.paginationOptions.pageSize;
this.pageSizeOptions = this.paginationOptions.pageSizeOptions;
this.routeSubscription = this.route.params
.map(params => params)
.subscribe(params => {
if(this.id == params['pageId']
&& (this.paginationOptions.currentPage != params['page']
|| this.paginationOptions.pageSize != params['pageSize'])
) {
this.validateParams(params['page'], params['pageSize']);
}
});
this.setShowingDetail();
}
/**
* Method provided by Angular. Invoked when the instance is destroyed.
*/
ngOnDestroy() {
this.stateSubscription.unsubscribe();
this.routeSubscription.unsubscribe();
}
/**
* @param route
* Route is a singleton service provided by Angular.
* @param router
* Router is a singleton service provided by Angular.
*/
constructor(
private route: ActivatedRoute,
private router: Router,
private store: Store<HostWindowState>
){
this.hostWindow = this.store.select<HostWindowState>('hostWindow');
}
/**
* Method to set set new page and update route parameters
*
* @param page
* The page being navigated to.
*/
public doPageChange(page: number) {
this.router.navigate([{ pageId: this.id, page: page, pageSize: this.pageSize }]);
this.currentPage = page;
this.setShowingDetail();
this.pageChange.emit(page);
}
/**
* Method to set set new page size and update route parameters
*
* @param pageSize
* The new page size.
*/
public setPageSize(pageSize: number) {
this.router.navigate([{ pageId: this.id, page: this.currentPage, pageSize: pageSize }]);
this.pageSize = pageSize;
this.setShowingDetail();
this.pageSizeChange.emit(pageSize);
}
/**
* Method to set pagination details of the current viewed page.
*/
private setShowingDetail() {
let firstItem;
let lastItem;
let lastPage = Math.round(this.collectionSize / this.pageSize);
firstItem = this.pageSize * (this.currentPage - 1) + 1;
if (this.currentPage != lastPage) {
lastItem = this.pageSize * this.currentPage;
} else {
lastItem = this.collectionSize;
}
this.showingDetail = {
range: firstItem + ' - ' + lastItem,
total: this.collectionSize
}
}
/**
* Validate query params
*
* @param page
* The page number to validate
* @param pageSize
* The page size to validate
*/
private validateParams(page: any, pageSize: any) {
let filteredPageSize = this.pageSizeOptions.find(x => x == pageSize);
if (!isNumeric(page) || !filteredPageSize) {
let filteredPage = isNumeric(page) ? page : this.currentPage;
filteredPageSize = (filteredPageSize) ? filteredPageSize : this.pageSize;
this.router.navigate([{ pageId: this.id, page: filteredPage, pageSize: filteredPageSize }]);
} else {
// (+) converts string to a number
this.currentPage = +page;
this.pageSize = +pageSize;
this.pageChange.emit(this.currentPage);
this.pageSizeChange.emit(this.pageSize);
}
}
/**
* Ensure options passed contains the required properties.
*
* @param paginateOptions
* The paginate options object.
*/
private checkConfig(paginateOptions: any) {
var required = ['id', 'currentPage', 'pageSize', 'pageSizeOptions'];
var missing = required.filter(function (prop) { return !(prop in paginateOptions); });
if (0 < missing.length) {
throw new Error("Paginate: Argument is missing the following required properties: " + missing.join(', '));
}
}
}

View File

@@ -3,10 +3,12 @@ import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Ng2PaginationModule } from 'ng2-pagination';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from 'ng2-translate/ng2-translate'; import { TranslateModule } from 'ng2-translate/ng2-translate';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { PaginationComponent } from "./pagination/pagination.component";
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -15,6 +17,7 @@ const MODULES = [
TranslateModule, TranslateModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
Ng2PaginationModule,
NgbModule NgbModule
]; ];
@@ -24,6 +27,7 @@ const PIPES = [
const COMPONENTS = [ const COMPONENTS = [
// put shared components here // put shared components here
PaginationComponent
]; ];
const PROVIDERS = [ const PROVIDERS = [

View File

@@ -0,0 +1,28 @@
import { Action } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
export class MockStore<T> extends BehaviorSubject<T> {
constructor(private _initialState: T) {
super(_initialState);
}
dispatch = (action: Action): void => {
}
select = <R>(pathOrMapFn: any): Observable<T> => {
return Observable.of(this.getValue());
}
nextState(_newState: T) {
this.next(_newState);
}
}
export class MockAction implements Action {
type = null;
payload: {};
}

View File

@@ -0,0 +1,36 @@
import {Params} from "@angular/router";
import {BehaviorSubject} from "rxjs";
export class RouterStub {
//noinspection TypeScriptUnresolvedFunction
navigate = jasmine.createSpy('navigate');
//navigate1: jasmine.createSpy('navigate');
}
export class ActivatedRouteStub {
// ActivatedRoute.params is Observable
private subject = new BehaviorSubject(this.testParams);
params = this.subject.asObservable();
constructor(params?: Params) {
if (params) {
this.testParams = params;
} else {
this.testParams = {};
}
}
// Test parameters
private _testParams: {};
get testParams() { return this._testParams; }
set testParams(params: {}) {
this._testParams = params;
this.subject.next(params);
}
// ActivatedRoute.snapshot.params
get snapshot() {
return { params: this.testParams };
}
}