This addon provides an Ember-specific wrapper around the the Fixtable library. Fixtable provides an easy way to create data grids with scrollable content and a fixed header/footer. In addition, Fixtable-Ember provides enhanced functionality such as sorting, filtering, and pagination.
See a live demo here: http://purecloudlabs.github.io/fixtable-ember/
(Note: The source for the demo site linked above can be found in this repo's tests/dummy/app directory in the master branch.)
Navigate to the root directory of the project where you want to install this addon. Then, install this addon in your app using the following commands:
ember install fixtable-ember
This addon creates a new component that you can use in your templates: fixtable-grid
Although it isn't strictly required, we recommend including Bootstrap 3 in your project for optimal display.
If you are using the fixtable-ember
addon within an Ember engine, the fixtable-ember
dependency must be under "dependencies" instead of "devDependencies" in the package.json file of your engine. Otherwise, the fixtable-grid
component will fail to render.
The fixtable-grid
component expects to be passed a columns
property, which should be a JavaScript array consisting of objects that have the following keys: key
, header
, and width
.
key
(required) - Unique string identifier for the column.header
(optional) - Text to show in the column header. If this isn't passed in,key
will be shown in the header.width
(optional) - Width of the column. Can be either a number (interpreted as pixels) or a string percentage (formatted like "50%"). If no width is passed in, the column will be sized automatically. Using a combination of pixel and percentage widths is not recommended.hideLabel
(optional) - If true, no column header will be shown for this column (even if the header property is specified).
Here's a simple example using just the key:
[
{ key: 'name' },
{ key: 'address' }
]
Here's an example using header
and a mixture of pixel widths and automatic sizing:
[
{
key: 'name',
header: 'Name',
width: 150 // -> 150px
},
{
key: 'address',
header: 'Street Address'
}
]
Here's an example using percentage width:
[
{
key: 'name',
width: '40%'
},
{
key: 'address',
width: '60%'
}
]
The component expects a content
property to be passed in. This should be an array of objects, either a native Array
or one of Ember's Array-like objects (Ember.ArrayProxy
, DS.ManyArray
, or their subclasses).
- Each object element in the array represents a row of data.
- Each key/value pair in a row object represents a single cell of data. The key should match a key defined in
columns
, and the value represents the content for the column/row intersection. - If there is no data for a given column, that key/value pair can be omitted from the row object.
For example, this is an example of content corresponding to the name and address columns defined in the examples above.
[
{ name: 'Sherlock Holmes ', address: '221B Baker Street' },
{ name: 'Dudley Dursley', address: '4 Privet Drive' },
{ name: 'Hank Hill', address: '84 Rainey Street' },
{ name: 'Gandalf Stormcrow' }
]
The primary feature of the Fixtable library is its ability to allow scrollable content with a fixed header and footer. To enable this feature, all you need to do is pass in a CSS class with a defined height through the fixtableClass
property.
The class (or classes, if you pass in a space-delimited class name string) specified in fixtableClass
will be added to the Fixtable container element.
Specifying a height for the Fixtable is required for optimal display. If you do not specify a height, the table may appear to "collapse" when there are no data rows. (Note that height does not need to be specified as a fixed number of pixels; percentage-height is also fine as long as the parent element has a size.)
For example:
.restrict-height {
height: 500px;
}
At its core, the Fixtable is an HTML table element. If you want to customize the look of the Fixtable by adding CSS classes directly to the table element, you can do that with the tableClass
property. For example, we might want to add the table-hover
class to add Bootstrap's default styles when you hover over a row in the table.
By default, Fixtable already adds the table
CSS class to the table element. As a result, you do not need to pass table
to the tableClass
property.
You can also add custom CSS classes directly to the cell (td
) elements for a given column. This should be done by specifying a cellClass
for the column in the column definition. For example, let's say we want to add the "name" CSS class to every cell in the Name column. Our column definition would need to look like this:
[
{
key: 'name',
header: 'Name',
cellClass: 'name'
},
{
key: 'address',
header: 'Street Address'
}
]
If your data is being loaded asynchronously, you can show a loading indicator to make this clear to the user. Loading state can be toggled by binding to the isLoading
property:
In this example, dataIsLoading
is assumed to be a property on the containing component or controller, indicating whether data has finished loading.
While isLoading
is true, a spinning 'loading' indicator will be shown in place of the grid content. When it becomes false, the loading indicator will be replaced with the actual data rows.
In the examples above, we are making the following assumptions:
- We defined column definitions and grid content on our model as
columnDefs
anddataRows
respectively - We have a
restrict-height
CSS class that we want to apply to the Fixtable to limit its height - We want to apply the
table-hover
class to the table element so that rows have a hover style - We are indicating whether data has finished loading by binding the component's
isLoading
property to ourdataIsLoading
property
With those assumptions, our final markup for the fixtable-grid
component ends up looking like this:
By default, the Fixtable grid assumes that its passed-in content represents all of the available content. However, if your content is split into multiple pages, there is built-in support for two different kinds of pagination: client-side and server-side.
To enable client paging, all you have to do is set the clientPaging
property to true. The Fixtable will then assume that all data has been loaded and is present in the content
collection, and it will handle pagination logic internally.
With client paging turned on, the Fixtable will now limit the number of rows that are shown in the table at a time. Beneath the table, there will be a pagination footer that shows the current page and the total number of pages. The footer also lets users go back or forward a page, jump to a specific page, and configure the page size.
The possible options for page size are 25, 50, 100, 250, and 500 -- however, this will be limited based on the number of rows. For example, if there are only 55 rows total, then the possible page sizes will be 25, 50, and 100. There's no need to show the 250 and 500 page size options since they will be functionally identical to a page size of 100.
Enabling server paging takes a little more setup:
- First, set the
serverPaging
property to true. - Bind the component's
totalRowsOnServer
property to a value representing the length of the dataset on the server. (The Fixtable needs this information to calculate how many pages there are.) - Attach an action to the component's
onReloadContent
property. The action function will receive two parameters --page
(the current 1-indexed page number) andpageSize
(the current page size) -- and is called whenever either the page or the page size changes.
Although the same pagination footer will be shown as with client-side pagination, the Fixtable will now assume that pagination should not be handled on the client (or at least, not by the Fixtable itself). Instead, it provides a hook named onReloadContent
to let the consumer know when the current page or the page size changes. The consumer is expected to use this information to update the bound content of the fixtable-grid
component.
Here's some sample markup:
Notice that we've turned on server paging, set the total number of rows, and bound the updatePage
action to the component's onReloadContent
property. We've also set a property that lets us toggle the loading indicator.
In our controller, we might define updatePage
something like this:
actions: {
updatePage(page, pageSize/*, filters*/) {
this.set('serverPageIsLoading', true);
this.store.query('rows', { page, pageSize )
.then(rows => {
this.set('serverPageIsLoading', false);
this.set('model.pagedDataRows', rows);
});
}
}
In this example, first we are showing the loading indicator to let the user know that data is loading. Next, we request additional data from the server based on the updated page
and pageSize
. When we receive the data, we hide the loading indicator and set the new rows into the model property bound to the fixtable-grid
component.
The default page size is 25 with the following default possible page sizes for the paginated table:
[ 25, 50, 100, 250, 500 ]
The user can switch between page sizes using the pagination footer at the bottom of the table. The page size options shown to the user will be restricted based on the number of data rows (e.g., if there are 53 rows, then the possible page sizes will be 25, 50, or 100) -- the goal is to only show as many page size options as necessary to display all of the data.
If you want to pass in different page size options, you can override the component's pageSize
, possiblePageSizes
, currentPage
properties. The possible page sizes must be listed in ascending order.
You may want to render something more complicated into a cell than just a simple string value. For example, you might want to make each cell link out to another route, or you might want to embed a Handlebars template into a cell. You can do that by creating a custom cell component.
It's very easy to create a custom cell component -- all you have to do is specify a component
for the column in the column definitions:
For example, here we have two regular columns and a third "profile" column that will render the link-to-profile
component in each cell.
[
{
key: 'name',
header: 'Name',
},
{
key: 'address',
header: 'Street Address'
},
{
key: 'profile',
header: 'Profile',
component: 'link-to-profile'
}
]
Custom cell components will automatically be provided with three properties:
columnDef
- the column definitiondataRow
- the object from thecontent
array represented by this rowrowExpanded
- a boolean value indicating whether or not the row is expanded (more on this in the Row Expansion section below)
For example, the Handlebars markup for the example link-to-profile
component could look like this:
This would render a link to the "profile" route in each row, passing the "name" property as a parameter.
If you have a custom cell component defined, you may want to allow the consumer of the fixtable-grid to handle actions raised by that cell component. To facilitate that, a bubbleAction
function is set on the custom component, which can be called like this from the cell component's JavaScript:
this.bubbleAction(myActionName[, args]);
The action with name myActionName
will bubble up to the consumer of the fixtable-grid
component. That consumer should set an action handler on the fixtable-grid
:
Where myActionName
is replaced with the actual name of the action bubbling up, and myActionHandler
is some action defined on the parent of the fixtable-grid
.
Alternatively, you skip calling this.bubbleAction
in the cell component's JavaScript by directly using bubbleAction
as an action in the markup of the component:
This will directly bubble the action with name myActionName
to the parent of the fixtable-grid
component, passing along any additional arguments that follow the action name.
You can supply your own footerComponent
which will be rendered in place of the default fixtable-footer
component. The following properties will be made available to your custom footer:
currentPage
- integer value of the currently selected page (starting at 1)totalRows
- integer value of the total number of rows; if you're using server-side paging, this will be equal tototalRowsOnServer
, otherwise it's the number of rows in the current filtered content arraytotalPages
- integer value of the number of available pages given the currentpageSize
andtotalRows
selectedDataRows
- array of the currently selected row objectspageSize
- integer value of the number of items in each pagepageSizeOptions
- array of integers which can optionally be used to provide the user an affordance for changingpageSize
isLoading
- boolean value to indicate when a new page is being loaded (it's up to youronReloadContent
method to update this value)goToNextPage
- action which your component may invoke to increment the currentPagegoToPreviousPage
- action which your component may invoke to decrement the currentPagebubbleAction
- action which your component may invoke to bubble actions up to the consumer of the fixtable-grid (this works the same as custom cell components; see Bubbling Actions from Custom Cell components above)
You can supply your own emptyStateComponent
which will be rendered in place of the default fixtable-empty-state
component when the content array is empty. You can also supply an object using the emptyStateComponentValues
property with dynamic values to be passed along to your component as values
.
The default empty state component simply shows a message, centered vertically and horizontally within Fixtable. This message defaults to "No data available", but you can override it by using the emptyStateComponentValues
property to set an object with a nullMessage
property. For example:
{{fixtable-grid emptyStateComponentValues={nullMessage: "Nothing to see here!"}}}
Fixtable supports filtering the displayed rows, either by search text or by selected option. To set this up, add a filter property to the column definition.
For example, this adds a search-type filter to the name column:
[
{
key: 'name',
header: 'Name',
filter: {
type: 'search'
}
},
{
key: 'address',
header: 'Street Address'
}
]
Similar to how pagination is handled, the implementation of filtering depends on whether all of the Fixtable's content is present on the client.
As long as serverPaging
is not set to true, the Fixtable can handle the filtering logic internally. Adding a filter type to the column definition, as described above, will show a filter field in the column header. Typing into the search field (or selecting a filter option) will automatically filter the visible data rows.
Server-side filtering presents the same user interface as client-side filtering, but the content is not automatically filtered (because the Fixtable cannot assume that all data is present on the client). Server-side filtering is active whenever the serverPaging
property is set to true. In other words, if you're using server-side pagination, you also need to use server-side filtering.
Server-side filtering uses the same onReloadContent
event as server-side pagination, so you don't need to define a separate function or property. The third parameter passed to the function (after page
and pageSize
) will be filters
, an object where the key/value pairs link column keys to filter text. For example:
{
name: 'smith',
address: ''
}
If the value is null or empty, that means that there is no filter applied to that column.
After requesting and receiving the updated data from the server, you should also update the value of totalRowsOnServer
so that it reflects the total number of filtered rows on the server. Otherwise, there may be additional "blank" pages in the Fixtable, because the number of pages shown is determined by totalRowsOnServer
.
Do note that it's possible for changing the filters to simultaneously change the current page. (The Fixtable automatically jumps to the first page if new filters are applied.) If this happens, the onReloadContent
function will still only be invoked a single time. As a result, you should likely always include both filter and pagination information in your server request.
There are two types of filters: search filters and select filters. Search-type filters discriminate rows based on the text typed into the filter field, excluding any row that does not include the searched-for text as a substring (case-insensitive).
You can specify a search-type filter in the column definitions as follows:
[
{
key: 'name',
header: 'Name',
filter: {
type: 'search'
}
}
]
Select-type filters discriminate rows based on a discretely selected option, where the row's value for that column must be equal to the selected filter option (again, case-insensitive). A label may be optionally provided for each option to have control over the text shown in the select-type filter dialog. The options for a select-type filter may be specified as follows:
[
{
key: 'grade',
header: 'Letter Grade',
filter: {
type: 'select',
selectOptions: [
{ value: 'A', label: 'The letter A' },
{ value: 'B' },
{ value: 'C', label: 'The letter C' },
{ value: 'D' },
{ value: 'F' },
]
}
}
]
If you prefer not to specify the possible select options in the column definitions, you can simply set the automaticOptions
flag to true in the filter
object of the column definition. This will cause the select options to be automatically generated from the values that are present in the data.
[
{
key: 'grade',
header: 'Letter Grade',
filter: {
type: 'select',
automaticOptions: true
}
}
]
Automatic filtering should not be used if server-side pagination is turned on, because Fixtable will not be able to determine all of the possible filter options without all of the data loaded onto the client. (Automatic filtering can be used with client-side pagination, however.) Note that the generated options will be unique and case-insensitive.
To avoid rapid and unnecessary filtering as the user types, especially in the case where filtering happens on the server and requires AJAX calls, the Fixtable automatically debounces filtering by 500ms. In other words, the filter will not be applied until 500ms after the user stops typing into a filter field. This can be configured by setting the component's filterDebounce
property, if desired. (The property should be set to a number representing the debounce time in milliseconds.)
For example, this lengthens the debounce time to a full second.
By default, the Fixtable will automatically filter itself as the user types into filter fields -- with a short debounce interval, as described above. However, real-time filtering can be disabled entirely. This may be useful in workflows where you want to reduce traffic to the server, or where the user wants to enter filters in multiple fields before seeing the filtered results.
To disable real-time filtering, simply set the realtimeFiltering
property to false
. The Fixtable will automatically show Apply and Clear buttons that apply or clear the entered filters, respectively.
You can specify placeholder text for search- and select-type filters by setting the value of the placeholder
property within the filter
object of the column definition.
[
{
key: 'name',
header: 'Name',
filter: {
type: 'search',
placeholder: 'name search'
}
}
]
If the filter is search-type, the placeholder
attribute of the <input>
element will be set to the placeholder value. If the filter is select-type, the text of the first <option>
of the <select>
element will be set to the placeholder value.
You may want to render something other than a search field or select list for the filter. To do so, specify the component to render by providing
the name in the component
property of the filter
object.
For example, given that a component named "checkbox-filter" exists:
[
{
key: 'name',
header: 'Name',
filter: {
component: 'checkbox-filter'
}
}
]
If both component
and type
are specified, component
will take precedence and type
will be ignored.
The components will have access to two properties: columnDef
and filter
. These properties represent the column definition and the filter value for
that column, respectively. Setting the filter
property in the component will invoke the onReloadContent
action or cause the manual
filter buttons to display based on the value of the realtimeFiltering
.
If you are using client-side filtering, then you should also provide a custom filter function along with your custom component. Set the custom filter function as the filterFunction
property of your column definition's filter
object.
When invoked, filterFunction
will be passed a data row and the current filter value for that column. It should return true if the given row passes the filter, and false otherwise.
[
{
key: 'name',
header: 'Name',
filter: {
component: 'checkbox-filter',
filterFunction(rowData, filterValue) {
return rowData.name === 'Roland Deschain';
}
}
}
]
Columns that have a custom cell component can be filtered, but only if the data rows defined in content
have values corresponding to the column using the custom cell component. The filtering will be based on the data in content
, not on what's rendered by the custom cell component.
Fixtable has built-in support for column sorting.
To enable sorting for a given column, just add sortable: true
to the column definition.
For example, in the column definitions below, the name column is sortable:
[
{
key: 'name',
header: 'Name',
sortable: true
},
{
key: 'address',
header: 'Street Address'
}
]
Clicking on the header of a sortable column will sort it in ascending order. Clicking on the column header again will toggle ascending/descending. Clicking on another column header will switch to sorting by the new column in ascending order.
To specify a default sort column or sort order, you can bind to the sortBy
and sortAscending
properties of the fixtable-grid
component, respectively. (sortAscending
defaults to true, so you can omit this property if that's what you want.) sortBy
should be a column key, and sortAscending
should be a boolean.
For example, this is how we might sort by ID in descending order:
In the owning component or controller:
sortKey: 'id',
ascending: false,
Note that you should use the mut
helper to indicate that the fixtable-grid
is allowed to mutate the value. Because the Fixtable needs to change these values depending on user actions, you should not use literal string and boolean values for these properties.
As long as serverPaging
is not turned on, Fixtable can handle the sorting logic on its own, under the assumption that all of the table data is already loaded on the client. Specifying the sortable
key in the column definition is enough to enable client-side sorting.
There are also some additional features that can be used to customize sorting on the client. By default, sorting is done in lexicographical order, using JavaScript's String.prototype.localeCompare function. If you want to sort using some other means, you can specify a custom sort function using the sortFunction
key of the column definition.
For example, this column definition represents an ID column that is sorted numerically.
{
key: 'id',
header: 'ID',
sortable: true,
sortFunction: (x, y) => x - y
}
When specifying a custom sort function, note that the intended sort direction is assumed to be ascending. When the column is sorted in descending order, the same function will be invoked, but the parameter order will be reversed. The sort function should expect to receive two parameters, corresponding to two cell values for the rows being compared. (In the example above, x
and y
would be ID values.) In your sort function, don't forget to consider the possibility of null or undefined cell values.
When server paging is turned on, Fixtable cannot implement sorting on the client because it doesn't have enough information. Instead, the same onReloadContent
action used for filtering and pagination will be invoked. The fourth parameter of the passed-in function will be a sortInfo
object that contains key
and ascending
properties. key
corresponds to the key of the column that should be sorted, and ascending
is a boolean that indicates whether the sort should be ascending or descending. The sortInfo
parameter will be null if no sort column is specified.
For example, sortInfo
would look like this if we wanted to sort by ID, ascending:
{
key: 'id',
ascending: true
}
Similarly to server-side pagination and filtering, the Fixtable will take care of notifying its consumer whenever the table needs to be sorted, but actually sorting the table's bound data is the responsibility of the consumer.
Currently, you can only sort by a single column at a time. (In other words, there is no concept of primary/secondary sort column.)
Also, just like with filtering, sorting is based on what's defined in content
, not on the actual markup rendered in the cell. Keep this in mind when using sorting with custom cell components.
You may want to implement UI for filtering data outside of Fixtable itself. Perhaps in a sibling component that allows for more complex controls than can comfortably fit in a column header.
To support this use case, Fixtable allows for an externalFilters
property to be set. It will then monitor this value for changes and trigger your onReloadContent
callback to refresh the table content. The current value of externalFilters
will be passed to your callback as the fifth parameter.
Fixtable does not prescribe any particular format for your external filters, so you are free to implement whatever data structure works best for your needs. This, however, means you may need to use notifyPropertyChange()
when your external filters are updated, to explicitly notify Fixtable of the change.
Note: serverPaging
must be enabled.
Let's recap onReloadContent
, since it can be triggered by several different kinds of content updates. This property of the fixtable-grid
should be set to an action on the owning controller or component. The bound action will be called whenever the table is paged, sorted, or filtered, or when any external filters are changed. (This is true regardless of whether client paging or server paging is active, but obviously it's more important for server paging.) The action will be invoked with the following parameters:
page
(Number) - The current 1-indexed page number.pageSize
(Number) - The maximum number of rows shown on a single page.filters
(Object) - Object where the key/value pairs map column keys to filter text.sortInfo
(Object) - Object with a stringkey
property representing the column to sort by, and a booleanascending
property indicating the sort direction. If the table is unsorted,sortInfo
will be null.externalFilters
(Object) - The current value of yourexternalFilters
property; this is completely outside of Fixtable's control.
Fixtable has built-in support for selecting rows. If you set the rowSelection
property to true, the fixtable-grid
will render a leftmost column of checkboxes that can be used to select individual rows. In the header for the checkbox column, there will also be a checkbox that can be used to toggle selection for all of the visible rows.
Selected rows will automatically have the active
CSS class applied to them. This will give them a default selected style if you have Bootstrap pulled in, but you can also override those styles as desired.
When rows are selected, any kind of content reload (i.e., anything that would trigger onReloadContent
-- sorting, paging, or filtering) will clear the selected rows. (This is true regardless of whether the Fixtable is using client paging, server paging, or no paging at all.)
Consumers should keep track of which rows are selected by subscribing to onSelectionChanged
. Note that whenever the onReloadContent
handler is called, the onSelectionChanged
handler will also be invoked immediately afterwards, informing the listener that the selected rows have been cleared out.
onSelectionChanged
should be bound to an action on the owning controller or component. The bound action will be called whenever a row is selected or deselected. It receives the following parameter:
selectedDataRows
(Object) - The selected data rows.
For simple row click interaction without showing the row checkbox onRowClick
should be bound to an action on the owning controller or component. The bound action will be called whenever the row is clicked. It receives the following parameter:
clickedRow
(Object) the data object for the row that was clicked.
Fixtable has built-in support for expandable rows, for cases where you may desire to show more information than can comfortably fit on a single row. When a row is "expanded," a second, full-width table row is displayed beneath the normal row. In this expanded space, Fixtable will render whatever custom component you specify in the expandedRowComponent
property. This component will receive the same dataRow
binding as any custom cell component in the main row (see Custom Cell Components above).
To allow for toggling rows between expanded and collapsed state, you should include a component in one of your column definitions that toggles the rowExpanded
property. Fixtable includes a simple component, fixtable-row-toggle
, that you can use for this purpose. You can also make a custom component if you need additional logic or prefer a different design. It's also noteworthy that any custom cell component can read and write the rowExpanded
property, so you can toggle or react to this value in multiple columns if needed.
Because "expanded" rows are actually rendered as two table rows, Fixtable takes some steps to ensure you can still style them as desired. For one thing, any border between the two rows will be removed by default to maintain the illusion of a single row (this can, naturally, be overridden in your own CSS). Fixtable also applies some helpful CSS classes to each row element:
fixtable-row-primary
- the primary, multi-column row (rendered from your column definition)fixtable-row-secondary
- the second, full-width column that renders yourexpandedRowComponent
expanded
- present on both row elements when the dataRow is expandedhover
- present on both row elements when the user's cursor is hovering over either row element (you should use this class rather than relying on the:hover
pseudo-class if you want to use hover styles with expanded rows)active
- present on both row elements if the row is selected (see Row Selection above)
Fixtable-Ember utilizes the core Fixtable library to handle the low-level DOM manipulation required for the fixed-header effect, ensuring Fixtable is styled to match your application's tables, etc. If you notice rendering issues, they're likely to originate in this core library and not Fixtable-Ember itself. If you set the debugMode
property to true
on your fixtable-grid
component, it will enable debug logs on its internal Fixtable
object which may be helpful in diagnosing issues.
git clone
this repositorynpm install
ember server
- Visit the dummy test app at http://localhost:4200/fixtable-ember/. The trailing slash seems to be required.
npm test
(Runsember try:testall
to test your addon against multiple Ember versions)ember test
ember test --server
ember build
For more information on using ember-cli, visit http://ember-cli.com/.