// 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>