Skip to content

Instantly share code, notes, and snippets.

@kyeotic
Created September 26, 2013 16:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kyeotic/6716633 to your computer and use it in GitHub Desktop.
Save kyeotic/6716633 to your computer and use it in GitHub Desktop.
A grid for durandal
<table class="paging-container grid-table" data-bind="grid: gridConfig">
<tbody class="grid-body" data-part="body" data-bind="foreach: { data: currentPageRows, as: 'row' }" >
<tr class="grid-row" data-bind="css: { 'grid-row-odd': $index() % 2 == 1 }">
<td class="grid-column-details" data-bind="click: $root.showJobDetails"><img class="info-btn" src="/Content/images/locationMoreInfoIcon.png"/></td>
<td class="grid-column-isNew" data-bind="if: isNew"><img src="Content/images/newJobStarHH.png"/></td>
<td class="grid-column-startDate" data-bind="text: startDate().format('{MM}/{dd}/{yyyy}')"></td>
<td class="grid-column-category" data-bind="text: jobCategory"></td>
<td class="grid-column-term" data-bind="text: term"></td>
<td class="grid-column-shift" data-bind="text: shiftStart"></td>
<td class="grid-column-type" data-bind="text: jobType"></td>
<td class="grid-column-spec" data-bind="text: specialty"></td>
<td class="grid-column-site" data-bind="text: hospital"></td>
<td class="grid-column-unit" data-bind="text: unit"></td>
<td class="grid-column-loc" data-bind="text: location"></td>
<td class="grid-column-rate" data-bind="text: '$' + bestRate() + '/hr'"></td>
<td class="grid-column-status" data-bind="command: action">
<button class="btn" data-bind="css: status().toLowerCase() + '_btn'"></button>
</td>
</tr>
</tbody>
</table>
define(['durandal/app', 'knockout', 'services/jobs', 'job/jobDetail'], function (app, ko, jobService, JobDetail) {
return function JobsGrid() {
var self = this;
self.jobs = ko.observableArray();
self.activate = function() {
return jobService.getAgencyNurseJobs().then(function(jobs) {
//app.log('jobs page setup', jobs);
self.jobs(jobs);
})
.fail(function(error) {
app.log(error);
}).done();
};
self.showJobDetails = function(job) {
JobDetail.show(job);
};
self.gridConfig = {
pageSize: 13,
columns: [
{ header: '', property: '', canSort: false },
{ header: '', property: 'isNew', canSort: false },
{ header: 'Start Date', property: 'startDate' },
{ header: 'Category', property: 'jobCategory' },
{ header: 'Term', property: 'term' },
{ header: 'Shift Start', property: 'shiftStart' },
{ header: 'Job Type', property: 'jobType' },
{ header: 'Specialty', property: 'specialty' },
{ header: 'Work Site', property: 'hospital' },
{ header: 'Unit', property: 'unit' },
{ header: 'Location', property: 'location' },
{ header: 'Best Rate', property: 'bestRate' },
{ header: 'Status', property: 'status', sort: function(a, b) { return a.jobStatus() < b.jobStatus() ? -1 : 1; } }
],
data: self.jobs
};
};
});
<thead class="grid-columns" data-part="header">
<tr class="grid-column" data-part="headerRow" data-bind="foreach: columns">
<th class="grid-header-cell" data-bind="click: $parent.setSortColumn">
<span data-bind="text: header"></span>
<!-- ko if: $data == $parent.sortColumn() -->
<img class="sort-btn" data-bind="attr: { src: 'Content/images/' + ($parent.sortDesc() ? 'sort-asc.gif' : 'sort-desc.gif') }" alt=""/>
<!-- /ko -->
</th>
</tr>
</thead>
<tbody class="grid-body" data-bind="foreach: { data: currentPageRows, as: 'row' }" data-part="body">
<tr data-bind="foreach: $parent.columns" data-part="bodyRow">
<td data-bind="text: $parents[1].getColumnText($data, row)"></td>
</tr>
</tbody>
<tfoot>
<tr data-part="footer">
<td class="grid-footer" data-bind="attr: { colspan: columns().length }">
<button class="btn-page" data-bind="click: pageToFirst">First</button>
<button class="btn-page" data-bind="command: pageBackward">Prev</button>
<!-- ko foreach: pageButtons -->
<button class="btn-page" data-bind="css: { 'btn-page-active' : isActive }, click: $parent.goToPage, text: name"></button>
<!-- /ko -->
<button class="btn-page" data-bind="command: pageForward">Next</button>
<button class="btn-page" data-bind="click: pageToLast">Last</button>
</td>
</tr>
</tfoot>
define(['durandal/app', 'knockout'], function (app, ko) {
/*
This widget can only be bound on a <Table> element in the DOM
Overridding it's parts can only be done if the un-"processed" <Table> would have legal HTML structure
Otherwise it will not render correctly in IE
*/
return function Grid() {
var self = this,
rows = ko.observableArray();
self.columns = ko.observableArray();
self.activate = function(config) {
//app.log('grid setup', config);
var columns = config.columns,
pageSize = config.pageSize || 10,
alwaysShowPaging = config.alwaysShowPaging || true,
pageSizeOptions = config.pageSizeOptions || [25, 50, 75, 100],
showPageSizeOptions = config.showPageSizeOptions || false;
self.columns(columns);
rows(config.data);
self.pageSize(pageSize);
self.alwaysShowPaging(alwaysShowPaging);
self.pageSizeOptions(pageSizeOptions);
self.showPageSizeOptions(showPageSizeOptions);
};
self.getColumnText = function(column, row) {
if (!column.property)
return '';
return ko.unwrap(row[column.property]);
};
//sorting
var customSort;
self.sortDesc = ko.observable(true);
self.sortColumn = ko.observable({});
self.setSortColumn = function (column) {
if (column.canSort === false)
return;
//If column.sort is undefined, it will clear the customSort, which is what we want in that case
customSort = column.sort;
//Switch if column is same, otherwise set to true
self.sortDesc(column == self.sortColumn() ? !self.sortDesc() : true);
self.sortColumn(column);
};
var standardSort = function(a, b, sortProperty) {
var propA = ko.unwrap(a[sortProperty]),
propB = ko.unwrap(b[sortProperty]);
if (propA == propB)
return 0;
return propA < propB ? -1 : 1;
};
self.sortedRows = ko.computed(function () {
//The reason we have to read AND unwrap the rows is because rows is observable
//But the data is was passed through activate is its content, which may ALSO be observable
//It's admittedly a bit strange, but it makes the external API the simplest
//If a layer before sorting every gets introduced (like filtering), this "double" needs to go there
var sorted = ko.unwrap(rows()),
sortDirection = self.sortDesc() ? 1 : -1,
sortProperty = self.sortColumn().property || '';
if (sortProperty === '' )
return sorted;
var sort;
if (customSort)
sort = function(a, b) { return customSort(a, b) * sortDirection; };
else
sort = function (a, b) { return standardSort(a, b, sortProperty) * sortDirection; };
return sorted.sort(sort);
}).extend({ throttle: 10 }); //Throttle so that sortColumn and direction don't cause double update, it flickers
//pagination
self.pageSize = ko.observable(20);
self.pageIndex = ko.observable(0);
self.pageSizeOptions = ko.observableArray();
self.alwaysShowPaging = ko.observable(true);
self.showPageSizeOptions = ko.observable(false);
self.lastPageIndex = ko.computed(function () {
return Math.max(Math.ceil(self.sortedRows().length / self.pageSize()) - 1, 0);
});
self.pageCurrentNumber = ko.computed(function () {
return self.pageIndex() + 1;
});
self.pageToFirst = function() {
self.pageIndex(0);
};
self.pageToLast = function() {
self.pageIndex(self.lastPageIndex());
};
self.pageForward = ko.command({
execute: function() {
self.pageIndex(self.pageIndex() + 1);
},
canExecute: function() {
return self.pageIndex() < self.lastPageIndex();
}
});
self.pageBackward = ko.command({
execute: function () {
self.pageIndex(self.pageIndex() - 1);
},
canExecute: function () {
return self.pageIndex() > 0;
}
});
self.currentPageRows = ko.computed({
read: function () {
var pageSize = self.pageSize(),
pageStartIndex = self.pageIndex() * self.pageSize(),
sortedRows = self.sortedRows();
if (self.pageIndex() == self.lastPageIndex())
return sortedRows.slice(pageStartIndex);
else
return sortedRows.slice(pageStartIndex, pageStartIndex + pageSize - 1);
},
deferEvaluation: true
});
var pageCount = 5; //Max index, 5 pages
//The buttons for paginiation
//Will be buttons for up to 5 pages, with a selected page
//Selected page will be in the middle, when possible
self.pageButtons = ko.computed(function() {
var current = self.pageIndex().toNumber(),
last = self.lastPageIndex().toNumber(),
top = last,
bottom = 0;
if (current === 0) {
//Get current to either the last page, or pageCount from current
top = Math.min(pageCount - 1, last);
} else if (current === last) {
//Get from either the first page, or pageCount less than current, to current
bottom = Math.max(0, current - pageCount + 1);
} else {
//If it fits, we want pageCount buttons with current in the middle
//If it won't fit, we want the smaller of pageCount or the total number of pages
//Because we don't want the number of buttons to shrink in the latter case
var padding = Math.floor(pageCount / 2);
bottom = Math.max(0, current - padding);
top = Math.min(last, current + padding);
//There is room to pad more, and we don't have pageCount buttons
while (top - bottom !== pageCount - 1 && (last > padding || bottom > 0)) {
if (top < last)
top++;
else
bottom--;
}
}
return (bottom).upto(top).map(function(n) {
return { name: n + 1, isActive: n === current };
});
});
self.goToPage = function(page) {
self.pageIndex(parseInt(page.name, 10) - 1);
};
};
});
@dealproc
Copy link

the command: action bit... what are your thoughts on having drop-down lists here? For some of my stuff, i'm in need of providing a menu for "advanced" features.

KOGrid looked dead, and I didn't really want to use a "framework" looking grid like Wijmo or whatever... and I really, REALLY wanted to use OData and support as much of those bits as available on the UI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment