Getting Started
Steps for getting started with FlexGrid in KnockoutJS applications:
- Add references to KnockoutJS, Wijmo, and Wijmo's KnockoutJS bindings.
- Add a view model to provide data and logic.
- Add a Wijmo FlexGrid control to the page and bind it to your data.
- (Optional) Add some CSS to customize the input control's appearance.
This will create a FlexGrid with default behavior, which includes
automatic column generation, column sorting and reordering, editing,
and clipboard support.
<html>
<head>
<link rel="stylesheet" type="text/css" href="css/bootstrap.css"/>
<link rel="stylesheet" type="text/css" href="css/wijmo.css" />
<link rel="stylesheet" href="styles/app.css" />
<script src="scripts/knockout.js" type="text/javascript"></script>
<script src="scripts/wijmo.js" type="text/javascript"></script>
<script src="scripts/wijmo.input.js" type="text/javascript"></script>
<script src="scripts/wijmo.grid.js" type="text/javascript"></script>
<script src="scripts/wijmo.knockout.js" type="text/javascript"></script>
<script src="scripts/bindings/appBindings.js"></script>
<script src="scripts/app.js"></script>
<script src="scripts/viewmodels/appVM.js"></script>
</head>
<body>
<!-- this is the grid -->
<div data-bind="wjFlexGrid: { itemsSource: data }"></div>
</body>
</html>
// create and apply application view model
function viewModel1() {
// generate some random data
function getData(count) {
var countries = 'US,Germany,UK,Japan,Italy,Greece'.split(','),
data = new wijmo.collections.ObservableArray();
for (var i = 0; i < count; i++) {
data.push({
id: i,
country: countries[i % countries.length],
date: new Date(2014, i % 12, i % 28),
amount: Math.random() * 10000,
active: i % 4 == 0
});
}
return data;
}
// data used by most grids in the sample
this.data = getData(100);
.............
};
(function () {
ko.applyBindings(new viewModel1());
})();
/* set default grid style */
.wj-flexgrid {
height: 300px;
background-color: white;
box-shadow: 4px 4px 10px 0px rgba(50, 50, 50, 0.75);
margin-bottom: 12px;
}
Column Definitions
The Getting Started example did not define any columns, so FlexGrid generated them
automatically.
This example shows how you can define the columns using HTML markup.
You can also do this in code, but using markup allows you to have more separation
between the controller and the view.
Specifying the columns allows you to choose which columns to show, and in what order.
This also gives you control over each column's width, heading, formatting, alignment,
and other properties.
In this case, we use star sizing to set the width of the "Country" column.
This tells the column to stretch to fill the available width of the grid so there is no
empty space.
On the "Revenue" column, we set the format property to "n0", which results in numbers
with thousand separators and no decimal digits.
<div data-bind="wjFlexGrid: { itemsSource: data }">
<div data-bind="wjFlexGridColumn: { header: 'Country', binding: 'country', width: '*' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Date', binding: 'date' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Revenue', binding: 'amount', format: 'n0' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Active', binding: 'active' }"></div>
</div>
Selection Modes
By default, FlexGrid allows you to select a range of cells with the mouse or keyboard,
just like Excel. The selectionMode property allows you to change that so that you
can select a row, a range of rows, non-contiguous rows (like in a list-box), a single cell,
or disable selection altogether.
This example allows you to pick the selectionMode from a Wijmo Menu control.
<div data-bind="wjFlexGrid: { itemsSource: data, selectionMode: selectionMode }"></div>
<div data-bind="wjMenu: { value: selectionMode, header: 'Selection Mode' }">
<span data-bind="wjMenuItem: { value: 'None' }">None</span>
<span data-bind="wjMenuItem: { value: 'Cell' }">Cell</span>
<span data-bind="wjMenuItem: { value: 'CellRange' }">CellRange</span>
<span data-bind="wjMenuItem: { value: 'Row' }">Row</span>
<span data-bind="wjMenuItem: { value: 'RowRange' }">RowRange</span>
<span data-bind="wjMenuItem: { value: 'ListBox' }">ListBox</span>
</div>
// initialize selection mode
this.selectionMode = ko.observable('CellRange');
Result (live):
None
Cell
CellRange
Row
RowRange
ListBox
Editing
FlexGrid has built-in support for fast, in-cell editing like you find in Excel. There is no
need to add extra columns with Edit buttons that switch between display and edit modes.
Users can start editing by typing into any cell. This puts the cell in quick-edit mode.
In this mode, pressing a cursor key finishes the editing and moves the selection to a different cell.
Another way to start editing is by pressing F2 or by clicking a cell twice. This puts the cell in
full-edit mode. In this mode, pressing a cursor key moves the caret within the cell text.
To finish editing and move to another cell, the user must press the Enter, Tab, or Escape key.
Data is automatically coerced to the proper type when editing finishes. If the user enters invalid
data, the edit is cancelled and the original data remains in place.
You can disable editing at the grid, column, or row levels using the isReadOnly property of the
grid, column, or row objects. In this example, we make the ID column read-only.
<div data-bind="wjFlexGrid: { itemsSource: data }">
<div data-bind="wjFlexGridColumn: { header: 'ID', binding: 'id', isReadOnly: true, width: 50 }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Country', binding: 'country', width: '*' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Date', binding: 'date' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Revenue', binding: 'amount', format: 'n0' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Active', binding: 'active' }"></div>
</div>
Grouping
FlexGrid supports grouping through the ICollectionView interface, which is identical to the
one in .NET. To enable grouping, add one or more GroupDescription objects to the
CollectionView.groupDescriptions property, and ensure that the grid's showGroups property
is set to true (the default value).
GroupDescription objects are flexible, allowing you to group data based on value or on grouping
functions. The example below groups dates by year; amounts by range returning three ranges: over 5,000,
500 to 5,000, and under 500; and anything else by value. Use the menu to see the effects of each grouping.
Notice that the "Revenue" column displays the totals in the group rows. We do this by
setting the column's aggregate property to "Sum." The aggregate is automatically
updated when you edit the values in the column.
<div data-bind="wjFlexGrid: { itemsSource: cvGroup }">
<div data-bind="wjFlexGridColumn: { header: 'Country', binding: 'country', width: '*' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Date', binding: 'date' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Revenue', binding: 'amount', format: 'n0', aggregate: 'Sum' }"></div>
</div>
<div data-bind="wjMenu: { value: groupBy, header: 'Group by' }">
<span data-bind="wjMenuItem: { value: '' }">(no grouping)</span>
<span data-bind="wjMenuItem: { value: 'country' }">Country</span>
<span data-bind="wjMenuItem: { value: 'amount' }">Revenue</span>
<span data-bind="wjMenuItem: { value: 'date' }">Date</span>
<span data-bind="wjMenuItem: { value: 'country,date' }">Country and Date</span>
<span data-bind="wjMenuItem: { value: 'country,amount' }">Country and Revenue</span>
<span data-bind="wjMenuItem: { value: 'country,date,amount' }">Country, Date, and Revenue</span>
</div>
// expose the data as a CollectionView to show grouping
this.cvGroup = new wijmo.collections.CollectionView(getData(100));
this.groupBy = ko.observable('');
// update CollectionView group descriptions when groupBy changes
this.groupBy.subscribe(function(oldValue) {
var cv = self.cvGroup;
cv.groupDescriptions.clear();
if (self.groupBy()) {
var groupNames = self.groupBy().split(',');
for (var i = 0; i < groupNames.length; i++) {
var groupName = groupNames[i];
if (groupName == 'date') { // group dates by year
var groupDesc = new wijmo.collections.PropertyGroupDescription(groupName, function (item, prop) {
return item.date.getFullYear();
});
cv.groupDescriptions.push(groupDesc);
} else if (groupName == 'amount') { // group amounts in ranges
var groupDesc = new wijmo.collections.PropertyGroupDescription(groupName, function (item, prop) {
return item.amount >= 5000 ? '> 5,000' : item.amount >= 500 ? '500 to 5,000' : '< 500';
});
cv.groupDescriptions.push(groupDesc);
} else { // group everything else by value
var groupDesc = new wijmo.collections.PropertyGroupDescription(groupName);
cv.groupDescriptions.push(groupDesc);
}
}
}
});
Result (live):
(no grouping)
Country
Revenue
Date
Country and Date
Country and Revenue
Country, Date, and Revenue
Filtering
The FlexGrid supports filtering through the ICollectionView interface, which is identical to the
one in .NET. To enable filtering, set the CollectionView.filter property to a function that
determines which objects to include in the view.
In this example, we create a filter for the country, and get the filter value from the input control.
<div data-bind="wjFlexGrid: { itemsSource: cvFilter }"></div>
<div class="input-group">
<span class="input-group-addon"><span class="glyphicon glyphicon-filter"></span></span>
<input type="text" data-bind="value: filter, valueUpdate: 'input'"class="form-control" placeholder="filter"/>
</div>
// expose the data as a CollectionView to show filtering
this.filter = ko.observable('');
var toFilter, lcFilter;
this.cvFilter = new wijmo.collections.CollectionView(getData(100));
// holds the cvFilter.currentItem
this.cvFilterCurrentItem = ko.observable(this.cvFilter.currentItem);
// updates the cvFilterCurrentItem observable
this.cvFilter.currentChanged.addHandler(function () { self.cvFilterCurrentItem(self.cvFilter.currentItem) });
this.cvFilter.filter = function (item) { // ** filter function
if (self.filter()) {
return item.country.toLowerCase().indexOf(lcFilter) > -1;
}
return true;
};
this.filter.subscribe(function (oldValue) { // ** refresh view when filter changes
if (toFilter) {
clearTimeout(toFilter);
}
toFilter = setTimeout(function () {
lcFilter = self.filter().toLowerCase();
self.cvFilter.refresh();
}, 500);
});
Paging
The FlexGrid supports paging through the IPagedCollectionView interface, which is nearly identical
to the one in .NET. To enable paging, set the IPagedCollectionView.pageSize property to the number
of items you want on each page, and provide a UI for navigating the pages.
In this example, we use JavaScript to show 10 items per page. We add navigation buttons, and call
IPagedCollectionView methods in the button click bindings. Note that we use the pageIndex
and pageCount properties to show the current page and total number of pages.
<div data-bind="wjFlexGrid: { itemsSource: cvPaging }" style="height:auto"></div>
<div class="btn-group">
<button type="button" class="btn btn-default"
data-bind="click: function () { cvPaging().moveToFirstPage() },
disable: cvPaging().pageIndex <= 0">
<span class="glyphicon glyphicon-fast-backward"></span>
</button>
<button type="button" class="btn btn-default"
data-bind="click: function () { cvPaging().moveToPreviousPage() },
disable: cvPaging().pageIndex <= 0">
<span class="glyphicon glyphicon-step-backward"></span>
</button>
<button type="button" class="btn btn-default" disabled style="width:100px">
<span data-bind="text: cvPaging().pageIndex + 1"></span> / <span data-bind=" text: cvPaging().pageCount"></span>
</button>
<button type="button" class="btn btn-default"
data-bind="click: function () { cvPaging().moveToNextPage() },
disable: cvPaging().pageIndex >= cvPaging().pageCount - 1">
<span class="glyphicon glyphicon-step-forward"></span>
</button>
<button type="button" class="btn btn-default"
data-bind="click: function () { cvPaging().moveToLastPage() },
disable: cvPaging().pageIndex >= cvPaging().pageCount - 1">
<span class="glyphicon glyphicon-fast-forward"></span>
</button>
</div>
// expose the data as a CollectionView to show paging
var cvPaging = new wijmo.collections.CollectionView(getData(100));
// expose it as an observable to allow chnge notificasions forcing re-read of the child properties by consumers
this.cvPaging = ko.observable(cvPaging);
// set page size
cvPaging.pageSize = 10;
// on any collection change, send a change notification for the cvPaging observable to force re-reading
// of the child properties by consumers
function notifyCvPagingUpdated () {
self.cvPaging.valueHasMutated();
}
cvPaging.collectionChanged.addHandler(notifyCvPagingUpdated);
cvPaging.currentChanged.addHandler(notifyCvPagingUpdated);
cvPaging.pageChanged.addHandler(notifyCvPagingUpdated);
Master-Detail
The ICollectionView interface has built-in support for currency with the currentItem
property and currentChanged event, which enables you to implement master-detail scenarios with FlexGrid.
The simplest way to organize binding to the current item is to add an observable property
holding the item and update its value in the ICollectionView.currentChanged event handler.
<div data-bind="wjFlexGrid: { itemsSource: cvFilter, isReadOnly: true, }">
<div data-bind="wjFlexGridColumn: { header: 'Country', binding: 'country', width: '*' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Date', binding: 'date' }"></div>
</div>
<dl class="dl-horizontal">
<dt>ID</dt>
<dd><span data-bind="text: cvFilterCurrentItem().id"></span></dd>
<dt>Country</dt>
<dd><span data-bind="text: cvFilterCurrentItem().country"></span></dd>
<dt>Date</dt>
<dd><span data-bind="text: format(cvFilterCurrentItem().date, 'd')"></span></dd>
<dt>Revenue</dt>
<dd><span data-bind="text: format(cvFilterCurrentItem().amount, 'n2')"></span></dd>
</dl>
this.cvFilter = new wijmo.collections.CollectionView(getData(100));
// holds the cvFilter.currentItem
this.cvFilterCurrentItem = ko.observable(this.cvFilter.currentItem);
// updates the cvFilterCurrentItem observable
this.cvFilter.currentChanged.addHandler(function () {
self.cvFilterCurrentItem(self.cvFilter.currentItem)
});
Result (live):
- ID
- Country
- Date
- Revenue
Cell Templates
FlexGrid has an itemFormatter property that gives you complete control over
the contents of the cells. The KnockoutJS binding we provide for the grid uses this
to support in-line cell templates, so you can define the appearance of the cells using
plain HTML.
To define a cell template for a column, add the HTML to display in each cell to the
column definition. Use the $item observable variable to access the data item from within the
template, and the $row and $col variables to retrieve the cell's row and column indexes.
<div data-bind="wjFlexGrid: { itemsSource: data }">
<div data-bind="wjFlexGridColumn: { header: 'Country', binding: 'country', width: '*', isReadOnly: true }">
<img data-bind="attr: { src: 'resources/' + $item().country + '.png' }" />
<span data-bind="text: $item().country"></span>
</div>
<div data-bind="wjFlexGridColumn: { header: 'Date', binding: 'date' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Revenue', binding: 'amount', format: 'n0' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Active', binding: 'active' }"></div>
</div>
Conditional Styling
The wjFlexGridColumn binding supports the wjStyle binding. This allows
you to customize the style used to display the data in each cell based on its value.
The wjStyle binding conforms to the same rules used by the native KnockoutJS style
binding. In the same way as for the custom cell templates, the $item, $row and $col observable
variables are available for binding, in addition to the properties defined in the view model.
This example uses a JavaScript function to create value ranges that return named
colors. We then call this function in the Revenue column inside the wjStyle binding
and use the $item variable to pass in the data and set the color.
<div data-bind="wjFlexGrid: { itemsSource: data }">
<div data-bind="wjFlexGridColumn: { header: 'Country', binding: 'country', width: '*' }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Date', binding: 'date' }"></div>
<div data-bind="wjFlexGridColumn: {
header: 'Revenue',
binding: 'amount',
format: 'n0'
},
wjStyle: { color: getAmountColor($item().amount) }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Active', binding: 'active' }"></div>
</div>
// get the color to be used for displaying an amount
this.getAmountColor = function (amount) {
if (amount < 4000) return 'darkred';
if (amount < 6000) return 'black';
return 'darkgreen';
}
Themes
The appearance of the FlexGrid is defined in CSS. In addition to the default theme, we
include about a dozen professionally designed themes that customize the appearance of
all Wijmo controls to achieve a consistent, attractive look.
You can customize the appearance of the grid using CSS. To do this, copy CSS rules
from the default theme to a new CSS file and modify the style attributes you want to change.
In this example, we add a "custom-flex-grid" class to the grid element and define some
CSS rules to create a simple "black and white, no borders" theme for any grids that
have the "custom-flex-grid" class.
We also customize the appearance of the glyphs used to show the column sorting direction
and the outline nodes in grouped grids. To see the custom glyphs, click a column header
cell.
<div data-bind="wjFlexGrid: { itemsSource: data }" class="custom-flex-grid"></div>
/* create a 'custom-flex-grid' theme for the FlexGrid */
.custom-flex-grid .wj-header.wj-cell {
background-color: #000;
color: #fff;
font-weight: bold;
border-right: solid 1px #404040;
border-bottom: solid 1px #404040;
}
.custom-flex-grid .wj-cell {
border: none;
background-color: #fff;
}
.custom-flex-grid .wj-alt:not(.wj-state-selected):not(.wj-state-multi-selected) {
background-color: #fff;
}
.custom-flex-grid .wj-state-selected {
background: #000;
color: #fff;
}
.custom-flex-grid .wj-state-multi-selected {
background: #222222;
color: #fff;
}
/* override the glyphs used to show sorting and grouping */
.custom-flex-grid .wj-glyph-up {
background-image:url('../resources/ascending.png');
background-repeat: no-repeat;
background-position: bottom right;
width: 1em; height: 1em;
border-top: 0px; border-bottom: 0px; border-left: 0px; border-right: 0px;
opacity: 1;
}
.custom-flex-grid .wj-glyph-down {
background-image:url('../resources/descending.png');
background-repeat: no-repeat;
background-position: bottom right;
width: 1em; height: 1em;
border-top: 0px; border-bottom: 0px; border-left: 0px; border-right: 0px;
opacity: 1;
}
.custom-flex-grid .wj-glyph-right {
background-image:url('../resources/collapsed.png');
background-repeat: no-repeat;
background-position: bottom right;
width: 1em; height: 1em;
border-top: 0px; border-bottom: 0px; border-left: 0px; border-right: 0px;
}
.custom-flex-grid .wj-glyph-down-right {
background-image:url('../resources/expanded.png');
background-repeat: no-repeat;
background-position: bottom right;
width: 1em; height: 1em;
border-top: 0px; border-bottom: 0px; border-left: 0px; border-right: 0px;
}
Trees and Hierarchical Data
In addition to grouping, FlexGrid supports hierarchical data, that is, data with items
that have lists of subitems. This type of hierarchical structure is very common, and is
usually displayed in a tree-view control.
To use FlexGrid with hierarchical data sources, set the childItemsPath property
to the name of the data element that contains the child elements. The grid automatically
scans the data and builds the tree for you.
<div class="custom-flex-grid"
data-bind="wjFlexGrid: {
itemsSource: treeData,
childItemsPath: 'items',
allowResizing: 'None',
selectionMode: 'ListBox',
headersVisibility: 'None'
}">
<div data-bind="wjFlexGridColumn: { binding: 'name', width: '*' }"></div>
<div data-bind="wjFlexGridColumn: { binding: '', width: 80, align: 'center' }"></div>
</div>
// hierarchical data
this.treeData = [
{ name: '\u266B Adriane Simione', items: [
{ name: '\u266A Intelligible Sky', items: [
{ name: 'Theories', length: '2:02' },
{ name: 'Giant Eyes', length: '3:29' },
{ name: 'Jovian Moons', length: '1:02' },
{ name: 'Open Minds', length: '2:41' },
{ name: 'Spacetronic Eyes', length: '3:41' }]
}
]},
{ name: '\u266B Amy Winehouse', items: [
{ name: '\u266A Back to Black', items: [
{ name: 'Addicted', length: '1:34' },
{ name: 'He Can Only Hold Her', length: '2:22' },
{ name: 'Some Unholy War', length: '2:21' },
{ name: 'Wake Up Alone', length: '3:43' },
{ name: 'Tears Dry On Their Own', length: '1:25' }]
},
// more hierarchical data...
Handling null values
By default, FlexGrid allows you to enter empty values in columns of type string,
and will not allow empty/null values in columns of any other type.
You can change this behavior using the isRequired property on grid columns.
If you set the isRequired property to false, the grid will allow you to
enter empty values in that column, regardless of type. Conversely, if you set
the isRequired property to true, the grid will not allow empty values
even in string columns.
Setting isRequired to null reverts to the default behavior (nulls allowed
only in string columns).
The grid below reverts the default behavior. It sets isRequired to false
for the first column, and to true for all others. You can delete content that
is not required by entering an empty string or simply by pressing the delete
key.
<div data-bind="wjFlexGrid: { itemsSource: data }">
<div data-bind="wjFlexGridColumn: { header: 'Country', binding: 'country', width: '*', isRequired: true }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Date', binding: 'date', isRequired: false }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Revenue', binding: 'amount', format: 'n0', isRequired: false }"></div>
<div data-bind="wjFlexGridColumn: { header: 'Active', binding: 'active', isRequired: false }"></div>
</div>