├── .DS_Store ├── .gitignore ├── .node-version ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── demo └── index.html ├── empty-test.js ├── env.json.dist ├── es-module.html ├── es6 ├── FirebasePaginator.js ├── FirebasePaginatorFiniteStrategy.js └── FirebasePaginatorInfiniteStrategy.js ├── firebase-paginator.html ├── firebase-paginator.js ├── firebase-paginator.spec.js ├── hero.svg ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── with-script.html └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deltaepsilon/firebase-paginator/4342cee89684f9b11a87110cb64b9490ea47c8a5/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .vscode 40 | service-account.json 41 | env.json 42 | 43 | .yo-rc.json 44 | bower_components 45 | dist 46 | .cache -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | lts 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.md 2 | bower.json 3 | demo/ 4 | *.dist 5 | *.svg 6 | index.html 7 | test.js 8 | .* 9 | env.json 10 | service-account.json 11 | bower_components 12 | 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 9 | # Polymer Elements 10 | ## Guide for Contributors 11 | 12 | Polymer Elements are built in the open, and the Polymer authors eagerly encourage any and all forms of community contribution. When contributing, please follow these guidelines: 13 | 14 | ### Filing Issues 15 | 16 | **If you are filing an issue to request a feature**, please provide a clear description of the feature. It can be helpful to describe answers to the following questions: 17 | 18 | 1. **Who will use the feature?** _“As someone filling out a form…”_ 19 | 2. **When will they use the feature?** _“When I enter an invalid value…”_ 20 | 3. **What is the user’s goal?** _“I want to be visually notified that the value needs to be corrected…”_ 21 | 22 | **If you are filing an issue to report a bug**, please provide: 23 | 24 | 1. **A clear description of the bug and related expectations.** Consider using the following example template for reporting a bug: 25 | 26 | ```markdown 27 | The `paper-foo` element causes the page to turn pink when clicked. 28 | 29 | ## Expected outcome 30 | 31 | The page stays the same color. 32 | 33 | ## Actual outcome 34 | 35 | The page turns pink. 36 | 37 | ## Steps to reproduce 38 | 39 | 1. Put a `paper-foo` element in the page. 40 | 2. Open the page in a web browser. 41 | 3. Click the `paper-foo` element. 42 | ``` 43 | 44 | 2. **A reduced test case that demonstrates the problem.** If possible, please include the test case as a JSBin. Start with this template to easily import and use relevant Polymer Elements: [http://jsbin.com/cagaye](http://jsbin.com/cagaye/edit?html,output). 45 | 46 | 3. **A list of browsers where the problem occurs.** This can be skipped if the problem is the same across all browsers. 47 | 48 | ### Submitting Pull Requests 49 | 50 | **Before creating a pull request**, please ensure that an issue exists for the corresponding change in the pull request that you intend to make. **If an issue does not exist, please create one per the guidelines above**. The goal is to discuss the design and necessity of the proposed change with Polymer authors and community before diving into a pull request. 51 | 52 | When submitting pull requests, please provide: 53 | 54 | 1. **A reference to the corresponding issue** or issues that will be closed by the pull request. Please refer to these issues using the following syntax: 55 | 56 | ```markdown 57 | (For a single issue) 58 | Fixes #20 59 | 60 | (For multiple issues) 61 | Fixes #32, #40 62 | ``` 63 | 64 | 2. **A succinct description of the design** used to fix any related issues. For example: 65 | 66 | ```markdown 67 | This fixes #20 by removing styles that leaked which would cause the page to turn pink whenever `paper-foo` is clicked. 68 | ``` 69 | 70 | 3. **At least one test for each bug fixed or feature added** as part of the pull request. Pull requests that fix bugs or add features without accompanying tests will not be considered. 71 | 72 | If a proposed change contains multiple commits, please [squash commits](https://www.google.com/url?q=http://blog.steveklabnik.com/posts/2012-11-08-how-to-squash-commits-in-a-github-pull-request) to as few as is necessary to succinctly express the change. A Polymer author can help you squash commits, so don’t be afraid to ask us if you need help with that! 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Chris Esplin 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 | # FirebasePaginator 2 | 3 | FirebasePaginator is a JavaScript utility for Node.js and the browser that enables simple, declarative pagination for your Firebase collections. It's been developed for Firebase 3.0, but it should work for Firebase 2.0 projects as well. 4 | 5 | ### Dependencies 6 | 7 | FirebasePaginator relies on the [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) object. Promise support is great for modern versions of Chrome, Firefox, Edge, Opera and Safari. Internet Explorer gets left out. Sorry IE, but even Microsoft is sick of you. You're dying. And attempting to support you would only prolong the misery. 8 | 9 | FirebasePaginator uses the [Axios](https://github.com/mzabriskie/axios) library for Node.js and XMLHttpRequest in the browser. 10 | 11 | In summary: Node.js requires Promise and Axios; the browser requires Promise and XMLHttpRequest. 12 | 13 | ### Install 14 | 15 | * NPM: `npm install --save firebase-paginator` 16 | * Bower: `bower install --save firebase-paginator` 17 | 18 | ### Test 19 | 20 | Find env.json.dist and copy it then rename the clone to env.json. This file is on the git ignore list. You will need to update this line: `"databaseURL": "https://your-firebase-database-name.firebaseio.com/"` 21 | 22 | You need to create a [firebase-admin](https://firebase.google.com/docs/admin/setup) account to get the required service-account.json file is also on the git ignore list. 23 | 24 | * `npm install` 25 | * `npm test` 26 | 27 | ### Usage 28 | 29 | If you're in Node.js, you'll need to do something like `var FirebasePaginator = require('firebase-paginator')`. 30 | 31 | If you're in the browser, you'll have access to FirebasePaginator on the `window` object like so: `var FirebasePaginator = window.FirebasePaginator;` 32 | 33 | Once you have your `FirebasePaginator` object, the rest is isomorphic JavaScript. Just pass in a Firebase ref and some options: 34 | 35 | **_pageSize_**: any integer greater than zero, defaults to 10 36 | 37 | **_finite_**: defaults to false 38 | 39 | **_auth_**: optional auth token for secure collections 40 | 41 | **_retainLastPage_**: applies to infinite pagination only; prevents a short last page from resetting the list; see [Finite vs Infinite Pagination](#finite-vs-infinite-pagination) 42 | 43 | ``` 44 | var options = { 45 | pageSize: 15, 46 | finite: true, 47 | auth: 'MyAuthTokenForSecurityPurposes', 48 | retainLastPage: false 49 | }; 50 | var paginator = new FirebasePaginator(ref, options); 51 | ``` 52 | 53 | # Functions 54 | 55 | #### FirebasePaginator.prototype.listen(callback) 56 | 57 | Listens to all events 58 | 59 | Useful for proxying events or just debugging 60 | 61 | ``` 62 | var paginator = new FirebasePaginator(ref); 63 | var itemsList = []; 64 | 65 | paginator.listen(function (eventName, eventPayload) { 66 | console(`Fired ${eventName} with the following payload: `, eventPayload); 67 | }); 68 | ``` 69 | 70 | #### FirebasePaginator.prototype.on(event, callback) 71 | 72 | Attaches a callback to an event 73 | 74 | ``` 75 | var paginator = new FirebasePaginator(ref); 76 | var itemsList = []; 77 | var handler = function() { 78 | collection = paginator.collection; 79 | }; 80 | 81 | paginator.on('value', handler); 82 | ``` 83 | 84 | #### FirebasePaginator.prototype.off(event, callback) 85 | 86 | Detaches a callback from an event 87 | 88 | ``` 89 | var paginator = new FirebasePaginator(ref); 90 | var itemsList = []; 91 | var handler = function() { 92 | collection = paginator.collection; 93 | }; 94 | 95 | paginator.off('value', handler); 96 | ``` 97 | 98 | #### FirebasePaginator.prototype.once(event, callback) -> returns promise 99 | 100 | Calls a callback exactly once for an event 101 | 102 | ``` 103 | var paginator = new FirebasePaginator(ref); 104 | var itemsList = []; 105 | var handler = function() { 106 | collection = paginator.collection; 107 | }; 108 | 109 | // Callback pattern 110 | paginator.once('value', handler); 111 | 112 | // Promise pattern 113 | paginator.once('value').then(handler); 114 | ``` 115 | 116 | #### FirebasePaginator.prototype.reset() -> returns promise 117 | 118 | Resets pagination 119 | 120 | Infinite: jumps to end of collection 121 | 122 | Finite: Refreshes keys list and jumps to page 1 123 | 124 | ``` 125 | var paginator = new FirebasePaginator(ref); 126 | paginator.reset() 127 | .then(function() { 128 | console.log('list has been reset'); 129 | }); 130 | ``` 131 | 132 | #### FirebasePaginator.prototype.previous() -> returns promise 133 | 134 | Pages backward 135 | 136 | ``` 137 | var paginator = new FirebasePaginator(ref); 138 | paginator.previous() 139 | .then(function() { 140 | console.log('paginated backward'); 141 | }); 142 | ``` 143 | 144 | #### FirebasePaginator.prototype.next() -> returns promise 145 | 146 | Pages forward 147 | 148 | ``` 149 | var paginator = new FirebasePaginator(ref); 150 | paginator.next() 151 | .then(function() { 152 | console.log('paginated forward'); 153 | }); 154 | ``` 155 | 156 | #### FirebasePaginator.prototype.goToPage() -> returns promise 157 | 158 | Jumps to any page 159 | 160 | Accepts page numbers from 1 to the pageCount 161 | 162 | Available for finite pagination **_only_** 163 | 164 | ``` 165 | var paginator = new FirebasePaginator(ref); 166 | paginator.goToPage(3) 167 | .then(function() { 168 | console.log('paginated to page 3'); 169 | }); 170 | ``` 171 | 172 | # Events 173 | 174 | #### value 175 | 176 | The **value** event fires after every change in data. FirebasePaginator listens to the Firebase **value** event, manipulates the data a bit and then fires its own **value** event. 177 | 178 | #### isLastPage 179 | 180 | **isLastPage** fires just after the **value** event if FirebasePaginator has reached the top of the list. 181 | 182 | #### ready 183 | 184 | FirebasePaginator fires its **ready** event once the first page is loaded. 185 | 186 | #### reset, next, previous 187 | 188 | The **reset**, **next** and **previous** events fire after each of the corresponding functions is complete and the new data is loaded. 189 | 190 | # Finite vs Infinite Pagination 191 | 192 | There are two ways to paginate Firebase data: finite and infinite paginations. 193 | 194 | Let's assume that pageSize is 10 and we have records 1 through 100. Also note that all Firebase pagination occurs from the bottom of the collection. 195 | 196 | #### Infinite Pagination 197 | 198 | Infinite pagination pulls the last 11 records of the collection, saves the 90th record's key as a cursor and adds records 91 through 100 to the collection. 199 | 200 | Infinite pagination steps backward by pulling another 11 records ending at the cursor (a.k.a. the 90th record's key). So paging back once will display records 81 to 90 with record 80's key as the new cursor. Page back again and you're at records 71 to 80 and so forth. 201 | 202 | By default, inifinite pagination resets its last page if you overrun the beginning of a list. For example, if you had 100 items and a `pageSize` of 30, paging backwards would return records 71-100, 41-70, 11-40 and 1-30. Notice that the last page is still 30 records. The default behavior is to reset the collection to the beginning of the list and return a full page if possible. The set `retainLastPage: true` in your options to return records 1-10 instead. 203 | 204 | Pros: 205 | 206 | * Scales forever 207 | * Users can page forward to discover new records as they're added to the collection 208 | * If a user is on the first page, new records will simply appear as they are added 209 | 210 | Cons: 211 | 212 | * Must page forward and backward sequentially. Can't skip pages. 213 | * No context for how many pages exist and where the user is in the list 214 | * If a user is on the first page, new records will simply appear as they are added 215 | 216 | #### Finite Pagination 217 | 218 | Finite pagination makes a single "shallow" REST query to pull all of the collection's keys. See [the docs](https://firebase.google.com/docs/reference/rest/database/#section-param-shallow) on how this is done. 219 | 220 | Once FirebasePaginator has all of the keys, it sorts them and finds the page endpoints. So if we have 100 records with a pageSize of 10, the page endpoints will be the keys for records 10, 20, 30, 40, 50... 100. 221 | 222 | Pros: 223 | 224 | * Users have context for where they are in the collection. 225 | * Users can skip pages. 226 | 227 | Cons: 228 | 229 | * Beware of scaling issues. Consider archiving records to [Google Cloud Datastore](https://cloud.google.com/datastore/docs/) if the collection grows too large. 230 | * Must call `paginator.reset()` to capture new records the may be added 231 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-paginator", 3 | "version": "1.0.16", 4 | "authors": [ 5 | "Anonymous " 6 | ], 7 | "description": "An element providing a solution to no problem in particular.", 8 | "keywords": [ 9 | "web-component", 10 | "polymer", 11 | "seed" 12 | ], 13 | "main": "firebase-paginator.html", 14 | "license": "http://polymer.github.io/LICENSE.txt", 15 | "homepage": "https://github.com//seed-element/", 16 | "ignore": [ 17 | "/.*", 18 | "/test/", 19 | "/demo/", 20 | "/bower_components/", 21 | "hero.svg", 22 | "package.json", 23 | "test.js", 24 | "*.md", 25 | "*.dist", 26 | "index.html" 27 | ], 28 | "dependencies": { 29 | "polymer": "Polymer/polymer#^1.2.0" 30 | }, 31 | "devDependencies": { 32 | "paper-toolbar": "PolymerElements/paper-toolbar#^1.1.6", 33 | "paper-item": "PolymerElements/paper-item#^1.2.1", 34 | "paper-icon-button": "PolymerElements/paper-icon-button#^1.1.2", 35 | "paper-toggle-button": "PolymerElements/paper-toggle-button#^1.1.2", 36 | "paper-input": "PolymerElements/paper-input#^1.1.16", 37 | "iron-component-page": "PolymerElements/iron-component-page#^1.0.0", 38 | "firebase": "^3.2.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | firebase-paginator Demo 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 43 | 44 | 45 | 46 | 47 | 128 | 129 | 130 | 131 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /empty-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var firebase = require('firebase'); 3 | var path = 'firebasePaginator/empty'; 4 | var firebaseConfig = require('./env.json').firebaseConfig; 5 | 6 | firebase.initializeApp(firebaseConfig); 7 | 8 | var FirebasePaginator = require('./firebase-paginator'); 9 | var ref = firebase.database().ref(path); 10 | var populateCollection = function () { 11 | return new Promise(function (resolve, reject) { 12 | var promises = []; 13 | var i = 3; 14 | 15 | while (i--) { 16 | promises.push(ref.push(3 - i)); 17 | } 18 | 19 | Promise.all(promises).then(resolve, reject); 20 | }); 21 | }; 22 | 23 | var testPage = function (paginator, length, start, end, testName) { 24 | return new Promise(function (resolve, reject) { 25 | test(testName || `should return records ${start} to ${end}`, function (t) { 26 | paginator.once('value', function (snap) { 27 | var collection = this.collection; 28 | var keys = Object.keys(collection); 29 | var i = keys.length; 30 | t.equal(i, length); 31 | t.equal(collection[keys[0]], start); 32 | t.equal(collection[keys[i - 1]], end); 33 | t.end(); 34 | resolve(paginator); 35 | }); 36 | }); 37 | }); 38 | }; 39 | 40 | ref.remove() 41 | .then(function () { 42 | var paginator = new FirebasePaginator(ref); 43 | return paginator; 44 | }) 45 | // .then(function (paginator) { 46 | // return new Promise(function (resolve, reject) { 47 | // test(`should return empty collection`, function (t) { 48 | // paginator.once('value', function (snap) { 49 | // t.equal(snap.numChildren(), 0); 50 | // t.end(); 51 | // resolve(paginator); 52 | // }); 53 | // }); 54 | // }); 55 | // }) 56 | .then(function (paginator) { 57 | return populateCollection() 58 | .then(function () { 59 | return paginator; 60 | }); 61 | }) 62 | .then(function (paginator) { 63 | return new Promise(function (resolve, reject) { 64 | test(`should return three results`, function (t) { 65 | paginator.once('value', function (snap) { 66 | t.equal(snap.numChildren(), 3); 67 | t.end(); 68 | resolve(paginator); 69 | }); 70 | }); 71 | }); 72 | }) 73 | .then(function () { 74 | var paginator = new FirebasePaginator(ref, { 75 | pageSize: 5, 76 | finite: true, 77 | auth: firebaseConfig.secret 78 | }); 79 | return paginator; 80 | }) 81 | .then(function (paginator) { 82 | return ref.remove() 83 | .then(function () { 84 | return paginator; 85 | }); 86 | }) 87 | .then(function (paginator) { 88 | return new Promise(function (resolve, reject) { 89 | test(`should return empty collection`, function (t) { 90 | paginator.once('value', function (snap) { 91 | t.equal(snap.numChildren(), 0); 92 | t.end(); 93 | resolve(paginator); 94 | }); 95 | }); 96 | }); 97 | }) 98 | .then(function () { 99 | return populateCollection() 100 | .then(function () { 101 | return paginator; 102 | }); 103 | }) 104 | .then(function (paginator) { 105 | return new Promise(function (resolve, reject) { 106 | test(`should return three results`, function (t) { 107 | paginator.once('value', function (snap) { 108 | t.equal(snap.numChildren(), 3); 109 | t.end(); 110 | resolve(paginator); 111 | }); 112 | }); 113 | }); 114 | }) 115 | .then(function () { 116 | console.log('complete'); 117 | process.exit(); 118 | }) 119 | .catch(function (err) { 120 | console.log('error', err); 121 | process.exit(); 122 | }); 123 | -------------------------------------------------------------------------------- /env.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "firebaseConfig": { 3 | "databaseURL": "https://quiver-two.firebaseio.com", 4 | "serviceAccount": "./service-account.json", 5 | "secret": "ABCDEFGHIJKLMNOPQRSTUVWXYZ123457890" 6 | } 7 | } -------------------------------------------------------------------------------- /es-module.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Firebase Paginator: With Script 8 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /es6/FirebasePaginator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import FinitePagingStrategy from './FirebasePaginatorFiniteStrategy'; 4 | import InfinitePagingStrategy from './FirebasePaginatorInfiniteStrategy'; 5 | 6 | class FirebasePaginator { 7 | constructor(ref, defaults) { 8 | this.defaults = defaults || {}; 9 | this.pages = {}; 10 | this.pageSize = defaults.pageSize ? parseInt(defaults.pageSize, 10) : 10; 11 | this.isFinite = defaults.finite ? defaults.finite : false; 12 | this.retainLastPage = defaults.retainLastPage || false; 13 | this.auth = defaults.auth; 14 | this.ref = ref; 15 | this.isBrowser = defaults.isBrowser; 16 | this.events = {}; 17 | this.pageCount; 18 | 19 | // Events 20 | this.listen = callback => { 21 | this.allEventHandler = callback; 22 | }; 23 | 24 | this.fire = this.fire.bind(this); 25 | this.on = this.on.bind(this); 26 | this.off = this.off.bind(this); 27 | this.once = this.once.bind(this); 28 | 29 | // Pagination can be finite or infinite. Infinite pagination is the default. 30 | const paginator = this; 31 | if (this.isFinite) { 32 | //this.setupFinite(); 33 | this.strategy = new FinitePagingStrategy(paginator); 34 | } else { 35 | this.strategy = new InfinitePagingStrategy(paginator); 36 | } 37 | 38 | this.next = this.next.bind(this); 39 | this.previous = this.previous.bind(this); 40 | this.goToPage = this.goToPage.bind(this); 41 | 42 | console.log('FirebasePaginator constructor this: ', this); 43 | } 44 | 45 | fire(eventName, payload) { 46 | if (typeof this.allEventHandler === 'function') { 47 | this.allEventHandler.call(this, eventName, payload); 48 | } 49 | 50 | if (this.events[eventName] && this.events[eventName].queue) { 51 | const queue = events[eventName].queue.reverse(); 52 | let i = queue.length; 53 | while (i--) { 54 | if (typeof queue[i] === 'function') { 55 | queue[i].call(this, payload); 56 | } 57 | } 58 | } 59 | } 60 | 61 | on(eventName, callback) { 62 | if (!this.events[eventName]) { 63 | this.events[eventName] = { 64 | queue: [] 65 | }; 66 | } 67 | this.events[eventName].queue.push(callback); 68 | } 69 | 70 | off(eventName, callback) { 71 | if (this.events[eventName] && this.events[eventName].queue) { 72 | const queue = this.events[eventName].queue; 73 | let i = queue.length; 74 | while (i--) { 75 | if (queue[i] === callback) { 76 | queue.splice(i, 1); 77 | } 78 | } 79 | } 80 | } 81 | 82 | once(eventName, callback) { 83 | return new Promise((resolve, reject) => { 84 | const handler = payload => { 85 | this.off(eventName, handler); 86 | if (typeof callback === 'function') { 87 | try { 88 | resolve(callback.call(this, payload)); 89 | } catch (e) { 90 | reject(e); 91 | } 92 | } else { 93 | resolve(payload); 94 | } 95 | }; 96 | this.on(eventName, handler); 97 | }); 98 | } 99 | 100 | // strategies based on finite or infinite 101 | next() { 102 | return this.strategy.next(); 103 | } 104 | previous() { 105 | return this.strategy.previous(); 106 | } 107 | reset() { 108 | return this.strategy.reset(); 109 | } 110 | goToPage(pageNumber) { 111 | console.log('access this.strategy', this.strategy); 112 | 113 | if (isFinite) return this.strategy.goToPage(pageNumber); 114 | else 115 | return new Promise((resolve, reject) => { 116 | reject({ message: 'infinite does not support paging' }); 117 | }); 118 | } 119 | } 120 | 121 | export default FirebasePaginator; 122 | -------------------------------------------------------------------------------- /es6/FirebasePaginatorFiniteStrategy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import axios from 'axios'; 3 | 4 | class FinitePagingStrategy { 5 | constructor(paginator) { 6 | this.paginator = paginator; 7 | 8 | // finite pagination 9 | let queryPath = this.paginator.ref.toString() + '.json?shallow=true'; 10 | if (this.paginator.auth) { 11 | queryPath += '&auth=' + this.paginator.auth; 12 | } 13 | const getKeys = () => { 14 | if (this.paginator.isBrowser) { 15 | return new Promise(function(resolve, reject) { 16 | var request = new XMLHttpRequest(); 17 | request.onreadystatechange = function() { 18 | if (request.readyState === 4) { 19 | var response = JSON.parse(request.responseText); 20 | if (request.status === 200) { 21 | resolve(Object.keys(response || {})); 22 | } else { 23 | reject(response); 24 | } 25 | } 26 | }; 27 | request.open('GET', queryPath, true); 28 | request.send(); 29 | }); 30 | } else { 31 | return axios.get(queryPath).then(function(res) { 32 | return Object.keys(res.data || {}); 33 | }); 34 | } 35 | }; 36 | 37 | this.goToPage = pageNumber => { 38 | const self = this.paginator; 39 | 40 | pageNumber = Math.min(self.pageCount, Math.max(1, parseInt(pageNumber))); 41 | 42 | let query; 43 | 44 | if (Object.keys(self.pages || {}).length) { 45 | // Null check for empty collections 46 | self.page = self.pages[pageNumber]; 47 | self.pageNumber = pageNumber; 48 | self.isLastPage = pageNumber === Object.keys(self.pages).length; 49 | query = self.ref 50 | .orderByKey() 51 | .limitToLast(self.pageSize) 52 | .endAt(self.page.endKey); 53 | } else { 54 | query = self.ref.orderByKey().limitToLast(self.pageSize); 55 | } 56 | 57 | return query.once('value').then(function(snap) { 58 | var collection = snap.val(); 59 | var keys = []; 60 | 61 | snap.forEach(function(childSnap) { 62 | keys.push(childSnap.key); 63 | }); 64 | 65 | self.snap = snap; 66 | self.keys = keys; 67 | self.collection = collection || {}; 68 | 69 | self.fire('value', snap); 70 | if (paginator.isLastPage) { 71 | self.fire('isLastPage'); 72 | } 73 | return paginator; 74 | }); 75 | }; 76 | 77 | this.reset = () => { 78 | return getKeys() 79 | .then(function(keys) { 80 | var orderedKeys = keys.sort(); 81 | var keysLength = orderedKeys.length; 82 | var cursors = []; 83 | 84 | for (var i = keysLength; i > 0; i -= self.pageSize) { 85 | cursors.push({ 86 | fromStart: { 87 | startRecord: i - self.pageSize + 1, 88 | endRecord: i 89 | }, 90 | fromEnd: { 91 | startRecord: keysLength - i + 1, 92 | endRecord: keysLength - i + self.pageSize 93 | }, 94 | endKey: keys[i - 1] 95 | }); 96 | } 97 | 98 | var cursorsLength = cursors.length; 99 | var k = cursorsLength; 100 | var pages = {}; 101 | while (k--) { 102 | cursors[k].pageNumber = k + 1; 103 | pages[k + 1] = cursors[k]; 104 | } 105 | paginator.pageCount = cursorsLength; 106 | paginator.pages = pages; 107 | 108 | return pages; 109 | }) 110 | .catch(function(err) { 111 | console.log('finite reset pagination error', err); 112 | }); 113 | }; 114 | 115 | const self = paginator; 116 | 117 | this.reset() // Refresh keys and go to first page. 118 | .then(function() { 119 | return self.goToPage(1); 120 | }) 121 | .then(function() { 122 | self.fire('ready', self); 123 | }); 124 | 125 | this.previous = () => { 126 | const self = paginator; 127 | return self 128 | .goToPage(Math.min(self.pageCount, self.pageNumber + 1)) 129 | .then(function() { 130 | return self.fire('previous'); 131 | }); 132 | }; 133 | 134 | this.next = () => { 135 | return this.goToPage(Math.max(1, this.pageNumber - 1)).then(function() { 136 | return fire('next'); 137 | }); 138 | }; 139 | } 140 | } 141 | export default FinitePagingStrategy; 142 | -------------------------------------------------------------------------------- /es6/FirebasePaginatorInfiniteStrategy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import axios from 'axios'; 3 | 4 | class InfinitePagingStrategy { 5 | constructor(paginator) { 6 | // infinite pagination 7 | this.paginator = paginator; 8 | 9 | const setPage = (cursor, isForward, isLastPage) => { 10 | const self = this.paginator; 11 | 12 | let query; 13 | 14 | query = self.ref.orderByKey(); 15 | 16 | // If there it's forward pagination, use limitToFirst(pageSize + 1) and startAt(theLastKey) 17 | 18 | if (self.isForward) { 19 | // forward pagination 20 | self.ref = self.ref.limitToFirst(self.pageSize + 1); 21 | if (cursor) { 22 | // check for forward cursor 23 | query = self.ref.startAt(cursor); 24 | } 25 | } else { 26 | // previous pagination 27 | query = self.ref.limitToLast(self.pageSize + 1); 28 | if (cursor) { 29 | // check for previous cursor 30 | query = self.ref.endAt(cursor); 31 | } 32 | } 33 | 34 | return query.once('value').then(snap => { 35 | const keys = []; 36 | const collection = {}; 37 | 38 | cursor = undefined; 39 | 40 | snap.forEach(function(childSnap) { 41 | keys.push(childSnap.key); 42 | if (!cursor) { 43 | cursor = childSnap.key; 44 | } 45 | collection[childSnap.key] = childSnap.val(); 46 | }); 47 | 48 | if (keys.length === self.pageSize + 1) { 49 | if (isLastPage) { 50 | delete collection[keys[keys.length - 1]]; 51 | } else { 52 | delete collection[keys[0]]; 53 | } 54 | } else if (isLastPage && keys.length < self.pageSize + 1) { 55 | // console.log('tiny page', keys.length, pageSize); 56 | } else if (isForward) { 57 | return setPage(); // force a reset if forward pagination overruns the last result 58 | } else if (!self.retainLastPage) { 59 | return setPage(undefined, true, true); // Handle overruns 60 | } else { 61 | isLastPage = true; 62 | } 63 | 64 | self.snap = snap; 65 | self.keys = keys; 66 | self.isLastPage = isLastPage || false; 67 | self.collection = collection; 68 | self.cursor = cursor; 69 | 70 | self.fire('value', snap); 71 | if (self.isLastPage) { 72 | self.fire('isLastPage'); 73 | } 74 | return this; 75 | }); 76 | }; 77 | 78 | const self = paginator; 79 | 80 | setPage().then(() => { 81 | self.fire('ready', paginator); 82 | }); // bootstrap the list 83 | 84 | this.reset = () => { 85 | return setPage().then(function() { 86 | return self.fire('reset'); 87 | }); 88 | }; 89 | 90 | this.previous = () => { 91 | return setPage(self.cursor).then(function() { 92 | return self.fire('previous'); 93 | }); 94 | }; 95 | 96 | this.next = () => { 97 | var cursor; 98 | if (self.keys && self.keys.length) { 99 | cursor = self.keys[self.keys.length - 1]; 100 | } 101 | return setPage(cursor, true).then(function() { 102 | return self.fire('next'); 103 | }); 104 | }; 105 | } 106 | } 107 | export default InfinitePagingStrategy; 108 | -------------------------------------------------------------------------------- /firebase-paginator.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 33 | 34 | 35 | 185 | -------------------------------------------------------------------------------- /firebase-paginator.js: -------------------------------------------------------------------------------- 1 | var isBrowser = typeof global != 'object' || typeof global.process != 'object'; 2 | 3 | function FirebasePaginator(ref, defaults) { 4 | var paginator = this; 5 | var defaults = defaults || {}; 6 | var pageSize = defaults.pageSize ? parseInt(defaults.pageSize, 10) : 10; 7 | var isFinite = defaults.finite ? defaults.finite : false; 8 | var retainLastPage = defaults.retainLastPage || false; 9 | var auth = defaults.auth; 10 | 11 | this.ref = ref; 12 | 13 | // Events 14 | this.listen = function(callback) { 15 | paginator.allEventHandler = callback; 16 | }; 17 | var events = {}; 18 | var fire = function(eventName, payload) { 19 | if (typeof paginator.allEventHandler === 'function') { 20 | paginator.allEventHandler.call(paginator, eventName, payload); 21 | } 22 | 23 | if (events[eventName] && events[eventName].queue) { 24 | var queue = events[eventName].queue.reverse(); 25 | var i = queue.length; 26 | while (i--) { 27 | if (typeof queue[i] === 'function') { 28 | queue[i].call(paginator, payload); 29 | } 30 | } 31 | } 32 | }; 33 | 34 | this.on = function(eventName, callback) { 35 | if (!events[eventName]) { 36 | events[eventName] = { 37 | queue: [] 38 | }; 39 | } 40 | events[eventName].queue.push(callback); 41 | }; 42 | 43 | this.off = function(eventName, callback) { 44 | if (events[eventName] && events[eventName].queue) { 45 | var queue = events[eventName].queue; 46 | var i = queue.length; 47 | while (i--) { 48 | if (queue[i] === callback) { 49 | queue.splice(i, 1); 50 | } 51 | } 52 | } 53 | }; 54 | 55 | this.once = function(eventName, callback) { 56 | return new Promise(function(resolve, reject) { 57 | var handler = function(payload) { 58 | paginator.off(eventName, handler); 59 | if (typeof callback === 'function') { 60 | try { 61 | resolve(callback.call(paginator, payload)); 62 | } catch (e) { 63 | reject(e); 64 | } 65 | } else { 66 | resolve(payload); 67 | } 68 | }; 69 | paginator.on(eventName, handler); 70 | }); 71 | }; 72 | 73 | /* 74 | * Pagination can be finite or infinite. Infinite pagination is the default. 75 | */ 76 | if (!isFinite) { 77 | // infinite pagination 78 | 79 | var setPage = function(cursor, isForward, isLastPage) { 80 | this.ref = ref.orderByKey(); 81 | 82 | // If there it's forward pagination, use limitToFirst(pageSize + 1) and startAt(theLastKey) 83 | 84 | if (isForward) { 85 | // forward pagination 86 | this.ref = this.ref.limitToFirst(pageSize + 1); 87 | if (cursor) { 88 | // check for forward cursor 89 | this.ref = this.ref.startAt(cursor); 90 | } 91 | } else { 92 | // previous pagination 93 | this.ref = this.ref.limitToLast(pageSize + 1); 94 | if (cursor) { 95 | // check for previous cursor 96 | this.ref = this.ref.endAt(cursor); 97 | } 98 | } 99 | 100 | return this.ref.once('value').then( 101 | function(snap) { 102 | var keys = []; 103 | var collection = {}; 104 | 105 | cursor = undefined; 106 | 107 | snap.forEach(function(childSnap) { 108 | keys.push(childSnap.key); 109 | if (!cursor) { 110 | cursor = childSnap.key; 111 | } 112 | collection[childSnap.key] = childSnap.val(); 113 | }); 114 | 115 | if (keys.length === pageSize + 1) { 116 | if (isLastPage) { 117 | delete collection[keys[keys.length - 1]]; 118 | } else { 119 | delete collection[keys[0]]; 120 | } 121 | } else if (isLastPage && keys.length < pageSize + 1) { 122 | // console.log('tiny page', keys.length, pageSize); 123 | } else if (isForward) { 124 | return setPage(); // force a reset if forward pagination overruns the last result 125 | } else if (!retainLastPage) { 126 | return setPage(undefined, true, true); // Handle overruns 127 | } else { 128 | isLastPage = true; 129 | } 130 | 131 | this.snap = snap; 132 | this.keys = keys; 133 | this.isLastPage = isLastPage || false; 134 | this.collection = collection; 135 | this.cursor = cursor; 136 | 137 | fire('value', snap); 138 | if (this.isLastPage) { 139 | fire('isLastPage'); 140 | } 141 | return this; 142 | }.bind(this) 143 | ); 144 | }.bind(this); 145 | 146 | setPage().then(function() { 147 | fire('ready', paginator); 148 | }); // bootstrap the list 149 | 150 | this.reset = function() { 151 | return setPage().then(function() { 152 | return fire('reset'); 153 | }); 154 | }; 155 | 156 | this.previous = function() { 157 | return setPage(this.cursor).then( 158 | function() { 159 | return fire('previous'); 160 | }.bind(this) 161 | ); 162 | }; 163 | 164 | this.next = function() { 165 | var cursor; 166 | if (this.keys && this.keys.length) { 167 | cursor = this.keys[this.keys.length - 1]; 168 | } 169 | return setPage(cursor, true).then(function() { 170 | return fire('next'); 171 | }); 172 | }; 173 | } else { 174 | // finite pagination 175 | var queryPath = ref.toString() + '.json?shallow=true'; 176 | if (auth) { 177 | queryPath += '&auth=' + auth; 178 | } 179 | var getKeys = function() { 180 | if (isBrowser) { 181 | return new Promise(function(resolve, reject) { 182 | var request = new XMLHttpRequest(); 183 | request.onreadystatechange = function() { 184 | if (request.readyState === 4) { 185 | var response = JSON.parse(request.responseText); 186 | if (request.status === 200) { 187 | resolve(Object.keys(response || {})); 188 | } else { 189 | reject(response); 190 | } 191 | } 192 | }; 193 | request.open('GET', queryPath, true); 194 | request.send(); 195 | }); 196 | } else { 197 | var axios = require('axios'); 198 | return axios.get(queryPath).then(function(res) { 199 | return Object.keys(res.data || {}); 200 | }); 201 | } 202 | }; 203 | 204 | this.goToPage = function goToPage(pageNumber) { 205 | pageNumber = Math.min(this.pageCount, Math.max(1, parseInt(pageNumber))); 206 | if (Object.keys(this.pages || {}).length) { 207 | // Null check for empty collections 208 | paginator.page = this.pages[pageNumber]; 209 | paginator.pageNumber = pageNumber; 210 | paginator.isLastPage = 211 | pageNumber === Object.keys(paginator.pages).length; 212 | paginator.ref = ref 213 | .orderByKey() 214 | .limitToLast(pageSize) 215 | .endAt(paginator.page.endKey); 216 | } else { 217 | paginator.ref = ref.orderByKey().limitToLast(pageSize); 218 | } 219 | 220 | return this.ref.once('value').then(function(snap) { 221 | var collection = snap.val(); 222 | var keys = []; 223 | 224 | snap.forEach(function(childSnap) { 225 | keys.push(childSnap.key); 226 | }); 227 | 228 | paginator.snap = snap; 229 | paginator.keys = keys; 230 | paginator.collection = collection || {}; 231 | 232 | fire('value', snap); 233 | if (paginator.isLastPage) { 234 | fire('isLastPage'); 235 | } 236 | return paginator; 237 | }); 238 | }; 239 | 240 | this.reset = function() { 241 | return getKeys() 242 | .then(function(keys) { 243 | var orderedKeys = keys.sort(); 244 | var keysLength = orderedKeys.length; 245 | var cursors = []; 246 | 247 | for (var i = keysLength; i > 0; i -= pageSize) { 248 | cursors.push({ 249 | fromStart: { 250 | startRecord: i - pageSize + 1, 251 | endRecord: i 252 | }, 253 | fromEnd: { 254 | startRecord: keysLength - i + 1, 255 | endRecord: keysLength - i + pageSize 256 | }, 257 | endKey: keys[i - 1] 258 | }); 259 | } 260 | 261 | var cursorsLength = cursors.length; 262 | var k = cursorsLength; 263 | var pages = {}; 264 | while (k--) { 265 | cursors[k].pageNumber = k + 1; 266 | pages[k + 1] = cursors[k]; 267 | } 268 | paginator.pageCount = cursorsLength; 269 | paginator.pages = pages; 270 | 271 | return pages; 272 | }) 273 | .catch(function(err) { 274 | console.log('finite reset pagination error', err); 275 | }); 276 | }; 277 | 278 | this.reset() // Refresh keys and go to first page. 279 | .then(function() { 280 | return paginator.goToPage(1); 281 | }) 282 | .then(function() { 283 | fire('ready', paginator); 284 | }); 285 | 286 | this.previous = function() { 287 | return this.goToPage(Math.min(this.pageCount, this.pageNumber + 1)).then( 288 | function() { 289 | return fire('previous'); 290 | } 291 | ); 292 | }.bind(paginator); 293 | 294 | this.next = function() { 295 | return this.goToPage(Math.max(1, this.pageNumber - 1)).then(function() { 296 | return fire('next'); 297 | }); 298 | }.bind(paginator); 299 | } 300 | } 301 | 302 | if (typeof window == 'object') { 303 | window.FirebasePaginator = FirebasePaginator; 304 | } 305 | 306 | if (typeof module == 'object') { 307 | module.exports = FirebasePaginator; 308 | } 309 | -------------------------------------------------------------------------------- /firebase-paginator.spec.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | const firebaseConfig = require('./env.json').firebaseConfig; 3 | const secret = firebaseConfig.secret; 4 | 5 | admin.initializeApp({ 6 | databaseURL: firebaseConfig.databaseURL, 7 | credential: admin.credential.cert(firebaseConfig.serviceAccount) 8 | }); 9 | 10 | const ref = admin.database().ref('firebasePaginator'); 11 | const collectionRef = ref.child('collection'); 12 | const smallCollectionRef = ref.child('small-collection'); 13 | const emptyCollectionRef = ref.child('empty-collection'); 14 | 15 | const FirebasePaginator = require('./firebase-paginator'); 16 | 17 | describe('Firebase Paginator', () => { 18 | let paginator; 19 | 20 | beforeAll(done => { 21 | ref.remove().then(done); 22 | }); 23 | 24 | beforeAll(done => { 25 | populateCollection(100, collectionRef).then(done); 26 | }); 27 | 28 | beforeAll(done => { 29 | populateCollection(3, smallCollectionRef).then(done); 30 | }); 31 | 32 | function populateCollection(count, ref) { 33 | return new Promise(function(resolve, reject) { 34 | var promises = []; 35 | var i = count; 36 | 37 | while (i--) { 38 | promises.push(ref.push(count - i)); 39 | } 40 | 41 | Promise.all(promises).then(resolve, reject); 42 | }); 43 | } 44 | 45 | describe('Initial Load', () => { 46 | it('collection', done => { 47 | collectionRef.once('value').then(snap => { 48 | expect(snap.numChildren()).toEqual(100); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('small-collection', done => { 54 | smallCollectionRef.once('value').then(snap => { 55 | expect(snap.numChildren()).toEqual(3); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('empty-collection', done => { 61 | emptyCollectionRef.once('value').then(snap => { 62 | expect(snap.numChildren()).toEqual(0); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('Finite Pagination', () => { 69 | describe('empty-collection', () => { 70 | beforeEach(() => { 71 | paginator = new FirebasePaginator(emptyCollectionRef, { 72 | finite: true, 73 | auth: secret 74 | }); 75 | }); 76 | 77 | testPage(0, undefined, undefined); 78 | }); 79 | 80 | describe('small-collection', () => { 81 | describe('pageSize: 10', () => { 82 | beforeEach(() => { 83 | paginator = new FirebasePaginator(smallCollectionRef, { 84 | finite: true, 85 | auth: secret, 86 | pageSize: 10 87 | }); 88 | }); 89 | testPage(3, 1, 3); 90 | }); 91 | 92 | describe('pageSize: 3', () => { 93 | beforeEach(() => { 94 | paginator = new FirebasePaginator(smallCollectionRef, { 95 | finite: true, 96 | auth: secret, 97 | pageSize: 3 98 | }); 99 | }); 100 | testPage(3, 1, 3); 101 | }); 102 | }); 103 | 104 | describe('collection', () => { 105 | describe('pageSize: 10', () => { 106 | beforeAll(() => { 107 | paginator = new FirebasePaginator(collectionRef, { 108 | finite: true, 109 | auth: secret, 110 | pageSize: 10 111 | }); 112 | }); 113 | 114 | testPage(10, 91, 100); 115 | 116 | for (let i = 90; i > 0; i -= 10) { 117 | testPage(10, i - 9, i, false, 'previous'); 118 | } 119 | 120 | testPage(10, 1, 10, 'should fail to back paginate', 'previous'); 121 | }); 122 | 123 | describe('pageSize: 3', () => { 124 | beforeAll(() => { 125 | paginator = new FirebasePaginator(collectionRef, { 126 | finite: true, 127 | auth: secret, 128 | pageSize: 3 129 | }); 130 | }); 131 | 132 | testPage(3, 98, 100, false); 133 | testPage(3, 95, 97, false, 'previous'); 134 | testPage(3, 92, 94, false, 'previous'); 135 | testPage(3, 95, 97, false, 'next'); 136 | testPage(3, 98, 100, false, 'next'); 137 | testPage( 138 | 3, 139 | 98, 140 | 100, 141 | 'should fail to forward paginate and stick 98 to 100', 142 | 'next' 143 | ); 144 | }); 145 | 146 | describe('pageSize: 30', () => { 147 | beforeAll(() => { 148 | paginator = new FirebasePaginator(collectionRef, { 149 | finite: true, 150 | auth: secret, 151 | pageSize: 30 152 | }); 153 | }); 154 | 155 | testPage(30, 71, 100, false); 156 | testPage(30, 41, 70, false, 'previous'); 157 | testPage(30, 11, 40, false, 'previous'); 158 | testPage(10, 1, 10, false, 'previous'); 159 | }); 160 | }); 161 | }); 162 | 163 | describe('Infinite Pagination', () => { 164 | describe('empty-collection', () => { 165 | beforeEach(() => { 166 | paginator = new FirebasePaginator(emptyCollectionRef, { 167 | finite: false, 168 | auth: secret 169 | }); 170 | }); 171 | 172 | testPage(0, undefined, undefined); 173 | }); 174 | 175 | describe('small-collection', () => { 176 | describe('pageSize: 10', () => { 177 | beforeEach(() => { 178 | paginator = new FirebasePaginator(smallCollectionRef, { 179 | finite: false, 180 | auth: secret, 181 | pageSize: 10 182 | }); 183 | }); 184 | testPage(3, 1, 3); 185 | }); 186 | 187 | describe('pageSize: 3', () => { 188 | beforeEach(() => { 189 | paginator = new FirebasePaginator(smallCollectionRef, { 190 | finite: false, 191 | auth: secret, 192 | pageSize: 3 193 | }); 194 | }); 195 | testPage(3, 1, 3); 196 | }); 197 | }); 198 | 199 | describe('collection', () => { 200 | describe('pageSize: 10', () => { 201 | beforeAll(() => { 202 | paginator = new FirebasePaginator(collectionRef, { 203 | finite: false, 204 | auth: secret, 205 | pageSize: 10 206 | }); 207 | }); 208 | 209 | testPage(10, 91, 100); 210 | 211 | for (let i = 90; i > 0; i -= 10) { 212 | testPage(10, i - 9, i, false, 'previous'); 213 | } 214 | 215 | testPage(10, 1, 10, 'should fail to back paginate', 'previous'); 216 | }); 217 | 218 | describe('pageSize: 3', () => { 219 | beforeAll(() => { 220 | paginator = new FirebasePaginator(collectionRef, { 221 | finite: false, 222 | auth: secret, 223 | pageSize: 3 224 | }); 225 | }); 226 | 227 | testPage(3, 98, 100, false); 228 | testPage(3, 95, 97, false, 'previous'); 229 | testPage(3, 92, 94, false, 'previous'); 230 | testPage(3, 95, 97, false, 'next'); 231 | testPage(3, 98, 100, false, 'next'); 232 | testPage( 233 | 3, 234 | 98, 235 | 100, 236 | 'should fail to forward paginate and stick 98 to 100', 237 | 'next' 238 | ); 239 | }); 240 | 241 | describe('pageSize: 30', () => { 242 | beforeAll(() => { 243 | paginator = new FirebasePaginator(collectionRef, { 244 | finite: false, 245 | auth: secret, 246 | pageSize: 30 247 | }); 248 | }); 249 | 250 | testPage(30, 71, 100, false); 251 | testPage(30, 41, 70, false, 'previous'); 252 | testPage(30, 11, 40, false, 'previous'); 253 | testPage(30, 1, 30, false, 'previous'); 254 | }); 255 | 256 | describe('pageSize: 30', () => { 257 | beforeAll(() => { 258 | paginator = new FirebasePaginator(collectionRef, { 259 | finite: false, 260 | auth: secret, 261 | pageSize: 30, 262 | retainLastPage: true 263 | }); 264 | }); 265 | 266 | testPage(30, 71, 100, false); 267 | testPage(30, 41, 70, false, 'previous'); 268 | testPage(30, 11, 40, false, 'previous'); 269 | testPage(10, 1, 10, false, 'previous'); 270 | 271 | it('should fire isLastPage even if retainLastValue is true', done => { 272 | let firedCount = 0; 273 | paginator.once('isLastPage').then(() => { 274 | firedCount++; 275 | }); 276 | paginator 277 | .previous() 278 | .then(() => { 279 | expect(firedCount).toEqual(1); 280 | done(); 281 | }) 282 | .catch(done.fail); 283 | }); 284 | }); 285 | }); 286 | }); 287 | 288 | function testPage(length, start, end, testName, precursorName) { 289 | it(testName || `should return records ${start} to ${end}`, done => { 290 | Promise.resolve() 291 | .then(() => { 292 | if (precursorName) { 293 | return paginator[precursorName](); 294 | } else { 295 | return paginator.once('value'); 296 | } 297 | }) 298 | .then(snap => { 299 | var collection = paginator.collection || {}; 300 | var keys = Object.keys(collection); 301 | var i = keys.length; 302 | 303 | // console.log('output', length, collection); 304 | expect(i).toEqual(length); 305 | if (length) { 306 | expect(collection[keys[0]]).toEqual(start); 307 | expect(collection[keys[i - 1]]).toEqual(end); 308 | } 309 | done(); 310 | }); 311 | }); 312 | } 313 | }); 314 | -------------------------------------------------------------------------------- /hero.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import FirebasePaginator from './firebase-paginator'; 2 | export default FirebasePaginator; 3 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-paginator", 3 | "version": "1.1.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "firebase-admin": { 8 | "version": "4.2.1", 9 | "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-4.2.1.tgz", 10 | "integrity": "sha1-kXlL/CFO4h3sx2XTCEERZqBcCyA=", 11 | "requires": { 12 | "@types/jsonwebtoken": "7.2.0", 13 | "faye-websocket": "0.9.3", 14 | "jsonwebtoken": "7.1.9" 15 | }, 16 | "dependencies": { 17 | "@types/jsonwebtoken": { 18 | "version": "7.2.0", 19 | "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-7.2.0.tgz", 20 | "integrity": "sha1-D+0yyFAdqArJg50tQDplyD13b/0=", 21 | "requires": { 22 | "@types/node": "7.0.12" 23 | } 24 | }, 25 | "@types/node": { 26 | "version": "7.0.12", 27 | "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.12.tgz", 28 | "integrity": "sha1-rl9noZwV91IUgATbB8u7Ny5p78k=" 29 | }, 30 | "base64url": { 31 | "version": "2.0.0", 32 | "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", 33 | "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" 34 | }, 35 | "buffer-equal-constant-time": { 36 | "version": "1.0.1", 37 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 38 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 39 | }, 40 | "ecdsa-sig-formatter": { 41 | "version": "1.0.9", 42 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", 43 | "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", 44 | "requires": { 45 | "base64url": "2.0.0", 46 | "safe-buffer": "5.0.1" 47 | } 48 | }, 49 | "faye-websocket": { 50 | "version": "0.9.3", 51 | "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.9.3.tgz", 52 | "integrity": "sha1-SCpQWw3wrmJrlphm0710DNuWLoM=", 53 | "requires": { 54 | "websocket-driver": "0.6.5" 55 | } 56 | }, 57 | "hoek": { 58 | "version": "2.16.3", 59 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", 60 | "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" 61 | }, 62 | "isemail": { 63 | "version": "1.2.0", 64 | "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", 65 | "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" 66 | }, 67 | "joi": { 68 | "version": "6.10.1", 69 | "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", 70 | "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", 71 | "requires": { 72 | "hoek": "2.16.3", 73 | "isemail": "1.2.0", 74 | "moment": "2.18.1", 75 | "topo": "1.1.0" 76 | } 77 | }, 78 | "jsonwebtoken": { 79 | "version": "7.1.9", 80 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.1.9.tgz", 81 | "integrity": "sha1-hHgE5SWL7FqUmajcSl56O64I1Yo=", 82 | "requires": { 83 | "joi": "6.10.1", 84 | "jws": "3.1.4", 85 | "lodash.once": "4.1.1", 86 | "ms": "0.7.3", 87 | "xtend": "4.0.1" 88 | } 89 | }, 90 | "jwa": { 91 | "version": "1.1.5", 92 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", 93 | "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", 94 | "requires": { 95 | "base64url": "2.0.0", 96 | "buffer-equal-constant-time": "1.0.1", 97 | "ecdsa-sig-formatter": "1.0.9", 98 | "safe-buffer": "5.0.1" 99 | } 100 | }, 101 | "jws": { 102 | "version": "3.1.4", 103 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", 104 | "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", 105 | "requires": { 106 | "base64url": "2.0.0", 107 | "jwa": "1.1.5", 108 | "safe-buffer": "5.0.1" 109 | } 110 | }, 111 | "lodash.once": { 112 | "version": "4.1.1", 113 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 114 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 115 | }, 116 | "moment": { 117 | "version": "2.18.1", 118 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", 119 | "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" 120 | }, 121 | "ms": { 122 | "version": "0.7.3", 123 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.3.tgz", 124 | "integrity": "sha1-cIFVpeROM/X9D8U+gdDUCpG+H/8=" 125 | }, 126 | "safe-buffer": { 127 | "version": "5.0.1", 128 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", 129 | "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" 130 | }, 131 | "topo": { 132 | "version": "1.1.0", 133 | "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", 134 | "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", 135 | "requires": { 136 | "hoek": "2.16.3" 137 | } 138 | }, 139 | "websocket-driver": { 140 | "version": "0.6.5", 141 | "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", 142 | "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", 143 | "requires": { 144 | "websocket-extensions": "0.1.1" 145 | } 146 | }, 147 | "websocket-extensions": { 148 | "version": "0.1.1", 149 | "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz", 150 | "integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=" 151 | }, 152 | "xtend": { 153 | "version": "4.0.1", 154 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 155 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 156 | } 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-paginator", 3 | "version": "1.1.2", 4 | "description": "Pagination for Firebase", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "test:watch": "jest --watch", 9 | "start": "npm run serve", 10 | "serve": "polymer serve -p 8080", 11 | "bundle": "parcel es-module.html" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/deltaepsilon/firebase-paginator.git" 16 | }, 17 | "keywords": [ 18 | "firebase", 19 | "pagination" 20 | ], 21 | "author": "Chris Esplin ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/deltaepsilon/firebase-paginator/issues" 25 | }, 26 | "homepage": "https://github.com/deltaepsilon/firebase-paginator", 27 | "dependencies": { 28 | "axios": "^0.13.1", 29 | "firebase-admin": "^4.2.1" 30 | }, 31 | "devDependencies": { 32 | "firebase": "^3.2.1", 33 | "jest": "^20.0.4", 34 | "jest-cli": "^20.0.4", 35 | "polymer-cli": "^1.6.0", 36 | "tape": "^4.6.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /with-script.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Firebase Paginator: With Script 8 | 9 | 10 | 11 | 14 | 15 | --------------------------------------------------------------------------------