// sample data var data = [ {city:"Fort Dodge", zip:"34889"}, {city:"Fitchburg", zip:"83366"}, {city:"Menomonee Falls", zip:"52534"}, {city:"Norfolk", zip:"02232"}, {city:"Dearborn", zip:"55899"}, {city:"Pomona", zip:"90483"}, {city:"Port Orford", zip:"02565"}, {city:"Clovis", zip:"98500"}, {city:"Sister Bay", zip:"20285"}, {city:"Bethlehem", zip:"57757"}, {city:"Broken Arrow", zip:"18820"}, {city:"Idabel", zip:"65964"}, {city:"Macomb", zip:"43974"}, {city:"Dearborn", zip:"03451"}, {city:"Corinth", zip:"93401"}, {city:"El Paso", zip:"26282"}, {city:"Cortland", zip:"67010"}, {city:"Barre", zip:"52555"}, {city:"Aspen", zip:"62155"}, {city:"New Brunswick", zip:"68095"}, {city:"Garland", zip:"85242"}, {city:"Gloucester", zip:"33586"}, {city:"Beacon", zip:"21493"}, {city:"New Rochelle", zip:"82251"}, {city:"Fairbanks", zip:"66241"}, {city:"Lewiston", zip:"38572"}, {city:"Bismarck", zip:"91912"}, {city:"Hammond", zip:"69642"}, {city:"Uniontown", zip:"31508"}, {city:"Rolla", zip:"92430"} ]; var getFakeJsonRequest = function(page, pageSize){ var start = (page-1)*pageSize, end = start + pageSize; while(end > data.length){ Array.prototype.push.apply(data,data); } var array = data.slice(start,end); return { json: JSON.stringify(array), delay: 1 }; }; /* knockout-paged.js - A Pager Plugin for Knockout.JS Written By: Leland M. Richardson Desired API: .paged(Number pageSize); assumes static data, creates pager with given pageSize .paged(Number pageSize, String url); assumes `url` is an AJAX endpoint which returns the requested data with url parameters "pg" and "perPage" .paged(Object config); pass config object with optional + required parameters (most flexible) //todo: perhaps some "inf-scroll" type functionality? //todo: restful configuration? Object Configuration: .paged({ pageSize: Number (10), cached: Boolean (true), url: String }); */ ;(function(ko,$){ // module scope // UTILITY METHODSextend // ------------------------------------------------------------------ var extend = ko.utils.extend; // escape regex stuff var regexEscape = function(str) { return (str + '').replace(/[\-#$\^*()+\[\]{}|\\,.?\s]/g, '\\$&'); }; // simple string replacement var tmpl = function(str, obj) { for (var i in obj) { if (obj.hasOwnProperty(i) !== true) continue; // convert to string var value = obj[i] + ''; str = str.replace(new RegExp('{' + regexEscape(i) + '}', 'g'), value); } return str; }; // construct url with proper data var construct_url = function(template,pg,pageSize){ var start = pageSize * (pg-1), end = start + pageSize, data = {pg: pg, pageSize: pageSize, start: start, end: end}; return typeof template === 'function' ? template(data) : tmpl(template,data); }; //constructor mapping function... var cmap = function(array,Ctor){ return $.map(array,function(el){ return new Ctor(el) }); }; // constructs the config object for each isntance based on parameters var config_init = function(defaults,a,b,c){ var cfg = extend({},defaults); if(typeof a === "number"){ // pageSize passed as first param cfg.pageSize = a; if(typeof b === "string"){ cfg.url = b; cfg.async = true; } } else { extend(cfg,a); } // don't let user override success function... use mapFromServer instead if(cfg.ajaxOptions && cfg.ajaxOptions.success){ console.log("'success' is not a valid ajaxOptions property. Please look into using 'mapFromServer' instead."); delete cfg.ajaxOptions.success; } return cfg; }; // PLUGIN DEFAULTS // ---------------------------------------------------------------------- var _defaults = { pageSize: 10, async: false, //TODO: make best guess based on other params passed? // async only options // -------------------------------------------- getPage: null, url: null, // this can be a string or a function ({pg: pg, pageSize: pageSize, start: start, end: end}) ctor: null, //constructor to be used for // function to be applied on "success" callback to map // response to array. Signature: (Object response) -> (Array) mapFromServer: null, ajaxOptions: {}, //options to pass into jQuery on each request cache: true }; // PLUGIN CONSTRUCTOR // ----------------------------------------------------------------------- var paged = function(a,b){ var items = this, hasInitialData = this().length > 0; // target observableArray // config initialization var cfg = config_init(_defaults,a,b), // current page current = ko.observable(1), pagedItems = ko.computed(function(){ var pg = current(), start = cfg.pageSize * (pg-1), end = start + cfg.pageSize; return items().slice(start,end); }); // array of loaded var loaded = [true]; // set [0] to true just because. if(hasInitialData){ loaded[current()] = true; } var isLoading = ko.observable(true); // next / previous / goToPage methods var goToPage = cfg.async ? function(pg){ if(cfg.cache && loaded[pg]){ //data is already loaded. change page in setTimeout to make async isLoading(true); setTimeout(function(){ current(pg); isLoading(false); },0); } else { // user has specified URL. make ajax request // ************************************************************ // IMPORTANT: API IS MODIFIED HERE TO WORK WITH jsFiddle!!!!!!! // ************************************************************ isLoading(true); $.ajax(extend({ url: '/echo/json/', data: getFakeJsonRequest(pg, cfg.pageSize), type: "POST", success: function(res){ // allow user to apply custom mapping from server result to data to insert into array var results; if(cfg.mapFromServer){ results = cfg.mapFromServer(res); } else { //todo: check to see if res.data or res.items or something... results = res; } onPageReceived(pg,results); isLoading(false); }, complete: function() { //todo: user could override... make sure they don't? (use compose) isLoading(false); } },cfg.ajaxOptions)); } } : current; // if not async, all we need to do is assign pg to current //maps new data to underlying array var onPageReceived = function(pg,data){ // if constructor passed in, map data to constructor if(cfg.ctor !== null){ data = cmap(data,cfg.ctor); } // append data to items array var start = cfg.pageSize*(pg-1); data.unshift(start,0); Array.prototype.splice.apply(items(),data); items.notifySubscribers(); if(cfg.cache) {loaded[pg] = true;} current(pg); }; var next = function(){ if(next.enabled()) goToPage(current()+1); }; next.enabled = ko.computed(function(){ //TODO: handle differently for ajax stuff return true || items().length > cfg.pageSize * current(); }); var prev = function(){ if(prev.enabled()) goToPage(current()-1); }; prev.enabled = ko.computed(function(){ return current() > 1; }); // actually go to first page goToPage(current()); // exported properties extend(items,{ current: current, pagedItems: pagedItems, pageSize: cfg.pageSize, isLoading: isLoading, // might not need this if not async? next: next, prev: prev, goToPage: goToPage, __paged_cfg: extend({},cfg) //might want to remove later }); // return target return items; }; // expose default options to be changed for users paged.defaultOptions = _defaults; //export to knockout ko.observableArray.fn.paged = paged; }(ko,$)); var Example = function(){ this.items = ko.observableArray().paged(4,'/url/which/wont/be/used/in/this/example'); }; ko.applyBindings(new Example());
<div id="test" class="container"> <table class="table table-bordered"> <thead> <tr> <th>City</th> <th>Zip</th> </tr> </thead> <tbody data-bind="foreach: items.pagedItems"> <tr> <td data-bind="text: city"></td> <td data-bind="text: zip"></td> </tr> </tbody> </table> <ul class="pager"> <li data-bind="css: {'disabled': !items.prev.enabled()}"> <a href="#" data-bind="click: items.prev">Previous</a> </li> <li class="li-loader"> <span> <span class="ajax-loader" data-bind="css: {active: items.isLoading}"></span> </span> </li> <li data-bind="css: {'disabled': !items.next.enabled()}"> <a href="#" data-bind="click: items.next">Next</a> </li> </ul> </div>