knockout - Generate Bootstrap Styling dynamically - javascript

I am using Knockout's forech data binding to render a template. The issue is that for every three items generted using foreach binding, I want to create a new div with class row. Essentially , I want only three items to be displayed in one row. For the fourth item, noew row should be created. But the foreach data binding has been applied to the div inside the row div. How do I achieve that? Following is the code.
HTML
<div class="row">
<!-- Item #1 -->
<div class="col-md-4 col-sm-6 col-xs-12" data-bind="foreach:items">
<div data-bind="attr: {id: ID}" class="item">
<!-- Use the below link to put HOT icon -->
<div class="item-icon"><span>HOT</span></div>
<!-- Item image -->
<div class="item-image">
<img data-bind="attr: {src: picture}" src="img/items/2.png" alt="" class="img-responsive"/>
</div>
<!-- Item details -->
<div class="item-details">
<!-- Name -->
<h5><a data-bind="text: itemname" href="single-item.html">HTC One V</a></h5>
<div class="clearfix"></div>
<!-- Para. Note more than 2 lines. -->
<!--p>Something about the product goes here. Not More than 2 lines.</p-->
<hr />
<!-- Price -->
<div data-bind="text: price" class="item-price pull-left">$360</div>
<!-- qty -->
<div data-bind="text: quantity" class="item-price text-center">$360</div>
<!-- Add to cart -->
<div class="pull-right">Add to Cart</div>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
Javascript:
function itemsKo()
{
var self=this;
self.query = ko.observable();
self.hide = ko.observable(false);
self.items = ko.observableArray();
self.subcat=function()
{
$.ajax({
url: "/items,
type: "get",
success: function(data){
ko.utils.arrayForEach(data, function(item) {
item.price = "Rs" + item.price;
self.items.push(item);
});
//console.log(JSON.stringify(window.vm.items()));
},
error:function(jqXHR, textStatus, errorThrown) {
alert("failure");
}
});
}
}

The easiest solution is to find a way to map your array into a structure that is rows/columns. So, an array of rows, where each row is an array of items in that row.
Here is an older answer that shows creating a computed in the VM to represent an array as a set of rows: Knockout.js - Dynamic columns but limit to a maximum of 5 for each row
Another option could be to create a custom binding that handles the plumbing of this computed for you. The advantage is that you do not need to bloat your view model with extra code and it is reusable. A possible implementation might look like:
ko.bindingHandlers.rows = {
init: function (element, valueAccessor, allBindings, data, context) {
var rows = ko.computed({
read: function() {
var index, length, row,
options = ko.unwrap(valueAccessor()),
items = ko.unwrap(options.items),
columns = ko.unwrap(options.columns)
result = [];
for (index = 0, length = items.length; index < length; index++) {
if (index % columns === 0) {
//push the previous row, except the first time
if (row) {
result.push(row);
}
//create an empty new row
row = [];
}
//add this item to the row
row.push(items[index]);
}
//push the final row
if (row) {
result.push(row);
}
//we know have an array of rows
return result;
},
disposeWhenNodeIsRemoved: element
});
//apply the real foreach binding with our rows computed
ko.applyBindingAccessorsToNode(element, { foreach: function() { return rows; } }, context);
//tell KO that we will handle binding the children
return { controlsDescendantBindings: true };
}
};
Here is a quick fiddle with it in action: http://jsfiddle.net/rniemeyer/nh6d7/
It is a computed, so the number of columns and the items can be observable and will cause it to re-render on changes. This could be a slight concern, if you are often updating the original items.

Related

AngularJS ng-repeat is slow

