├── .browserslistrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── demo-output.gif ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── server.js ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ └── pagination │ │ │ ├── pagination.component.html │ │ │ ├── pagination.component.scss │ │ │ ├── pagination.component.spec.ts │ │ │ └── pagination.component.ts │ └── services │ │ └── pager.service.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Navanath Jadhav 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Server side pagination in angular 2 | 3 | This project contains generic and reusable pagination component to achieve server side pagination in angular. The pagination component has been created from scratch and does not use any third party pagination library. 4 | 5 | ## Stack used 6 | - Angular 7 | - Typescript 8 | - Node.js 9 | - Bootstrap 10 | 11 | ## Give a Star ⭐ 12 | If you liked this project or found it helpful then please give it a star. It will keep motivating me do more work like this! Thanks. 13 | 14 | ## Demo Output 15 | 16 | ![Demo](./demo-output.gif) 17 | 18 | ## Outline 19 | - Pagination supportive API 20 | - Reusable pagination component 21 | - Generic pager service for calculating pager data 22 | - Parent component containing table for consuming pagination component 23 | 24 | ## Share & Care 25 | 26 | If you think this project is helpful share it on [Twitter](https://twitter.com/intent/tweet?url=https://github.com/navanathjadhav/pagination-angular) 27 | 28 | ## Read More 29 | View detailed article on Ever Blogs: [Server-side pagination in Angular 11 | Ever Blogs](https://everblogs.com/angular/server-side-pagination-in-angular-11/) 30 |
31 | Please visit [Ever Blogs](https://everblogs.com/) to read interesting blogs on other Technologies. 32 | 33 | ## Development server 34 | 35 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 36 | 37 | Run `npm run server:start` for a mock Node.js server API. It will start on `http://localhost:4000/`. 38 | 39 | ## Code scaffolding 40 | 41 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 42 | 43 | ## Build 44 | 45 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 46 | 47 | ## Running unit tests 48 | 49 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 50 | 51 | ## Running end-to-end tests 52 | 53 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 54 | 55 | ## Contribute 56 | 57 | You are always welcome to help to make this project more knowledgeable and helpful for other developers. 58 | 59 | Just submit a PR. 60 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "pagination-angular": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/pagination-angular", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "aot": true, 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets" 32 | ], 33 | "styles": [ 34 | "src/styles.scss" 35 | ], 36 | "scripts": [] 37 | }, 38 | "configurations": { 39 | "production": { 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.prod.ts" 44 | } 45 | ], 46 | "optimization": true, 47 | "outputHashing": "all", 48 | "sourceMap": false, 49 | "namedChunks": false, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "500kb", 57 | "maximumError": "1mb" 58 | }, 59 | { 60 | "type": "anyComponentStyle", 61 | "maximumWarning": "2kb", 62 | "maximumError": "4kb" 63 | } 64 | ] 65 | } 66 | } 67 | }, 68 | "serve": { 69 | "builder": "@angular-devkit/build-angular:dev-server", 70 | "options": { 71 | "browserTarget": "pagination-angular:build" 72 | }, 73 | "configurations": { 74 | "production": { 75 | "browserTarget": "pagination-angular:build:production" 76 | } 77 | } 78 | }, 79 | "extract-i18n": { 80 | "builder": "@angular-devkit/build-angular:extract-i18n", 81 | "options": { 82 | "browserTarget": "pagination-angular:build" 83 | } 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "main": "src/test.ts", 89 | "polyfills": "src/polyfills.ts", 90 | "tsConfig": "tsconfig.spec.json", 91 | "karmaConfig": "karma.conf.js", 92 | "assets": [ 93 | "src/favicon.ico", 94 | "src/assets" 95 | ], 96 | "styles": [ 97 | "src/styles.scss" 98 | ], 99 | "scripts": [] 100 | } 101 | }, 102 | "lint": { 103 | "builder": "@angular-devkit/build-angular:tslint", 104 | "options": { 105 | "tsConfig": [ 106 | "tsconfig.app.json", 107 | "tsconfig.spec.json", 108 | "e2e/tsconfig.json" 109 | ], 110 | "exclude": [ 111 | "**/node_modules/**" 112 | ] 113 | } 114 | }, 115 | "e2e": { 116 | "builder": "@angular-devkit/build-angular:protractor", 117 | "options": { 118 | "protractorConfig": "e2e/protractor.conf.js", 119 | "devServerTarget": "pagination-angular:serve" 120 | }, 121 | "configurations": { 122 | "production": { 123 | "devServerTarget": "pagination-angular:serve:production" 124 | } 125 | } 126 | } 127 | } 128 | } 129 | }, 130 | "defaultProject": "pagination-angular" 131 | } 132 | -------------------------------------------------------------------------------- /demo-output.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navanathjadhav/pagination-angular/ff22c24e197ad0b3242a6ba7654a54c8423e329d/demo-output.gif -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | SELENIUM_PROMISE_MANAGER: false, 20 | baseUrl: 'http://localhost:4200/', 21 | framework: 'jasmine', 22 | jasmineNodeOpts: { 23 | showColors: true, 24 | defaultTimeoutInterval: 30000, 25 | print: function() {} 26 | }, 27 | onPrepare() { 28 | require('ts-node').register({ 29 | project: require('path').join(__dirname, './tsconfig.json') 30 | }); 31 | jasmine.getEnv().addReporter(new SpecReporter({ 32 | spec: { 33 | displayStacktrace: StacktraceOption.PRETTY 34 | } 35 | })); 36 | } 37 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', async () => { 12 | await page.navigateTo(); 13 | expect(await page.getTitleText()).toEqual('pagination-angular app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | async navigateTo(): Promise { 5 | return browser.get(browser.baseUrl); 6 | } 7 | 8 | async getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/pagination-angular'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pagination-angular", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "server:start": "node server.js", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~11.0.5", 16 | "@angular/common": "~11.0.5", 17 | "@angular/compiler": "~11.0.5", 18 | "@angular/core": "~11.0.5", 19 | "@angular/forms": "~11.0.5", 20 | "@angular/platform-browser": "~11.0.5", 21 | "@angular/platform-browser-dynamic": "~11.0.5", 22 | "@angular/router": "~11.0.5", 23 | "rxjs": "~6.6.0", 24 | "tslib": "^2.0.0", 25 | "zone.js": "~0.10.2" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "~0.1100.5", 29 | "@angular/cli": "~11.0.5", 30 | "@angular/compiler-cli": "~11.0.5", 31 | "@types/jasmine": "~3.6.0", 32 | "@types/node": "^12.11.1", 33 | "codelyzer": "^6.0.0", 34 | "jasmine-core": "~3.6.0", 35 | "jasmine-spec-reporter": "~5.0.0", 36 | "karma": "~5.1.0", 37 | "karma-chrome-launcher": "~3.1.0", 38 | "karma-coverage": "~2.0.3", 39 | "karma-jasmine": "~4.0.0", 40 | "karma-jasmine-html-reporter": "^1.5.0", 41 | "protractor": "~7.0.0", 42 | "ts-node": "~8.3.0", 43 | "tslint": "~6.1.0", 44 | "typescript": "~4.0.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const express = require("express"); 3 | const app = express(); 4 | 5 | /* 6 | * Define hostname & PORT 7 | */ 8 | const hostname = "localhost"; 9 | const port = 4000; 10 | 11 | /* 12 | * CORS 13 | */ 14 | app.use(function (req, res, next) { 15 | res.header("Access-Control-Allow-Origin", "*"); 16 | res.header( 17 | "Access-Control-Allow-Headers", 18 | "Origin, X-Requested-With, Content-Type, Accept, access_token" 19 | ); 20 | res.header("Access-Control-Allow-Methods", "PUT, GET, POST, OPTIONS, DELETE"); 21 | next(); 22 | }); 23 | 24 | app.use( 25 | express.urlencoded({ 26 | extended: true, 27 | }) 28 | ); 29 | 30 | app.use(express.json()); 31 | 32 | /* 33 | * Mock students data 34 | */ 35 | const students = [ 36 | { name: "Alice Doe", email: "alice@xyz.com", department: "Computer Science" }, 37 | { 38 | name: "John Doe", 39 | email: "john@xyz.com", 40 | department: "Information Technology", 41 | }, 42 | { name: "Alice Doe", email: "alice@xyz.com", department: "Computer Science" }, 43 | { 44 | name: "John Doe", 45 | email: "john@xyz.com", 46 | department: "Information Technology", 47 | }, 48 | { name: "Alice Doe", email: "alice@xyz.com", department: "Computer Science" }, 49 | { 50 | name: "John Doe", 51 | email: "john@xyz.com", 52 | department: "Information Technology", 53 | }, 54 | { name: "Alice Doe", email: "alice@xyz.com", department: "Computer Science" }, 55 | { 56 | name: "John Doe", 57 | email: "john@xyz.com", 58 | department: "Information Technology", 59 | }, 60 | { name: "Alice Doe", email: "alice@xyz.com", department: "Computer Science" }, 61 | { 62 | name: "John Doe", 63 | email: "john@xyz.com", 64 | department: "Information Technology", 65 | }, 66 | { name: "Alice Doe", email: "alice@xyz.com", department: "Computer Science" }, 67 | { 68 | name: "John Doe", 69 | email: "john@xyz.com", 70 | department: "Information Technology", 71 | }, 72 | { name: "Alice Doe", email: "alice@xyz.com", department: "Computer Science" }, 73 | { 74 | name: "John Doe", 75 | email: "john@xyz.com", 76 | department: "Information Technology", 77 | }, 78 | { name: "Alice Doe", email: "alice@xyz.com", department: "Computer Science" }, 79 | ]; 80 | 81 | /* 82 | * Get students with query params: [pageNumber, recordsPerPage & searchTerm] 83 | */ 84 | app.get("/api/students", (req, res) => { 85 | let studentInternal = students; 86 | 87 | // Calculate start, Aka skip if you use it in db queries 88 | const start = 89 | parseInt(req.query.recordsPerPage) * (parseInt(req.query.pageNumber) - 1); 90 | 91 | // Calculate end, Aka limit if you use it in db queries 92 | const end = start + parseInt(req.query.recordsPerPage); 93 | 94 | // Match if searchTerm is received from client 95 | // Use your DB query here 96 | if (req.query.searchTerm) { 97 | studentInternal = students.filter((student) => { 98 | return ( 99 | student.name.match(new RegExp(req.query.searchTerm, "i")) || 100 | student.email.match(new RegExp(req.query.searchTerm, "i")) || 101 | student.department.match(new RegExp(req.query.searchTerm, "i")) 102 | ); 103 | }); 104 | } 105 | 106 | // Artificial delay for showing loader in client 107 | setTimeout(() => { 108 | // Send response: { count, data } 109 | res.status(200).json({ 110 | count: studentInternal.length, 111 | data: studentInternal.slice(start, end), 112 | }); 113 | }, 1000); 114 | }); 115 | 116 | const server = http.createServer(app); 117 | 118 | server.listen(port, hostname, () => { 119 | console.log(`Server running at http://${hostname}:${port}/`); 120 | }); 121 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 52 |
53 |
54 | 55 | 56 | 57 |
58 |
59 |

Students

60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 75 | 76 | 77 | 78 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
NameEmailDepartment
No students
79 | Loading... 80 |
{{ student.name }}{{ student.email }}{{ student.department }}
89 | 97 | 98 |
99 |
100 |
101 |
102 | 103 |
104 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navanathjadhav/pagination-angular/ff22c24e197ad0b3242a6ba7654a54c8423e329d/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | }); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'pagination-angular'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.componentInstance; 22 | expect(app.title).toEqual('pagination-angular'); 23 | }); 24 | 25 | it('should render title', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.nativeElement; 29 | expect(compiled.querySelector('.content span').textContent).toContain('pagination-angular app is running!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewChecked, 3 | ChangeDetectorRef, 4 | Component, 5 | EventEmitter, 6 | OnInit, 7 | } from '@angular/core'; 8 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | templateUrl: './app.component.html', 13 | styleUrls: ['./app.component.scss'], 14 | }) 15 | export class AppComponent implements OnInit, AfterViewChecked { 16 | constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {} 17 | 18 | // Defaults 19 | form!: FormGroup; 20 | students: any = []; 21 | searchTerm: string = ''; 22 | reload: EventEmitter = new EventEmitter(); 23 | isLoadingStudents: boolean = false; 24 | recordsPerPage: number = 5; 25 | 26 | // On init 27 | ngOnInit(): void { 28 | this.buildForm(); 29 | } 30 | 31 | // On After view checked, detect changes manually 32 | ngAfterViewChecked(): void { 33 | this.cdr.detectChanges(); 34 | } 35 | 36 | 37 | // Init form 38 | buildForm() { 39 | this.form = this.fb.group({ 40 | term: ['', [Validators.required]], 41 | recordsPerPage: [''], 42 | }); 43 | } 44 | 45 | // On form submit => assign search term 46 | submitForm(): void { 47 | if (this.form.invalid) { 48 | return; 49 | } 50 | this.searchTerm = this.form.value.term; 51 | } 52 | 53 | // Clear search results on search box empty 54 | clearSearchResult() { 55 | if (this.form.controls.term.value === '' && this.searchTerm) { 56 | this.searchTerm = ''; 57 | setTimeout(() => { 58 | this.reload.emit(true); 59 | this.reload.emit(false); 60 | }, 100); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | import { AppComponent } from './app.component'; 6 | import { PaginationComponent } from './components/pagination/pagination.component'; 7 | import { PagerService } from './services/pager.service'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | AppComponent, 12 | PaginationComponent 13 | ], 14 | imports: [ 15 | BrowserModule, 16 | HttpClientModule, 17 | FormsModule, 18 | ReactiveFormsModule, 19 | ], 20 | providers: [PagerService], 21 | bootstrap: [AppComponent] 22 | }) 23 | export class AppModule { } 24 | -------------------------------------------------------------------------------- /src/app/components/pagination/pagination.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Showing {{ pager.startIndex + 1 }} to {{ pager.endIndex + 1 }} of 4 | {{ totalRecordsCount }} entries 5 |
6 | 7 | 31 |
32 | -------------------------------------------------------------------------------- /src/app/components/pagination/pagination.component.scss: -------------------------------------------------------------------------------- 1 | .custom-disabled { 2 | pointer-events: none; 3 | } -------------------------------------------------------------------------------- /src/app/components/pagination/pagination.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PaginationComponent } from './pagination.component'; 4 | 5 | describe('PaginationComponent', () => { 6 | let component: PaginationComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ PaginationComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PaginationComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/pagination/pagination.component.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { 3 | Component, 4 | Input, 5 | EventEmitter, 6 | Output, 7 | OnInit, 8 | SimpleChange, 9 | } from '@angular/core'; 10 | import { PagerService } from 'src/app/services/pager.service'; 11 | 12 | @Component({ 13 | selector: 'app-pagination', 14 | templateUrl: './pagination.component.html', 15 | styleUrls: ['./pagination.component.scss'], 16 | }) 17 | export class PaginationComponent implements OnInit { 18 | // Current page number 19 | currentPageNumber: number = 1; 20 | // Total records count 21 | totalRecordsCount: number = 0; 22 | // Total pages 23 | totalPages: number = 0; 24 | // Pager 25 | pager: any = {}; 26 | 27 | constructor(private http: HttpClient, private pagerService: PagerService) {} 28 | 29 | // API route 30 | @Input() apiRoute: string = ''; 31 | // Search term 32 | @Input() searchTerm: string = ''; 33 | // Records per page 34 | @Input() recordsPerPage: number = 0; 35 | // Response data 36 | @Output() responseData = new EventEmitter(); 37 | // Loading 38 | @Output() loading = new EventEmitter(); 39 | // Reload 40 | @Input() reload: EventEmitter | undefined; 41 | 42 | // On init 43 | ngOnInit() { 44 | this.watchReload(); 45 | } 46 | 47 | // To be emitted from parent component 48 | watchReload() { 49 | if (this.reload) { 50 | this.reload.subscribe((reload: any) => { 51 | if (reload) { 52 | this.getData(this.currentPageNumber); 53 | } 54 | }); 55 | } 56 | } 57 | 58 | // Fetch new page data 59 | next() { 60 | this.getData(this.currentPageNumber + 1) 61 | } 62 | 63 | // Fetch previous page data 64 | prev() { 65 | this.getData(this.currentPageNumber - 1) 66 | } 67 | 68 | // Fetch data from API 69 | getData(pageNo: any) { 70 | this.loading.emit(true); 71 | this.responseData.emit([]); 72 | this.currentPageNumber = Number(pageNo); 73 | let finalPath = `${this.apiRoute}?pageNumber=${this.currentPageNumber}&recordsPerPage=${this.recordsPerPage}`; 74 | 75 | // add search term only if search available 76 | if (this.searchTerm && this.searchTerm.length) { 77 | finalPath = `${finalPath}&searchTerm=${this.searchTerm}`; 78 | } 79 | 80 | this.http.get(finalPath).subscribe( 81 | (response: any) => { 82 | this.totalRecordsCount = response.count; 83 | this.responseData.emit(response.data); 84 | this.totalPages = Math.ceil(response.count / this.recordsPerPage); 85 | this.setPagination(this.currentPageNumber); 86 | this.loading.emit(false); 87 | }, 88 | (error) => { 89 | this.loading.emit(false); 90 | alert(error.message); 91 | } 92 | ); 93 | } 94 | 95 | // Set pagination data and pager data 96 | setPagination(pageNo: any) { 97 | pageNo = Number(pageNo); 98 | this.currentPageNumber = pageNo; 99 | this.pager = this.pagerService.getPager( 100 | this.totalRecordsCount, 101 | pageNo, 102 | this.recordsPerPage 103 | ); 104 | } 105 | 106 | // Watch for changes: [searchTerm & recordsPerPage] 107 | ngOnChanges(changes: { [propName: string]: SimpleChange }): void { 108 | const term = changes['searchTerm'] && changes['searchTerm'].currentValue; 109 | const recordsPerPage = 110 | changes['recordsPerPage'] && changes['recordsPerPage'].currentValue; 111 | if (term) { 112 | this.getData(this.currentPageNumber); 113 | } 114 | if (recordsPerPage) { 115 | this.getData(this.currentPageNumber); 116 | } 117 | } 118 | 119 | // Track by 120 | trackByFn(index: any, item: any) { 121 | return item; // or item.id 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/app/services/pager.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | @Injectable() 3 | export class PagerService { 4 | getPager(totalItems: number, currentPage: number = 1, pageSize: number = 10) { 5 | // calculate total pages 6 | let totalPages = Math.ceil(totalItems / pageSize); 7 | 8 | // ensure current page isn't out of range 9 | if (currentPage < 1) { 10 | currentPage = 1; 11 | } else if (currentPage > totalPages) { 12 | currentPage = totalPages; 13 | } 14 | 15 | let startPage: number, endPage: number; 16 | if (totalPages <= 10) { 17 | // less than 10 total pages so show all 18 | startPage = 1; 19 | endPage = totalPages; 20 | } else { 21 | // more than 10 total pages so calculate start and end pages 22 | if (currentPage <= 6) { 23 | startPage = 1; 24 | endPage = 10; 25 | } else if (currentPage + 4 >= totalPages) { 26 | startPage = totalPages - 9; 27 | endPage = totalPages; 28 | } else { 29 | startPage = currentPage - 5; 30 | endPage = currentPage + 4; 31 | } 32 | } 33 | 34 | // calculate start and end item indexes 35 | let startIndex = (currentPage - 1) * pageSize; 36 | let endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1); 37 | 38 | // create an array of pages to ng-repeat in the pager control 39 | let pages = Array.from(Array(endPage + 1 - startPage).keys()).map(i => startPage + i); 40 | 41 | // return object with all pager properties required by the view 42 | return { 43 | totalItems: totalItems, 44 | currentPage: currentPage, 45 | pageSize: pageSize, 46 | totalPages: totalPages, 47 | startPage: startPage, 48 | endPage: endPage, 49 | startIndex: startIndex, 50 | endIndex: endIndex, 51 | pages: pages, 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navanathjadhav/pagination-angular/ff22c24e197ad0b3242a6ba7654a54c8423e329d/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navanathjadhav/pagination-angular/ff22c24e197ad0b3242a6ba7654a54c8423e329d/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pagination Angular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "declaration": false, 13 | "downlevelIteration": true, 14 | "experimentalDecorators": true, 15 | "moduleResolution": "node", 16 | "importHelpers": true, 17 | "target": "es2015", 18 | "module": "es2020", 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ] 23 | }, 24 | "angularCompilerOptions": { 25 | "strictInjectionParameters": true, 26 | "strictInputAccessModifiers": true, 27 | "strictTemplates": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | --------------------------------------------------------------------------------