Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Ember Table Extensions
# So, this is pretty horrible. If we just encode using btoa, any UTF-8 chars cause an error.
# If we use either of the workarounds on MDN[1], the £ sign is encoded wrong. I suspect
# Excel totally sucking at encodings is the reason why. So, the workaround is, to use
# the MDN workaround on chars with values > 255, and allow chars 0-255 to be encoded
# as is with btoa. Note that if you use either of the workarounds on MDN, chars
# 128-255 will be encoded as UTF-8, which includeds the £ sign. This will cause excel
# to choke on these chars. Excel will still choke on chars > 255, but at least the £
# sign works now...
# [1]
App.encode64 = (str) ->
escaped = for i in [0...str.length]
if str.charCodeAt(i) > 255
btoa escaped.join('')
# The elsewhere handler will fire when anywhere outside the current view
# is clicked, e.g. to hide a popup when the background is clicked
Ember.ClickElsewhere = Ember.Mixin.create
onClickElsewhere: Ember.K
clickHandler: (->
@get('elsewhereHandler').bind @
elsewhereHandler: (event) ->
isThisElement = $('element')).length is 1
unless isThisElement
@onClickElsewhere event
didInsertElement: ->
$(window).bind 'click', @get("clickHandler")
willDestroy: ->
$(window).unbind 'click', @get("clickHandler")
class App.Compare
# a fast compare utility function
@fast: (ascending=true) ->
if ascending
(a, b) ->
if a < b then -1
else if a > b then 1
else 0
(b, a) ->
if a < b then -1
else if a > b then 1
else 0
$light-gray: #ccc;
$dark-gray: #666;
$accent-color: #66ccff;
.extended-table {
margin-top: 1em;
&>h2 {
float: left;
line-height: 1.4em;
.presentation-container {
max-width: 100%;
width: auto;
height: 400px;
clear: both;
a.csv-download {
float: right;
margin-top: 0.5em;
font-size: 0.8em;
.column-select, .column-group {
float: right;
position: relative;
margin-left: 1em;
.choose-columns, .group-column {
text-decoration: none;
color: black;
padding: 0.5em;
cursor: pointer;
float: right;
line-height: 1em;
border: 1px solid $light-gray;
span {
font-size: 0.8em;
color: $light-gray;
&.is-choosing-columns, &.is-choosing-group {
border-color: $accent-color;
span {
color: $accent-color;
margin-bottom: 1em;
ul, .radio-group {
z-index: 1;
position: absolute;
top: 3em;
right: 0;
background-color: white;
border: 1px solid $accent-color;
padding: 1em;
width: 15em;
@include border-radius(4px);
@include box-shadow($light-gray 0 0 15px);
label {
font-size: 0.8em;
input, label {
cursor: pointer;
li em {
font-size: 0.8em;
margin-bottom: 1em;
display: block;
text-align: center;
color: $dark-gray;
.radio-group label {
display: block;
.tables-container .table-container {
.table-scrollable-wrapper .table-block .table-row {
.table-cell.clickable {
cursor: pointer;
&:hover {
color: $accent-color;
.table-fixed-wrapper .table-block .table-row {
.table-cell.header-cell { {
cursor: pointer;
span.sort-direction {
color: $accent-color;
font-size: 0.9em;
padding: 0;
position: relative;
top: -1px;
&.inactive {
color: $light-gray;
span.filter-header-text {
&, & span {
line-height: 27px;
input.filter {
position: absolute;
left: 2px;
bottom: 2px;
right: 2px;
@include opacity(0.4);
&:focus, &:hover, &.is-populated {
@include opacity(1);
App.FilterField = Em.TextField.extend
classNames: ['filter']
classNameBindings: ['isPopulated']
type: 'search'
results: 0
attributeBindings: ['autofocus', 'results']
isPopulated: (->
!Em.isEmpty @get('value')
).property 'value'
keyDown: (event) ->
code = event.keyCode
action = switch code
when 38 then 'hilitePrev' # up arrow
when 40 then 'hiliteNext' # down arrow
else null
if action
cancel: ->
if (action = @get('cancelAction'))
@get('controller').send action, @
App.RadioButtonGroup = Em.View.extend
templateName: 'radio_button_group'
classNames: ['radio-group']
groupName: (->
change: ->
@set 'value', @$('input:checked').val()
updateSelected: (->
@$("input[value=#{@get('value')}]")[0].checked = true
).observes 'value'
didInsertElement: -> @updateSelected()
{{#each button in view.content}}
<input type="radio" {{bindAttr value="button.value"}} {{bindAttr name="view.groupName"}}/>
<a href="#" {{action toggleColumnGroup}} {{bindAttr class=":group-column isChoosingGroup"}}>
Group by
{{#if isChoosingGroup}}
{{view App.RadioButtonGroup
<a href="#" {{action toggleColumnSelect}} {{bindAttr class=":choose-columns isChoosingColumns"}}>
Choose columns
{{#if isChoosingColumns}}
{{#if groupColumn}}
<em>Can't change columns when grouping</em>
{{#each allColumns}}
{{view Em.Checkbox checkedBinding=isVisible
<span {{action sortByColumn view.content}} {{bindAttr class="view.isFilterable:filter-header-text view.isSortable"}}>
{{#if view.isSortable}}
<span {{bindAttr class=":sort-direction view.isSorting::inactive"}}>
{{#if view.isSortingAsc}}
{{#if view.isSortingDesc}}
{{#unless view.isSorting}}
<span class="header-cell-name">{{view.content.headerCellName}}</span>
{{#if view.isFilterable}}
{{view App.FilterField valueBinding="view.content.filterString"
{{#if title}}
{{#if hasColumnSelect}}
{{view App.Table.ColumnSelectView}}
{{#if isGroupable}}
{{view App.Table.ColumnGroupView}}
<div class="presentation-container">
{{view Ember.Table.TablesContainer}}
{{#if isDownloadable}}
<a href='#' {{action downloadCsv}} class='csv-download'>
{{#if isDownloading}}
Downloading - please wait&hellip;
Download as CSV / Excel
#= require util/compare
#= require click_elsewhere
#= require util/base64encode
App.Table = Ember.View.extend
contextBinding: 'controller'
templateName: 'table/table'
classNames: 'extended-table'
App.Table.ColumnSelectView = Em.View.extend Em.ClickElsewhere,
templateName: 'table/column_select'
classNames: ['column-select']
onClickElsewhere: ->
@set 'controller.isChoosingColumns', false
App.Table.ColumnGroupView = Em.View.extend Em.ClickElsewhere,
templateName: 'table/column_group'
classNames: ['column-group']
onClickElsewhere: ->
@set 'controller.isChoosingGroup', false
App.Table.SwankyHeaderCell = Ember.Table.HeaderCell.extend
templateName: 'table/swanky_header_cell'
isSortableBinding: 'content.isSortable'
isFilterableBinding: 'content.isFilterable'
sortAscendingBinding: 'controller.sortAscending'
isSorting: (->
@get('controller.sortColumn') is @get('content')
).property 'controller.sortColumn'
isSortingAsc: (->
@get('isSorting') and @get('sortAscending')
).property 'isSorting', 'sortAscending'
isSortingDesc: (->
@get('isSorting') and !@get('sortAscending')
).property 'isSorting', 'sortAscending'
App.Table.ClickableTableCell = Ember.Table.TableCell.extend
classNames: ['clickable']
click: (event) ->
@get('content').click @get('row')
App.Table.ColumnDefinition = Ember.Table.ColumnDefinition.extend
isSortable: false
isFilterable: false
isVisible: true
isGroupable: false
headerCellViewClass: 'App.Table.SwankyHeaderCell'
filterPresent: (->
!Em.isEmpty @get('filterString')
).property 'filterString'
observeFilterValue: (->
@controller.updateFilters @contentPath, @get('filterString')
).observes 'filterString'
isLastVisibleColumn: (->
@get('controller.columns.length') < 2 and @get('isVisible')
).property 'controller.columns.@each', 'isVisible'
isDisabled: (->
@get('controller.groupColumn')? or @get('isLastVisibleColumn')
).property 'controller.groupColumn', 'isLastVisibleColumn'
isGroupedColumn: (->
@get('controller.groupColumn') is @
).property 'controller.groupColumn'
App.SwankyTableController = Ember.Table.TableController.extend
# Extensions for general api improvement or multiple extra features
hasFooter: no
columnsByPath: {}
createColumn: (options) ->
col = App.Table.ColumnDefinition.create options
col.controller = @
col.tableCellViewClass = 'App.Table.ClickableTableCell'
col.contentPath ||= col.headerCellName.underscore()
@columnsByPath[col.contentPath] = col unless options.noPath
columns: (->
columns = if @get 'groupColumn'
@get 'groupColumns'
@get 'allColumns'
columns.filterProperty 'isVisible'
).property 'allColumns.@each.isVisible', 'groupColumns.@each.isVisible'
getCellContent: (key) ->
column = @columnsByPath[key]
column.getCellContent.bind column
# Extensions for downloading
downloadCsv: ->
@set 'isDownloading', true @, ->
# build the csv after a timeout
# to allow the ui time to update
@set 'isDownloading', false
, 300
_doDownload: ->
columns = @get('columns')
rows = []
# headers
headers = columns.mapProperty 'headerCellName'
# excel is amazingly stupid
if /^ID/.test headers[0]
headers[0] = " " + headers[0]
rows.push headers
# body
@get('sortedContent').forEach (row) ->
rows.push (column) ->
column.getCellContent row
csv = d3.csv.formatRows rows
a = document.createElement("a")
a.href = "data:text/csv;base64," + App.encode64(csv) = "App Data Download.csv"
document.body.appendChild a
document.body.removeChild a
# Extensions for filtering
filters: {}
filterColumn: (view) ->
view.toggleProperty 'isFiltering'
unless view.get 'isFiltering'
view.set 'content.filterString', null
hideFilter: (filterView) ->
filterView.get('parentView').toggleProperty 'isFiltering'
updateFilters: (path, value) ->
if Em.isEmpty value
delete @filters[path]
@filters[path] = value.toLowerCase()
@set 'filtersKey', $.param(@filters)
filteredContent: (->
if Em.isEmpty(Em.keys(@filters)) or @get('groupColumn')?
return @get('groupedContent').toArray()
@get('groupedContent').filter (row) =>
shouldInclude = true
for own key, value of @filters
cellContent = @getCellContent(key)(row)
unless ~cellContent.toString().toLowerCase().indexOf(value)
shouldInclude = false
).property 'groupedContent.@each', 'filtersKey', 'groupColumn'
# Extensions for column selection
hasColumnSelect: true
toggleColumnSelect: ->
@toggleProperty 'isChoosingColumns'
init: ->
if @get('hasColumnSelect') and !@get('allColumns')?
throw "Please specify allColumns not columns so that column hiding works"
firstSortable = @get('columns').findProperty 'isSortable'
@sortByColumn firstSortable if firstSortable?
# Extensions for sorting
sortByColumn: (column) ->
return unless column.isSortable
if column is @get('sortColumn')
@toggleProperty 'sortAscending'
@set 'sortAscending', !column.initialSortDesc
sortColumn: column
_tableScrollTop: 0
getCellSort: (column) ->
if column.getSortValue
column.getSortValue.bind column
column.getCellContent.bind column
sortedContent: (->
return @get 'filteredContent' if Em.isEmpty @get('sortColumn')
getSortValue = @getCellSort @get('sortColumn')
compare = @get('sortAscending')
result = @get('filteredContent').sort (a, b) ->
compare getSortValue(a), getSortValue(b)
).property 'filteredContent.@each', 'sortColumn', 'sortAscending'
bodyContent: (->
@_super().set 'content', @get('sortedContent')
).property 'content', 'tableRowClass', 'sortedContent.@each'
# Extensions for grouping
groupColumnIndex: 'none'
isGroupable: (->
@get('allColumns').findProperty 'isGroupable'
).property 'allColumns.@each'
groupColumns: (->
column = @get('groupColumn')
return [] unless column?
group = @createColumn
isSortable: true
headerCellName: column.get('headerCellName')
contentPath: 'groupValue'
getSortValue: (row) -> row.get('sortValue')
count = @createColumn
isSortable: true
headerCellName: "Count"
contentPath: 'countValue'
columns = [group, count]
@get('allColumns').forEach (column) =>
if column.groupValue?
columns.push @createColumn
noPath: true
headerCellName: column.get('headerCellName')
isSortable: true
getSortValue: (row) ->
if column.groupSortValue?
column.groupSortValue(row.rowsValue || [])
@getCellContent row
getCellContent: (row) ->
column.groupValue(row.rowsValue || [])
).property 'allColumns.@each', 'groupColumn'
groupedContent: (->
column = @get('groupColumn')
return @get 'content' unless column?
sortFunc = @getCellSort column
sortValues = {}
groups = _(@get('content').toArray()).groupBy (row) =>
key = column.getCellContent row
sortValues[key] ||= sortFunc(row)
Em.keys(groups).map (key) =>
rows = groups[key]
groupValue: key
sortValue: sortValues[key]
countValue: rows.length
rowsValue: rows
).property 'content.@each', 'groupColumn'
groupableColumns: (->
@get('allColumns').filterProperty 'isGroupable'
).property 'allColumns.@each'
groupRadioButtons: (->
buttons = [{label: "None", value: 'none'}]
@get('groupableColumns').forEach (column, i) ->
label: column.get('headerCellName')
value: i
).property 'groupableColumns.@each'
groupColumn: (->
index = parseInt @get('groupColumnIndex'), 10
return null if isNaN index
).property 'groupColumnIndex'
toggleColumnGroup: ->
@toggleProperty 'isChoosingGroup'
#Usage example:
# In the template:
# {{view App.Table controllerBinding="tableController"}}
# define your table controller like this:
App.MyTableController = App.SwankyTableController.extend
isDownloadable: true
# Note you have to define allColumns not columns now
allColumns: (->
col1 = @createColumn
isSortable: true
isFilterable: true
isGroupable: true
columnWidth: 75
headerCellName: 'Column 1'
contentPath: 'somePath.toTheContent'
col2 = @createColumn
isSortable: true
isFilterable: true
columnWidth: 100
headerCellName: 'Column 2'
contentPath: 'somePath'
# clicking on a cell
click: (theContentOfTheCell) ->
@controller.transitionToRoute 'aRoute', theContentOfTheCell
col3 = @createColumn
isFilterable: true
isSortable: true
# The row will sort by this value
getSortValue: (row) -> (row.get('title') || "").toLowerCase()
# contentPath is just a shortcut for getCellContent: (row) -> row.get(path)
contentPath: 'title'
columnWidth: 200
headerCellName: 'Title'
col4 = @createColumn
columnWidth: 100
headerCellName: "Total"
isSortable: true
initialSortDesc: true
getSortValue: (row) -> row.get('total')
getCellContent: (row) -> moneyFormat(row.get('total'))
groupSortValue: (rows) ->
# given a set of grouped rows, how should you sort that group
_(rows).sum (row) -> row.get('total')
groupValue: (rows) ->
# Given a set of grouped rows, how should you display that group
moneyFormat @groupSortValue(rows)
[col1, col2, col3, col4]
# In the controller:
tableController: (->
title: 'My Title'
target: @
contentBinding: 'target.tableRows'
tableRows: (->
# This should return the objects for the table
foo: 1, total: 2
foo: 1, total: 2

This comment has been minimized.

Copy link

@adorum adorum commented May 22, 2015

Hi, I am very interested in your Ember table extensions but I am not very familiar with coffeescript. How should I proceed to use the extension. Currently I am using Ember table as it is, just ember-table.js and ember-table.css grunt dist output. What is a way to put this 2 things together, your extensions and ember table?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.