It is not like it is slow on rendering many entries. The problem is that whenever the $scope.data got updated, it adds the new item first at the end of the element, then reduce it as it match the new $scope.data.
For example:
<div class="list" ng-repeat="entry in data">
<h3>{{entry.title}}</h3>
</div>
This script is updating the $scope.data:
$scope.load = function() {
$scope.data = getDataFromDB();
}
Lets say I have 5 entries inside $scope.data. The entries are:
[
{
id: 1,
title: 1
},
{
id: 2,
title: 2
},
......
]
When the $scope.data already has those entries then got reloaded ($scope.data = getDataFromDB(); being called), the DOM element for about 0.1s - 0.2s has 10 elements (duplicate elements), then after 0.1s - 0.2s it is reduced to 5.
So the problem is that there is delay about 0.1s - 0.2s when updating the ng-repeat DOM. This looks really bad when I implement live search. Whenever it updates from the database, the ng-repeat DOM element got added up every time for a brief millisecond.
How can I make the rendering instant?
EDITED
I will paste all my code here:
The controller:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = [];
$scope.loadViewModels($scope.orderBy, table);
}
$scope.loadViewModels = function (orderBy, table, cb) {
if (!$scope.endOfPage) {
let searchKey = $scope.page.searchString;
let skip = ($scope.currentPage - 1) * $scope.itemsPerPage;
let searchClause = '';
if (searchKey && searchKey.length > 0) {
let searchArr = [];
$($scope.vmKeys).each((i, key) => {
searchArr.push(key + ` LIKE '%` + searchKey + `%'`);
});
searchClause = `WHERE ` + searchArr.join(' OR ');
}
let sc = `SELECT * FROM ` + table + ` ` + searchClause + ` ` + orderBy +
` LIMIT ` + skip + `, ` + $scope.itemsPerPage;
sqlite.query(sc, rows => {
$scope.$apply(function () {
var data = [];
let loadedCount = 0;
if (rows != null) {
$scope.currentPage += 1;
loadedCount = rows.length;
if (rows.length < $scope.itemsPerPage)
$scope.endOfPage = true
for (var i = 0; i < rows.length; i++) {
let item = rows.item(i);
let returnObject = {};
$($scope.vmKeys).each((i, key) => {
returnObject[key] = item[key];
});
data.push(returnObject);
}
$scope.viewModels = $scope.viewModels.concat(data);
}
else
$scope.endOfPage = true;
if (cb)
cb(loadedCount);
})
});
}
}
The view:
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
<div class="row note-list" ng-if="showList">
<h3>Notes</h3>
<input ng-model="page.searchString" id="search"
ng-keyup="search('notes')" type="text" class="form-control"
placeholder="Search Notes" style="margin-bottom:10px">
<div class="col-12 note-list-item"
ng-repeat="data in viewModels track by data.id"
ng-click="edit(data.id)"
ontouchstart="touchStart()" ontouchend="touchEnd()"
ontouchmove="touchMove()">
<p ng-class="deleteMode ? 'note-list-title w-80' : 'note-list-title'"
ng-bind-html="data.title"></p>
<p ng-class="deleteMode ? 'note-list-date w-80' : 'note-list-date'">{{data.dateCreated | displayDate}}</p>
<div ng-if="deleteMode" class="note-list-delete ease-in" ng-click="delete($event, data.id)">
<span class="btn fa fa-trash"></span>
</div>
</div>
<div ng-if="!deleteMode" ng-click="new()" class="add-btn btn btn-primary ease-in">
<span class="fa fa-plus"></span>
</div>
</div>
<div ng-if="!showList" class="ease-in">
<div>
<div ng-click="back()" class="btn btn-primary"><span class="fa fa-arrow-left"></span></div>
<div ng-disabled="!isDataChanged" ng-click="save()" class="btn btn-primary" style="float:right">
<span class="fa fa-check"></span>
</div>
</div>
<div contenteditable="true" class="note-title"
ng-bind-html="selected.title" id="title">
</div>
<div contenteditable="true" class="note-container" ng-bind-html="selected.note" id="note"></div>
</div>
</div>
<script src="../js/pages/note.js"></script>
Calling it from:
$scope.loadViewModels($scope.orderBy, 'notes');
The sqlite query:
query: function (query, cb) {
db.transaction(function (tx) {
tx.executeSql(query, [], function (tx, res) {
return cb(res.rows, null);
});
}, function (error) {
return cb(null, error.message);
}, function () {
//console.log('query ok');
});
},
It is apache cordova framework, so it uses webview in Android emulator.
My Code Structure
<html ng-app="app" ng-controller="pageController">
<head>....</head>
<body>
....
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
....
</div>
</body>
</html>
So there is controller inside controller. The parent is pageController and the child is noteController. Is a structure like this slowing the ng-repeat directives?
Btw using track by is not helping. There is still delay when rendering it. Also I can modify the entries as well, so when an entry was updated, it should be updated in the list as well.
NOTE
After thorough investigation there is something weird. Usually ng-repeat item has hash key in it. In my case ng-repeat items do not have it. Is it the cause of the problem?
One approach to improve performance is to use the track by clause in the ng-repeat expression:
<div class="list" ng-repeat="entry in data track by entry.id">
<h3>{{entry.title}}</h3>
</div>
From the Docs:
Best Practice: If you are working with objects that have a unique identifier property, you should track by this identifier instead of the object instance, e.g. item in items track by item.id. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones. For large collections, this significantly improves rendering performance.
For more information, see
AngularJS ngRepeat API Reference -- Tracking and Duplicates
In your html, try this:
<div class="list" ng-repeat="entry in data">
<h3 ng-bind="entry.title"></h3>
</div>
After thorough research, I found my problem. Every time I reset / reload my $scope.viewModels I always assign it to null / empty array first. This what causes the render delay.
Example:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = []; <------ THIS
$scope.loadViewModels($scope.orderBy, table);
}
So instead of assigning it to null / empty array, I just replace it with the new loaded data, and the flickering is gone.

