I'm new to the whole Backbone (and I'm actually not much of a JavaScript programmer yet), and I'm running into issues with a filter form.
Basically I have a database that my backbone collection gets populated from. On the page, there is a filter section where I can checkmark things such as: "Jobstatus: Running, Completed, Failed, etc..." Or input such as "User name: test0." All that works great until I started paginating my results.
I have a router set up to handle #page/:page. This works as well, say I go to page 2 with no filters checked, and then on page 2 I check some filters (which right now sets my results back to page 1 by using navigate(page/1)), when I hit my back button, I go back to page 2 (since that is where my no filter results were last on), but my filter box is still checked.
So if there are 10 rows per page and I have 14 results with no filters. I go to page 2 to see results 11-14, check a filter that returns me to page 1 with a total of 5 results, and then i hit back. I am now on page 2 of those filtered results seeing rows 11-5 of 5...
Is there anyway for the history to remember the form checkboxes and other inputs? So if I hit back in the situation I describe, it goes to page 2, but with the form filters removes as they were prior.
I was thinking I'd have to use routes for every value on the form... but hoping there is a better way of doing this (hopefully without rewriting the entire code as well).
Any help is appreciated.
I have two main views for input: one tied to the filter form, one for the pagination selection. The filter form view calls another view for the results (which is ties to a collection of rows).
Here is the filter form view:
var FilterForm = Backbone.View.extend({
events: {
"submit": "refreshData",
"change input": "refreshData"
},
initialize: function () {
this.refreshData();
},
refreshData: function () {
Backbone.history.navigate('page/1');
pageNumberModel.set('pageNumber', 1);
this.newPage();
},
newPage: function () {
var pageNumber = pageNumberModel.get('pageNumber');
var rowsPerPage = pageNumberModel.get('rowsPerPage');
var startRow = ((pageNumber * rowsPerPage) - rowsPerPage) + 1;
var endRow = startRow + (rowsPerPage - 1);
$('#startrow').val(startRow);
$('#endrow').val(endRow);
var inputData = this.$el.serializeArray();
var inputDatareduced = _(inputData).reduce(function (acc, field) {
acc[field.name] = field.value;
return acc;
});
$.get("Database.aspx", inputData, function (outputData) {
jobQueueRows.set($.parseJSON($.parseJSON(outputData)['JobQueue']));
jobQueueRowsView.render();
pageNumberModel.set($.parseJSON($.parseJSON(outputData)['Pagination']));
pageNumberView.render();
rowNumberView.render();
});
}
});
I would recommend creating a filterModel for your FilterForm view and using that to hold the state. Here is a reference implementation.
FilterModel:
var FilterModel = Backbone.Model.extend({
initialize: function(){
if( !this.get('filters'))
this.set({filters: new Array()});
},
applyFilter: function(key){
var filters = this.get("filters");
set("filters", _.union(filters, key));
return this;
},
removeFilter: function(key){
var filters = this.get("filters");
set("filters", _.without(filters, key));
return this;
},
defaults: {
fooFilters: [
{key: "size", label: "Filter By Size"},
{key: "color", label: "Past Week"}]
}
});
Bind a handler in your view to update the filter model:
filterClicked: function (e) {
var filter = $(e.target);
if(filter.is(':checked'))
this.model.applyFilter(filter.attr("id"));
else
this.model.removeFilter(filter.attr("id"));
},
Modify your filter template to apply the models state to your checkboxes when rendering:
<ul id="fooFilters" class="foo-filter-list">
<% _.each(fooFilters, function(f) { %>
<li>
<label for="<%=f.key%>"><%=f.name%></label>
<input type="checkbox" name="" value="" id="<%=f.key%>" <%if($.inArray(f.key, filters) === -1 {%>checked<%}%>/>
</li>
<% }); %>
</ul>
Related
I am working on setting up a scenario as following:
1) User is shown existing results on first grid
2) User can select multiple results and click an 'Edit' button which will extract the selected items from the first grid
3)Second grid will be populated with the rows the user has selected from the first grid and will allow them to make edits to the content
4)Pressing save will update the results and show the first grid with the rows updated
So far using drips and drabs of various forum threads (here and here), I have managed to accomplish the first two steps.
$("#editButton").kendoButton({
click: function () {
// extract selected results from the grid and send along with transition
var gridResults = $("#resultGrid").data("kendoGrid"); // sourceGrid
var gridConfig = $("#resultConfigGrid").data("kendoGrid"); // destinationGrid
gridResults.select().each(function () {
var dataItem = gridResults.dataItem($(this));
gridConfig.dataSource.add(dataItem);
});
gridConfig.refresh();
transitionToConfigGrid();
}
});
dataItem returns what i am expecting to see with regards to the selected item(s) - attached dataItem.png. I can see the gridConfig populating but with blank rows (gridBlankRows.png).
gridConfig setup:
$(document).ready(function () {
// build the custom column schema based on the number of lots - this can vary
var columnSchema = [];
columnSchema.push({ title: 'Date Time'});
for(var i = 0; i < $("#maxNumLots").data("value"); ++i)
{
columnSchema.push({
title: 'Lot ' + i,
columns: [{
title: 'Count'
}, {
title: 'Mean'
}, {
title: 'SD'
}]
});
}
columnSchema.push({ title: 'Comment'});
columnSchema.push({ title: 'Review Comment' });
// build the datasource with CU operations
var configDataSource = new kendo.data.DataSource({
transport: {
create: function(options) {},
update: function(options) {}
}
});
$("#resultConfigGrid").kendoGrid({
columns: columnSchema,
editable: true
});
});
I have run out of useful reference material to identify what I am doing wrong / what could be outstanding here. Any help/guidance would be greatly appreciated.
Furthermore, I will also need functionality to 'Add New' results. If possible I would like to use the same grid (with a blank datasource) in order to accomplish this. The user can then add rows to the second grid and save with similar functionality to the update functionality. So if there is any way to factor this into the response, I would appreciate it.
The following example...
http://dojo.telerik.com/EkiVO
...is a modified version of...
http://docs.telerik.com/kendo-ui/framework/datasource/crud#examples
A couple of notes:
it matters if you are adding plain objects to the second Grid's dataSource (gridConfig.dataSource.add(dataItem).toJSON();), or Kendo UI Model objects (gridConfig.dataSource.add(dataItem);). In the first case, you will need to pass back the updated values from Grid2 to Grid1, otherwise this will occur automatically;
there is no need to refresh() the second Grid after adding, removing or changing its data items
both Grid dataSources must be configured for CRUD operations, you can follow the CRUD documentation
the Grid does not persist its selection across rebinds, so if you want to preserve the selection in the first Grid after some values have been changed, use the approach described at Persist Row Selection
I have an ExtJS grid that has a PagingToolbar for (remote) paging, and a checkbox column defined as:
{
dataIndex : 'selected',
xtype: 'checkcolumn',
width : 60
}
However, if I check a box and then page forwards and backwards, the checkbox state is not saved - all the checkboxes are unchecked.
I guess since the store only contains data for the current page (from the server), I need some way of storing the state for rows that are not in the current page of data, and then reinstating the checkboxes when I return to that page.
Is there a best practice, or example of storing the checkbox state in the store while paging?
Well, that's so low-level work that no one has yet thought to make a "best practice" for it. E.g.
beforeload:function(store) {
if(!store.checkedItems) store.checkedItems = [];
store.each(function(item) {
store.checkedItems[item.get("Id")] = item.get("selected");
});
},
load:function(store) {
if(!store.checkedItems) store.checkedItems = [];
store.each(function(item) {
item.set("selected",store.checkedItems[item.get("Id")]);
});
}
or
beforeload:function(store) {
if(!store.checkedItems) store.checkedItems = [];
store.each(function(item) {
store.checkedItems[item.get("Id")] = {selected: item.get("selected") }; // extensible if you want to keep more than one checkbox...
});
},
load:function(store) {
if(!store.checkedItems) store.checkedItems = [];
store.each(function(item) {
item.set(store.checkedItems[item.get("Id")]);
});
}
In an AngularJS application, I have an ag-grid that uses virtual paging/infinite scrolling to lazy-load rows from a dataset that is too large to show at once. I have turned on check-box selection in the first column, so that the user should be able to select individual rows for arbitrary application-specific actions.
The AngularJS application uses ui-router to control multiple views. So, building on the virtual-paging example with "sorting & filtering", with constructed data about Olympic winners, from the ag-grid documentation, I've further extended the code a bit. From index.html:
<body ng-controller="MainController" class="container">
<div ui-view="contents"></div>
</body>
and the following ui-router states:
myapp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("example.page1")
$stateProvider
.state('example', {
abstract: true,
views: {
contents: {
template: '<div ui-view="example"></div>'
}
}
})
.state('example.page1', {
url: '/page1',
views: {
example: {
templateUrl: 'page1.html'
}
}
})
.state('example.page2', {
url: '/page2',
views: {
example: {
template: 'Go back to the <a ui-sref="example.page1">example grid</a>.'
}
}
});
});
where page1.html looks like the following:
<div ng-controller="GridController">
<div ag-grid="gridOptions" class="ag-fresh" style="height: 250px;"></div>
</div>
<div>
<h3>Selected rows:</h3>
<ul class="list-inline">
<li ng-repeat="row in currentSelection track by row.id">
<a ng-click="remove(row)">
<div class="badge">#{{ row.id }}, {{ row.athlete }}</div>
</a>
</li>
</ul>
</div>
<p>Go to <a ui-sref="example.page2">the other page</a>.</p>
What I want to accomplish:
That selections made in the ag-grid is remembered (sticky) when scrolling a (virtual) page out of view and back again, so that a user can select multiple rows on separate pages.
That the remembered selections are available outside the grid, and support adding and removing selections (as intended by the ng-click="remove(row)" in page1.html, shown above).
That the selections should be remembered when switching from the view with the ag-grid to another one, and back again.
(Optional) That the selections are remembered for the user's session.
How can I accomplish this?
I've created a working example of this can be implemented.
First of all, we'll write a AngularJS service, selectionService to keep track of the selections:
function _emptyArray(array) {
while (array.length) {
array.pop();
}
}
function _updateSharedArray(target, source) {
_emptyArray(target);
_.each(source, function _addActivity(activity) {
target.push(activity);
});
}
myapp.factory('selectionService', function ($rootScope, $window) {
var _collections = {},
_storage = $window.sessionStorage,
_prefix = 'selectionService';
angular.element($window).on('storage', _updateOnStorageChange);
function _persistCollection(collection, data) {
_storage.setItem(_prefix + ':' + collection, angular.toJson(data));
}
function _loadCollection(collection) {
var item = _storage.getItem(_prefix + ':' + collection);
return item !== null ? angular.fromJson(item) : item;
}
function _updateOnStorageChange(event) {
var item = event.originalEvent.newValue;
var keyParts = event.originalEvent.key.split(':');
if (keyParts.length < 2 || keyParts[0] !== _prefix) {
return;
}
var collection = keyParts[1];
_updateSharedArray(_getCollection(collection), angular.fromJson(item));
_broadcastUpdate(collection);
}
function _broadcastUpdate(collection) {
$rootScope.$emit(_service.getUpdatedSignal(collection));
}
function _afterUpdate(collection, selected) {
_persistCollection(collection, selected);
_broadcastUpdate(collection);
}
function _getCollection(collection) {
if (!_.has(_collections, collection)) {
var data = _loadCollection(collection);
// Holds reference to a shared array. Only mutate, don't replace it.
_collections[collection] = data !== null ? data : [];
}
return _collections[collection];
}
function _add(item, path, collection) {
// Add `item` to `collection` where item will be identified by `path`.
// For example, path could be 'id', 'row_id', 'data.athlete_id',
// whatever fits the row data being added.
var selected = _getCollection(collection);
if (!_.any(selected, path, _.get(item, path))) {
selected.push(item);
}
_afterUpdate(collection, selected);
}
function _remove(item, path, collection) {
// Remove `item` from `collection`, where item is identified by `path`,
// just like in _add().
var selected = _getCollection(collection);
_.remove(selected, path, _.get(item, path));
_afterUpdate(collection, selected);
}
function _getUpdatedSignal(collection) {
return 'selectionService:updated:' + collection;
}
function _updateInGridSelections(gridApi, path, collection) {
var selectedInGrid = gridApi.getSelectedNodes(),
currentlySelected = _getCollection(collection),
gridPath = 'data.' + path;
_.each(selectedInGrid, function (node) {
if (!_.any(currentlySelected, path, _.get(node, gridPath))) {
// The following suppressEvents=true flag is ignored for now, but a
// fixing pull request is waiting at ag-grid GitHub.
gridApi.deselectNode(node, true);
}
});
var selectedIdsInGrid = _.pluck(selectedInGrid, gridPath),
currentlySelectedIds = _.pluck(currentlySelected, path),
missingIdsInGrid = _.difference(currentlySelectedIds, selectedIdsInGrid);
if (missingIdsInGrid.length > 0) {
// We're trying to avoid the following loop, since it seems horrible to
// have to loop through all the nodes only to select some. I wish there
// was a way to select nodes/rows based on an id.
var i;
gridApi.forEachNode(function (node) {
i = _.indexOf(missingIdsInGrid, _.get(node, gridPath));
if (i >= 0) {
// multi=true, suppressEvents=true:
gridApi.selectNode(node, true, true);
missingIdsInGrid.splice(i, 1); // Reduce haystack.
if (!missingIdsInGrid.length) {
// I'd love for `forEachNode` to support breaking the loop here.
}
}
});
}
}
var _service = {
getCollection: _getCollection,
add: _add,
remove: _remove,
getUpdatedSignal: _getUpdatedSignal,
updateInGridSelections: _updateInGridSelections
};
return _service;
});
The selectionService service allows adding and removing arbitrary objects to separate collections, identified by collection, a name you find suitable. This way the same service can be used for remembering selections in multiple ag-grid instances. Each object will be identified using a path parameter. The path is used to retrieve the unique identifier using lodash's get function.
Furthermore, the service uses sessionStorage to persist the selections during the user's whole tab/browser session. This might be overkill; we could have just relied on the service to keep track of the selections since it will only get instantiated once. This can of course be modified to your needs.
Then there were the changes that had to be done to the GridController. First of all the columnDefs entry for the first column had to be changed slightly
var columnDefs = [
{
headerName: "#",
width: 60,
field: 'id', // <-- Now we use a generated row ID.
checkboxSelection: true,
suppressSorting: true,
suppressMenu: true
}, …
where the new, generated row ID is generated once the data has been retrieved from the remote server
// Add row ids.
for (var i = 0; i < allOfTheData.length; i++) {
var item = allOfTheData[i];
item.id = 'm' + i;
}
(The 'm' in the ID was included just to make sure I didn't confused that ID with other IDs used by ag-grid.)
Next, the necessary changes to gridOptions were to add
{
…,
onRowSelected: rowSelected,
onRowDeselected: rowDeselected,
onBeforeFilterChanged: clearSelections,
onBeforeSortChanged: clearSelections,
…
}
Were the different handlers are quite straight forward, communicating with the selectionService
function rowSelected(event) {
selectionService.add(event.node.data, 'id', 'page-1');
}
function rowDeselected(event) {
selectionService.remove(event.node.data, 'id', 'page-1');
}
function clearSelections(event) {
$scope.gridOptions.api.deselectAll();
}
Now, the GridController needs to handle updates signalled by the selectionService too
$scope.$on('$destroy',
$rootScope.$on(selectionService.getUpdatedSignal('page-1'),
updateSelections));
and
function updateSelections() {
selectionService.updateInGridSelections($scope.gridOptions.api, 'id', 'page-1');
}
calls selectionService.updateInGridSelections which will update the in-grid selections of the grid in question. That was the most cumbersome function to write. For example, if a selection has been added externally (outside the grid), then we'll have to perform a forEachNode run, even if we know all the necessary nodes have already been selected in-grid; there's no way to exit that loop early.
Finally, another crux was to clear and reapply the selections before and after, respectively, when the filters or sort orders are changed, or when new data is retrieved from the server (which is only simulated in the demo). The solution was to include a call to updateSelections after the params.successCallback inside the getRows handler
params.successCallback(rowsThisPage, lastRow);
updateSelections();
Now, the most puzzling findings during the implementation of this solution was that the ag-grid API grid options onAfterFilterChanged and onAfterSortChanged couldn't be used for reapplying the selections because they trigger before the (remote) data has finished loading.
I'm quite new to backbone so there could be a really simple solution to this problem. I have an app where you can view a show page of which there is a table I'm adding pagination to. I have created a utility object Table to handle the pagination so it can be used on every table on each show page:
var Table = function(rowsStart, increment, data) {
this.rowsStart = rowsStart;
this.increment = increment;
this.data = data;
this.totalRows = _.size(data);
this.totalRowsRoundUp = Math.ceil(_.size(data)/10)*10;
this.paginate = function(paginateVol) {
// Scope the figures
this.rowsStart += paginateVol
this.increment += paginateVol
rS = this.rowsStart;
inc = this.increment;
// Show first increment results
var rowsToDisplay = [];
$.each(this.data, function(i,e){
if (i < inc && i >= rS) {
rowsToDisplay.push(e)
}
});
// Send back the rows to display
return rowsToDisplay
};
}
This works fine when visiting the first show page table in the backbone history but when I visit further show pages and action this pagination object it triggers on all visited table views and produces weird results on my current view.
My View look like this:
// Create a view for the outer shell of our table - not inclusive of rows
queriesToolApp.ReportKeywordsView = Backbone.View.extend({
el: '#report-queries-js',
events: {
'click #prev' : 'clickBack',
'click #next' : 'clickNext'
},
initialize: function() {
// compile our template
this.template = _.template($('#tpl_indiv_report').html());
// create an instance of the Table paginator object
this.paginator = new Table(0, 10, this.collection.attributes);
},
render: function(paginateVol) {
// Scope this
_this = this;
var data = _this.collection.attributes;
// Render the script template
this.$el.html(this.template());
// Select the table body to append
var tableBody = $('#report-queries-row');
// Store the keyword object and the pagination amount
var keywordObj = this.paginator.paginate(paginateVol);
// Append keyword data to page
$.each(keywordObj, function(index, keyword){
// Create a new instance of the individual view
var reportKeywordIndView = new queriesToolApp.ReportKeywordIndView({model: keyword})
// append this to the table
tableBody.append(reportKeywordIndView.render().el);
// start table listen when last row appended
});
},
clickBack: function() {
// Render the view passing in true as it's going back
this.render(-10);
},
clickNext: function() {
// Render the view passing in nothing as default is forward
this.render(10);
}
})
Here is the individual view:
queriesToolApp.ReportKeywordIndView = Backbone.View.extend({
tagName: 'tr',
className: 'table-row',
initialize: function() {
// Define the template and compile the template in the view
this.template = _.template($('#tpl_indiv_report_row').html());
},
render: function() {
// Set the view with the data provided from the model
this.$el.html(this.template(this.model));
return this;
}
})
And I start backbone here:
$(function() {
// create a new instance of the router
var router = new queriesToolApp.AppRouter();
// Record the history of the url hash fragments
Backbone.history.start();
});
Is there a way around this?
I think to make it work keeping the objects you defined, I'd be helpful to see the AppRouter code. One thing seems certian: the ReportKeywordIndView instances keep getting created but probably not being deleted. This could work a different way, without that extra view.
As its written here, the row view looks too simple to be a backbone view. You could probably fix this by changing the template to include the TR tag, remove the view and make some adjustments in the main view.
This can go in the initialize function:
this.rowTemplate = _.template($('#tpl_indiv_report_row').html());
This can replace the each code which creates the row view and adds it:
// append this to the table
tableBody.append(_this.rowTemplate(keyword));
I have a div that is setup to bind to a observeableArray ,but I only want to show at most 50 items from that observeableArray at any given time. I want to handle this with pagination with a previous and next button along with indices on the page to allow users to cycle through pages of items from the collection. I know I could probably do this with a computedObservable and a custom data binding but I'm not sure how to do it (I'm still a Knockout neophyte). Can anyone point me in the right direction?
Here is my code (the JS is in TypeScript):
<div class="container-fluid">
<div class="row-fluid">
<div class="span12">
<%=
if params[:q]
render 'active_search.html.erb'
else
render 'passive_search.html.erb'
end
%>
<%= form_tag("/search", method: "get", :class => "form-search form-inline") do %>
<%= label_tag(:q, "Search for:") %>
<%= text_field_tag(:q, nil, class:"input-medium search-query") %>
<%= submit_tag("Search", :class=>"btn") %>
<% end %>
<div class="media" data-bind="foreach: tweetsArray">
<%= image_tag('twitter-icon.svg', :class=>"tweet_img", :style=>"display:inline;") %>
<div class="media-body" style="display:inline;">
<h4 class="media-heading" data-bind="text: user.screen_name" style="display:inline;"></h4>
<span data-bind="text:text" style="display:inline;"></span> <br />
<span data-bind="text:'Created at '+created_at"></span> <br />
</div>
</div>
<div class="pagination pagination-centered">
<ul>
<li>
Prev
</li>
<li>
1
</li>
<li>
Next
</li>
</ul>
</div>
</div>
</div>
</div>
<script>
var viewModel = new twitterResearch.TweetViewModel();
ko.applyBindings(viewModel);
//TODO: notes to self, use custom binding for pagination along with a computed observable to determine where at in the list you are
//document.onReady callback function
$(function() {
$.getJSON('twitter', {}, function(data) {
viewModel.pushTweet(data);
console.log(data.user);
});
});
</script>
declare var $: any;
declare var ko: any;
module twitterResearch {
class Tweet {
text: string;
created_at: string;
coordinates: string;
user: string;
entities: string;
id: number;
id_str: string;
constructor(_text: string, _created_at: string, _coordinates: any, _user: any,
_entities: any, _id_str: string, _id: number){
this.text = _text;
this.created_at = _created_at;
this.coordinates = _coordinates;
this.user = _user;
this.entities = _entities;
this.id_str = _id_str;
this.id = _id;
}
}
export class TweetViewModel{
tweetsArray: any;
constructor()
{
this.tweetsArray = ko.observableArray([]);
}
//tweet is going to be the JSON tweet we return
//from the server
pushTweet(tweet)
{
var _tweet = new Tweet(tweet.text, tweet.created_at, tweet.coordinates,
tweet.user, tweet.entities, tweet.id_str, tweet.id);
this.tweetsArray.push(_tweet);
this.tweetsArray.valueHasMutated();
}
}
}
Pagination is quite simple with Knockout. I would personally achieve it this way:
Have an observableArray containing all your elements
Have an observable containing the current page (initialized to 0)
Have a variable declaring the number of elements per page
Have a computed that returns the number of pages, calculated thanks to the number of elements per page and the total number of elements.
Finally, add a computed that slices the array containing all the elements.
Given that, you can now add a function that increments (next) or decrements (previous) the current page.
Here is a quick example:
var Model = function() {
var self = this;
this.all = ko.observableArray([]);
this.pageNumber = ko.observable(0);
this.nbPerPage = 25;
this.totalPages = ko.computed(function() {
var div = Math.floor(self.all().length / self.nbPerPage);
div += self.all().length % self.nbPerPage > 0 ? 1 : 0;
return div - 1;
});
this.paginated = ko.computed(function() {
var first = self.pageNumber() * self.nbPerPage;
return self.all.slice(first, first + self.nbPerPage);
});
this.hasPrevious = ko.computed(function() {
return self.pageNumber() !== 0;
});
this.hasNext = ko.computed(function() {
return self.pageNumber() !== self.totalPages();
});
this.next = function() {
if(self.pageNumber() < self.totalPages()) {
self.pageNumber(self.pageNumber() + 1);
}
}
this.previous = function() {
if(self.pageNumber() != 0) {
self.pageNumber(self.pageNumber() - 1);
}
}
}
You'll find a simple and complete example here: http://jsfiddle.net/LAbCv/ (might be a bit buggy, but the idea is there).
Actually I am working on a website, which has a lot of tables (most of them need paging).So actually, I needed some reusable-component for paging to use it in all the cases which I need paging.
Also, I needed more advanced features than which provided in the accepted answer to this question.
So I developed my own component to solving this issue, here it is.
Now on Github
JsFiddle
And for more details, continue reading (Please consider to take the code from GitHub, not from here, as the GitHub code was updated and enhanced since I put it here)
JavaScript
function PagingVM(options) {
var self = this;
self.PageSize = ko.observable(options.pageSize);
self.CurrentPage = ko.observable(1);
self.TotalCount = ko.observable(options.totalCount);
self.PageCount = ko.pureComputed(function () {
return Math.ceil(self.TotalCount() / self.PageSize());
});
self.SetCurrentPage = function (page) {
if (page < self.FirstPage)
page = self.FirstPage;
if (page > self.LastPage())
page = self.LastPage();
self.CurrentPage(page);
};
self.FirstPage = 1;
self.LastPage = ko.pureComputed(function () {
return self.PageCount();
});
self.NextPage = ko.pureComputed(function () {
var next = self.CurrentPage() + 1;
if (next > self.LastPage())
return null;
return next;
});
self.PreviousPage = ko.pureComputed(function () {
var previous = self.CurrentPage() - 1;
if (previous < self.FirstPage)
return null;
return previous;
});
self.NeedPaging = ko.pureComputed(function () {
return self.PageCount() > 1;
});
self.NextPageActive = ko.pureComputed(function () {
return self.NextPage() != null;
});
self.PreviousPageActive = ko.pureComputed(function () {
return self.PreviousPage() != null;
});
self.LastPageActive = ko.pureComputed(function () {
return (self.LastPage() != self.CurrentPage());
});
self.FirstPageActive = ko.pureComputed(function () {
return (self.FirstPage != self.CurrentPage());
});
// this should be odd number always
var maxPageCount = 7;
self.generateAllPages = function () {
var pages = [];
for (var i = self.FirstPage; i <= self.LastPage() ; i++)
pages.push(i);
return pages;
};
self.generateMaxPage = function () {
var current = self.CurrentPage();
var pageCount = self.PageCount();
var first = self.FirstPage;
var upperLimit = current + parseInt((maxPageCount - 1) / 2);
var downLimit = current - parseInt((maxPageCount - 1) / 2);
while (upperLimit > pageCount) {
upperLimit--;
if (downLimit > first)
downLimit--;
}
while (downLimit < first) {
downLimit++;
if (upperLimit < pageCount)
upperLimit++;
}
var pages = [];
for (var i = downLimit; i <= upperLimit; i++) {
pages.push(i);
}
return pages;
};
self.GetPages = ko.pureComputed(function () {
self.CurrentPage();
self.TotalCount();
if (self.PageCount() <= maxPageCount) {
return ko.observableArray(self.generateAllPages());
} else {
return ko.observableArray(self.generateMaxPage());
}
});
self.Update = function (e) {
self.TotalCount(e.TotalCount);
self.PageSize(e.PageSize);
self.SetCurrentPage(e.CurrentPage);
};
self.GoToPage = function (page) {
if (page >= self.FirstPage && page <= self.LastPage())
self.SetCurrentPage(page);
}
self.GoToFirst = function () {
self.SetCurrentPage(self.FirstPage);
};
self.GoToPrevious = function () {
var previous = self.PreviousPage();
if (previous != null)
self.SetCurrentPage(previous);
};
self.GoToNext = function () {
var next = self.NextPage();
if (next != null)
self.SetCurrentPage(next);
};
self.GoToLast = function () {
self.SetCurrentPage(self.LastPage());
};
}
HTML
<ul data-bind="visible: NeedPaging" class="pagination pagination-sm">
<li data-bind="css: { disabled: !FirstPageActive() }">
<a data-bind="click: GoToFirst">First</a>
</li>
<li data-bind="css: { disabled: !PreviousPageActive() }">
<a data-bind="click: GoToPrevious">Previous</a>
</li>
<!-- ko foreach: GetPages() -->
<li data-bind="css: { active: $parent.CurrentPage() === $data }">
<a data-bind="click: $parent.GoToPage, text: $data"></a>
</li>
<!-- /ko -->
<li data-bind="css: { disabled: !NextPageActive() }">
<a data-bind="click: GoToNext">Next</a>
</li>
<li data-bind="css: { disabled: !LastPageActive() }">
<a data-bind="click: GoToLast">Last</a>
</li>
</ul>
Features
Show on need When there is no need for paging at all (for example the items which need to display less than the page size) then the HTML component will disappear.
This will be established by statementdata-bind="visible: NeedPaging".
Disable on need
for example, if you are already selected the last page, why the last page or the Next button should be available to press?
I am handling this and in that case I am disabling those buttons by applying the following binding data-bind="css: { disabled: !PreviousPageActive() }"
Distinguish the Selected page
a special class (in this case called active class) is applied on the selected page, to make the user know in which page he/she is right now. This is established by the binding data-bind="css: { active: $parent.CurrentPage() === $data }"
Last & First
going to the first and last page is also available by simple buttons dedicated to this.
Limits for displayed buttons
suppose you have a lot of pages, for example, 1000 pages, then what will happen? would you display them all for the user? absolutely not you have to display just a few of them according to the current page. for example, showing 3 pages before and other 3 pages after the selected page.
This case has been handled here <!-- ko foreach: GetPages() -->
the GetPages function applying a simple algorithm to determine if we need to show all the pages (the page count is under the threshold, which could be determined easily), or to show just some of the buttons.
you can determine the threshold by changing the value of the maxPageCount variable
Right now I assigned it as the following var maxPageCount = 7; which mean that no more than 7 buttons could be displayed for the user (3 before the SelectedPage, and 3 after the Selected Page) and the Selected Page itself.
You may wonder, what if there were not enough pages after OR before the current page to display? do not worry I am handling this in the algorithm for example, if you have 11 pages and you have maxPageCount = 7 and the current selected page is 10, Then the following pages will be shown
5,6,7,8,9,10(selected page),11
so we always stratifying the maxPageCount, in the previous example showing 5 pages before the selected page and just 1 page after the selected page.
Selected Page Validation
All set operation for the CurrentPage observable which determine the selected page by the user, is going through the function SetCurrentPage. In only this function we set this observable, and as you can see from the code, before setting the value we make validation operations to make sure that we will not go beyond the available page of the pages.
Already clean
I use only pureComputed not computed properties, which means you do not need to bother yourself with cleaning and disposing of those properties. Although, as you will see in the example below, you need to dispose of some other subscriptions which are outside of the component itself
NOTE 1
You may notice that I am using some bootstrap classes in this component,
This is suitable for me, but , of course, you can use your own classes instead of the bootstrap classes.
The bootstrap classes which I used here are pagination, pagination-sm, active and disabled
Feel free to change them as you need.
NOTE 2
So I introduced the component for you, It is time to see how it could work.
You would integrate this component into your main ViewModel as like this.
function MainVM() {
var self = this;
self.PagingComponent = ko.observable(new Paging({
pageSize: 10, // how many items you would show in one page
totalCount: 100, // how many ALL the items do you have.
}));
self.currentPageSubscription = self.PagingComponent().CurrentPage.subscribe(function (newPage) {
// here is the code which will be executed when the user changes the page.
// you can handle this in the way you need.
// for example, in my case, I am requesting the data from the server again by making an ajax request
// and then updating the component
var data = /*bring data from server , for example*/
self.PagingComponent().Update({
// we need to set this again, why? because we could apply some other search criteria in the bringing data from the server,
// so the total count of all the items could change, and this will affect the paging
TotalCount: data.TotalCount,
// in most cases we will not change the PageSize after we bring data from the server
// but the component allows us to do that.
PageSize: self.PagingComponent().PageSize(),
// use this statement for now as it is, or you have to made some modifications on the 'Update' function.
CurrentPage: self.PagingComponent().CurrentPage(),
});
});
self.dispose = function () {
// you need to dispose the manual created subscription, you have created before.
self.currentPageSubscription.dispose();
}
}
Last but not least, Sure do not forget to change the binding in the HTML component according to your special viewModel, or wrap all the component with the with binding like this
<div data-bind="with: PagingComponent()">
<!-- put the component here -->
</div>
Cheers
I have created a blogpost with detailed explanation on how to create pagination with the help of a little JQuery plugin (here).
Basically, I have used normal knockout data binding with AJAX and after data has been retrieved from the server, I call the plugin. You can find the plugin here. It's called Simple Pagination.
This question is still one of the top searches for "knockout pagination", so the knockout extension knockout-paging (git) is worth mentioning.
It provides pagination by extending ko.observableArray. It is well documented and easy to use.
The usage example is here.