How to bind dynamic checkbox value to Knockout observableArray on an object?

I've posted my fiddle here, that has comments with it.
How can I convert/map the AllCustomers array into another array of Customer objects??
I need to push the checked checkboxes objects in to self.Customer(), {CustomerType,checked}
Then I would loop through list of Customer object Array and return an array of all checked Customers - self.CheckedCustomers
function Customer(type, checked)
{
var self = this;
self.CustomerType = ko.observable(type);
self.IsChecked = ko.observable(checked || false);
}
function VM()
{
var self = this;
//dynamically populated - this is for testing puposes
self.AllCustomers = ko.observableArray([
{
code: "001",
name:'Customer 1'
},
{
code: "002",
name:'Customer 2'
},
{
code: "003",
name:'Customer 3'
},
]);
self.selectedCustomers = ko.observableArray([]);
self.Customer = ko.observableArray([]);
//How can I convert the AllCustomers array into an array of Customer object??
//I need to push the checked object in to self.Customer(), {CustomerType,checked}
//uncomment below - just for testing looping through Customer objects
/*
self.Customer = ko.observableArray([
new Customer("001"),
new Customer("002")
]);
*/
// This array should return all customers that checked the box
self.CheckedCustomers = ko.computed(function()
{
var selectedCustomers = [];
ko.utils.arrayForEach(self.Customer(), function (customer) {
if(customer.IsChecked())
selectedCustomers.push(customer);
});
return selectedCustomers;
});
}
ko.applyBindings(new VM());
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<!-- ko foreach: AllCustomers -->
<input type="checkbox" data-bind="value: $data.code, checked:$parent.selectedCustomers" />
<span data-bind="text: $data.name"></span>
<!-- /ko -->
<br />
<h4>selectedCustomers code</h4>
<div data-bind="foreach: selectedCustomers">
<span data-bind="text: $data"></span>
</div>
<h4>Checked boxes are??</h4>
<div data-bind="foreach: CheckedCustomers">
<span data-bind="text: CustomerType"></span>
<span data-bind="text: IsChecked"></span>
<span>,</span>
</div>
<!-- Use this to loop through list of Customer object Array, uncomment below to test-->
<!--
<!-- ko foreach: Customer --
<input type="checkbox" data-bind="checked: IsChecked" />
<span data-bind="text: CustomerType"></span>
<!-- /ko --
-->
You're trying to convert object with properties code and name to an object of properties CustomerType and IsChecked. I'm assuming you want code to be mapped to CustomerType when creating new Customer object.
Here's a working jsfiddle for more or less what you wanted.
https://jsfiddle.net/s9yd0e7o/10/
Added the following code:
self.selectedCustomers.subscribe(function() {
self.Customer.removeAll();
ko.utils.arrayForEach(self.selectedCustomers(), function(item) {
self.Customer.push(new Customer(item, true));
});
});

Loops in Handlebars

I have an array of tracks coming from a database that I want to display in a div.
More specifically I need to put every two of them in a bootstrap row. I can easily do it in the controller JS file by first collecting all in a fragment element and then use a loop to put them into the rows and then in the target div but I am wondering if it would be possible to do it directly while producing them in handlebars?
Here is the handlebars template:
{{#if result}}
{{#each result}}
<div class="playlist-item col-xs-4">
<a href="#/user/{{username}}/playlist/{{id}}" class="no-style">
<h3 class="result-title">{{title}}</h3>
<p class="result-description">{{description}}</p>
<img class="result-image img-circle" src="{{img}}">
</a>
<br>
<a type="button" id="{{id.videoId}}" class="btn btn-default btn-remove"
href="#/user/{{username}}/playlist/{{id}}/remove-from-playlist">Remove from
playlist</a>
</div>
{{/each}}
{{else}}
<h4>You currently have no tracks in your playlist</h4>
{{/if}}
Here is the JS:
showPlaylist() {
return Promise.all([
userController.loadPlaylist(),
templates.loadTemplate('playlist'),
])
.then(([tracks, template]) => {
let fragment = document.createDocumentFragment()
let div = document.createElement('DIV');
div.innerHTML = template(tracks);
div = [...div.children];
let len = div.length
while(div.length > 0) {
let row = document.createElement('div')
row.className = 'row'
let col = div.splice(0,2)
row.append(col[0])
if(col[1]) {
row.append(col[1])
}
len -= 2;
fragment.append(row)
}
$('#container').html(fragment)
})
}
It is possible to group your items into rows, but you would need to use a custom helper function to do it.
We will need to create a block helper that takes an array of items, breaks the array into rows of specified number of columns, and then applies the block "row" template to each row. If we were to call our block helper "eachRow", the resulting template might look like the following:
{{#eachRow result 2}}
<div class="row">
{{#each columns}}
<div class="playlist-item col-xs-4">
{{!-- TODO: Rest of item template goes here. --}}
</div>
{{/each}}
</div>
{{/eachRow}}
Notice that we still use the item template within a regular Handlebars #each block. Except now the #each is wrapped within a "row" template block. The 2 is a parameter that will be passed to our helper that is to be the number of columns in each row.
Next, we will write our helper:
Handlebars.registerHelper('eachRow', function (items, numColumns, options) {
var result = '';
for (var i = 0; i < items.length; i += numColumns) {
result += options.fn({
columns: items.slice(i, i + numColumns)
});
}
return result;
});
This helper simply iterates over our source array in increments of numColumns and for each iteration applies our "row" block template, passing the array of items (columns) that are to render in that row. The helper concatenates the rows and returns the result.
I have created a fiddle for reference.

Knockout paging binding

Sorry if this is a really basic question but I'm in the process of learning Knockout and trying to wire up paging to my dataset.
In my code below, you will see that I am retrieving the dataset, and the page size dropdown affects the results appropriately. And when I change the page number (#'d links in footer of table), nothing happens. Could someone tell me what I'm missing?
function ViewModel(){
var vm = this;
// variables
vm.drinks = ko.observableArray();
vm.pageSizes = [15,25,35,50];
vm.pageSize = ko.observable(pageSizes[0]);
vm.currentPage = ko.observable(0);
// computed variables
// returns number of pages required for number of results selected
vm.PageCount = ko.computed(function(){
if(vm.pageSize()){
return Math.ceil(vm.drinks().length / vm.pageSize());
}else{
return 1;
}
});
// returns items from the array for the current page
vm.PagedResults = ko.computed(function(){
//return vm.drinks().slice(vm.currentPage, vm.pageSize());
return vm.drinks().slice(vm.currentPage() * vm.pageSize(), (vm.currentPage() * vm.pageSize()) + vm.pageSize());
});
// returns a list of numbers for all pages
vm.PageList = ko.computed(function(){
if(vm.PageCount() > 1){
return Array.apply(null, {length: vm.PageCount()}).map(Number.call, Number);
}
});
// methods
vm.ResetCurrentPage = function(){
vm.currentPage(0);
}
// go to page number
vm.GoToPage = function(page){
vm.currentPage(page);
}
// populate drink list
vm.GetDrinks = function(){
// get data
$(function () {
$.ajax({
type: "GET",
url: 'https://mysafeinfo.com/api/data?list=alcoholicbeverages&format=json',
dataType: "json",
success: function (data) {
vm.drinks(data);
}
});
});
}
// populate drinks
vm.GetDrinks();
}
// apply bindings
ko.applyBindings(ViewModel);
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="row">
<div class="col-sm-3 pull-right form-horizontal">
<label class="control-label col-sm-4">
Results:
</label>
<div class="col-sm-8">
<select data-bind="value: pageSize,
optionsCaption: 'Page Size',
options: pageSizes, event:{ change: ResetCurrentPage }"
class="form-control"></select>
</div>
</div>
</div>
<table class="table table-striped table-condensed">
<thead>
<tr>
<th style="width: 25%">Name</th>
<th>Category</th>
<th style="width: 50%">Description</th>
</tr>
</thead>
<tbody data-bind="foreach: PagedResults">
<tr>
<td data-bind="text: nm"></td>
<td data-bind="text: cat"></td>
<td data-bind="text: dsc"></td>
</tr>
</tbody>
<tfooter>
<tr>
<td colspan="3">
Current Page: <label data-bind="text: currentPage"></label><br />
<ul data-bind="foreach: PageList" class="pagination">
<li class="page-item"><a class="page-link" href="#" data-bind="text: $data + 1, click: GoToPage">1</a></li>
</ul>
</td>
</tr>
</tfooter>
</table>
Thanks to f_martinez for helping with my issue, here is the working example if anyone ends up here looking for how to do paging. jsfiddle
I will keep this open for now in case f_martinez would like to post an answer to accept.
function ViewModel() {
var vm = this;
// variables
vm.drinks = ko.observableArray();
vm.pageSizes = [15, 25, 35, 50];
vm.pageSize = ko.observable(pageSizes[0]);
vm.currentPage = ko.observable(0);
// computed variables
// returns number of pages required for number of results selected
vm.PageCount = ko.computed(function() {
if (vm.pageSize()) {
return Math.ceil(vm.drinks().length / vm.pageSize());
} else {
return 1;
}
});
// returns items from the array for the current page
vm.PagedResults = ko.computed(function() {
if (vm.PageCount() > 1) {
//return vm.drinks().slice(vm.currentPage, vm.pageSize());
return vm.drinks().slice(vm.currentPage() * vm.pageSize(), (vm.currentPage() * vm.pageSize()) + vm.pageSize());
} else {
return vm.drinks();
}
});
// returns a list of numbers for all pages
vm.PageList = ko.computed(function() {
if (vm.PageCount() > 1) {
return Array.apply(null, {
length: vm.PageCount()
}).map(Number.call, Number);
}
});
// methods
// reset to first page
vm.ResetCurrentPage = function() {
vm.currentPage(0);
}
// go to page number
vm.GoToPage = function(page) {
vm.currentPage(page);
}
// determines if page # is active returns active class
vm.GetClass = function(page) {
return (page == vm.currentPage()) ? "active" : "";
}
// populate drink list
vm.GetDrinks = function() {
// get data
$(function() {
$.ajax({
type: "GET",
url: 'https://mysafeinfo.com/api/data?list=alcoholicbeverages&format=json',
dataType: "json",
success: function(data) {
vm.drinks(data);
}
});
});
}
// populate drinks
vm.GetDrinks();
}
// apply bindings
ko.applyBindings(ViewModel);
.pagination > li > a:focus,
.pagination > li > a:hover,
.pagination > li > span:focus,
.page-link.active {
background-color: rgb(238, 238, 238);
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="row">
<div class="col-sm-3 pull-right form-horizontal">
<label class="control-label col-sm-4">
Results:
</label>
<div class="col-sm-8">
<select data-bind="value: pageSize,
optionsCaption: 'All Results',
options: pageSizes, event:{ change: ResetCurrentPage }" class="form-control"></select>
</div>
</div>
</div>
<table class="table table-striped table-condensed">
<thead>
<tr>
<th style="width: 25%">Name</th>
<th>Category</th>
<th style="width: 50%">Description</th>
</tr>
</thead>
<tbody data-bind="foreach: PagedResults">
<tr>
<td data-bind="text: nm"></td>
<td data-bind="text: cat"></td>
<td data-bind="text: dsc"></td>
</tr>
</tbody>
<tfooter>
<tr>
<td colspan="3" class="text-center">
<ul data-bind="foreach: PageList" class="pagination">
<li class="page-item">
</li>
</ul>
</td>
</tr>
</tfooter>
</table>
Stack overflow is about giving solution to common problems, and the answer solution is valid for OP ,but it is not very re-usable for other cases, Here is re-usable solution to this common problem (Knockout paging)
I am working on 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.
So I developed my own component of the this issue, here it is.
Now on Github
JsFiddle
And for more details, continue reading
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 statement data-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 happened? 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 the page page 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 was 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 go 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 those properties. Although ,as you will see in example below, you need to dispose some other subscriptions which are outside of the component itself
NOTE 1
You may noticed 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 in 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 change 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 allow 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 you special viewModel, or wrap all the component with the with binding like this
<div data-bind="with: PagingComponent()">
<!-- put the component here -->
</div>
Cheers

Placing a Container Around Every 'n' Items in a Knockout JS foreach Loop

I would like to construct the following HTML using a knockout JS foreach loop...
<div>
<div>
<article></article>
<article></article>
<article></article>
</div>
<div>
<article></article>
<article></article>
<article></article>
</div>
</div>
... where each 'article' is an item in the array.
I've tried the following, which seems logically sound, but it doesn't work - I'm assuming knockout is getting confused by the unbalanced tags inside the comments...
<div data-bind="foreach: articles()">
<!-- ko: if ($index() % 3 == 0)
<div>
<!-- /ko -->
<article></article>
<!-- ko: if ($index() % 3 == 2)
</div>
<!-- /ko -->
</div>
Any thoughts on how I can achieve this would be appreciated!
My take on this when tackling similar problems has always been that the view-model has to be structured as closely to the view as possible, so that you're not doing logic in the view itself. So therefore the place to group your array of articles is using a ko.computed in the viewmodel to build a structure like:
groupedArticles = [
[article1, article2, article3],
[article4, article5, article6]
]
then in your view you can do:
<!-- ko foreach: groupedArticles -->
<div>
<!-- ko foreach: $data -->
<article></article>
<!-- /ko -->
</div>
<!-- /ko -->
Let me know if that makes sense or not; if not I can try to add a fiddle to demo it.
Update
I found a fiddle which used this pattern. I needed to upgrade KO to the latest version to get it to work, you can now try it using this: http://jsfiddle.net/hFPgT/160/
This is from the question, How to get Knockout to group foreach
And the relevant code is:
this.grouped = ko.computed(function () {
var rows = [], current = [];
rows.push(current);
for (var i = 0; i < this.items.length; i += 1) {
current.push(this.items[i]);
if (((i + 1) % 4) === 0) {
current = [];
rows.push(current);
}
}
return rows;
}, this);
Like #sifriday, I would go for a separate computed array which I'd name articleGroups (or view). In my more recent understanding of Knockout, I found it most convenient to put all view-related logic (here: grouping) inside components' viewModels, so I built a component for it here.
The additional benefit is that you can pass parameters in the view; try the below snippet with different values for 'groupBy' for example.
// setup
var articles = [];
for (var i = 0; i < 50; i++)
articles.push({i: i+1, text: "text"});
// listview component
ko.components.register('article-view', {
viewModel: function(params) {
var groupBy = this.groupBy = ko.observable(params.groupBy);
this.articleGroups = ko.computed(function() {
var result = [], group = groupBy();
ko.utils.arrayForEach(ko.unwrap(params.data), function(item, index) {
if (index % group === 0)
result.push([item]);
else
result[result.length-1].push(item);
});
return result;
});
},
template: {element: 'article-group'}
});
// viewModel instantiation
VM = { articles: ko.observableArray(articles)};
ko.applyBindings(VM);
body>div>div>div { border-bottom: 1px solid gray; padding-bottom: 10px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script type="text/template" id="article-group">
<input type="number" data-bind="value: groupBy" min="1" max="50">
<div data-bind="foreach: articleGroups, as: 'group'">
<div data-bind="foreach: $data, event: {load: console.log($data)}">
<article data-bind="text: i"></article>
</div>
</div>
</script>
<div data-bind="component: {name: 'article-view', params: {groupBy: 5, data: articles}}"></div>

Categories