Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
988b91a
add dummy application example
Mar 13, 2025
36498d2
tutorial example: add specific info for working pagination
Mar 13, 2025
47aa470
fix `@hasMenu` config (renamed to @showMenu)
Mar 13, 2025
a37efad
cleanup example route
Mar 13, 2025
46e0571
cleanup controller.js: remove unused functions from view
Mar 13, 2025
a0068a1
specify possibility to pass pagination info in `@meta.pagination`
Mar 13, 2025
442e6f2
remove code for `@searchDebounceTime` + cleanup of debounce logic
Mar 13, 2025
5249bf1
add `@initialSelection` to set up the initial selection in a table
Mar 14, 2025
968a4e9
clarify `_` converted to space syntax for `@links`
Mar 19, 2025
75f31d4
add if/else for handling icon-links in example raw-data-table, making…
Mar 19, 2025
61ce7c3
fix typo in documentation of splitDefinitions
Mar 19, 2025
2d96bdf
move `@rowLink` above `@onClickRow` so the text referring to each oth…
Mar 19, 2025
67db96e
Also allow passing an array to `@customHeaders`, `@customFields` and …
Mar 19, 2025
4e8bf31
specify in readme that `@fields` defines the order of fields in the t…
Mar 19, 2025
b39c728
remove duplicate attribute hash in row.hbs
Mar 19, 2025
6501136
Allow passing components directly as custom fields via `@customFieldC…
Mar 19, 2025
1d04004
add `@selectionProperty` to support row selection for non-ember-data …
Mar 19, 2025
966deb5
specify `fields` item is an array, not object
Mar 20, 2025
ff30b57
rephrase isAscending/isDescending
Mar 20, 2025
674b56c
reword explanation and some terms in readme and code
Mar 26, 2025
47d4e9e
refactor away `@wrapper` + refactor rowLink(Model)/onRowClicked
Mar 27, 2025
1a2e8fe
allow passing arrays/objects directly to `@fields`, `@links`, `@custo…
Apr 1, 2025
7199433
add more demo routes for multiple types of example tables
Apr 2, 2025
1a8ef16
readme fix: make more clear that autosearch is enabled by default
Apr 2, 2025
ddac326
rewrite old tests for new raw-data-table
May 15, 2025
06e199f
remove depenency to ember-data and reduce packages that were not needed
May 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
468 changes: 268 additions & 200 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions addon/components/data-table.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
content=@content
noDataMessage=this.noDataMessage
enableSelection=@enableSelection
selectionProperty=@selectionProperty
enableLineNumbers=@enableLineNumbers
onClickRow=@onClickRow
sort=this.sort
Expand Down
119 changes: 59 additions & 60 deletions addon/components/data-table.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { isEmpty } from '@ember/utils';
import Component from '@glimmer/component';
import { typeOf } from '@ember/utils';
import { toComponentSpecifications, splitDefinitions } from "../utils/string-specification-helpers";
import { toComponentSpecifications, definitionsToArray } from "../utils/string-specification-helpers";
import attributeToSortParams from "../utils/attribute-to-sort-params";
import get from '../utils/get';

const DEFAULT_DEBOUNCE_TIME = 2000;
export default class DataTable extends Component {
@tracked _selection = undefined;

get filter() {
return this.args.filter !== undefined
? this.args.filter
: this.args.view?.filter;
return this.args.filter;
}

get sort() {
return this.args.sort !== undefined
? this.args.sort
: this.args.view?.sort;
return this.args.sort;
}

get selection() {
if (this._selection === undefined && this.args.selection === undefined)
if (this._selection === undefined && this.args.initialSelection === undefined)
return [];
else if (this._selection !== undefined)
return this._selection;
else
return this.args.selection;
return this.args.initialSelection;
}

set selection(newSelection) {
this._selection = newSelection; // also trigers dependent properties
this._selection = newSelection; // also triggers dependent properties
}

get noDataMessage() {
Expand All @@ -40,24 +39,19 @@ export default class DataTable extends Component {
}

get isLoading() {
return this.args.isLoading !== undefined
? this.args.isLoading
: this.args.view?.isLoading;
return this.args.isLoading;
}

/**
* Calculates the search debounce time.
*
* If the user supplies searchDebounceTime, that is what we should
* use. A shorthand form is supported in which the user supplies a
* A shorthand form is supported in which the user supplies a
* number to autoSearch in which case we use that. This would not
* work with 0 (which is a strange debounce time in itself) so this
* option exists for now.
*/
get searchDebounceTime() {
return this.args.searchDebounceTime === undefined
? isNaN(this.args.autoSearch) ? 2000 : this.args.autoSearch
: this.args.searchDebounceTime;
return isNaN(this.args.autoSearch) ? DEFAULT_DEBOUNCE_TIME : this.args.autoSearch
}

get enableSelection() {
Expand All @@ -68,31 +62,24 @@ export default class DataTable extends Component {
return this.selection.length === 0;
}

get enableSizes() {
return this.args.enableSizes === undefined ? true : this.args.enableSizes;
}

get page() {
const page = this.args.page !== undefined
? this.args.page
: this.args.view?.page;
return page || 0;
return this.args.page || 0;
}

get size() {
if ( this.args.size )
return this.args.size;
else if ( this.args.view?.size )
return this.args.view.size;
else
return 5;
return this.args.size || 5;
}

get sizeOptions() {
if (!this.enableSizes) {
const sizeOptions =
this.args.sizes === undefined
? [5, 10, 25, 50, 100]
: definitionsToArray(this.args.sizes).map((nrOrStr) =>
parseInt(nrOrStr)
);
if (isEmpty(sizeOptions)) {
return null;
} else {
const sizeOptions = this.args.sizes || [5, 10, 25, 50, 100];
if (!sizeOptions.includes(this.size) && this.size) {
sizeOptions.push(this.size);
}
Expand Down Expand Up @@ -124,13 +111,10 @@ export default class DataTable extends Component {
}

get fieldsWithMeta() {
const fields = this.args.fields;

if (typeOf(fields) === 'string') {
return toComponentSpecifications(fields, [{raw: "attribute"},{name: "label", default: "attribute"}]);
} else {
return fields || [];
}
return toComponentSpecifications(this.args.fields, [
{ raw: 'attribute' },
{ name: 'label', default: 'attribute' },
]);
}

attributeToSortParams(attribute) {
Expand All @@ -156,22 +140,44 @@ export default class DataTable extends Component {
hasCustomHeader: hasCustomHeader
|| this.customHeaders.includes(attribute),
isCustom: isCustom
|| this.customFields.includes(attribute)
|| this.customFields.includes(attribute),
customFieldComponent: this.customFieldComponents[attribute] || null,
customHeaderComponent: this.customHeaderComponents[attribute] || null
}));
}

get customHeaders() {
return splitDefinitions(this.args.customHeaders);
const headers = this.args.customHeaders;
if(typeOf(headers) === "object") {
return Object.keys(headers).filter(attr => isEmpty(headers[attr]));
} else {
return definitionsToArray(headers);
}
}

get customFields() {
return splitDefinitions(this.args.customFields);
const fields = this.args.customFields;
if(typeOf(fields) === "object") {
return Object.keys(fields).filter(attr => isEmpty(fields[attr]));
} else {
return definitionsToArray(fields);
}
}

get customFieldComponents() {
const fields = this.args.customFields;
return typeOf(fields) === "object"? fields : {};
}

get customHeaderComponents() {
const headers = this.args.customHeaders;
return typeOf(headers) === "object"? headers : {};
}

get sortableFields() {
const sortableFields = this.args.sortableFields;
if (sortableFields || sortableFields === "")
return splitDefinitions(sortableFields);
return definitionsToArray(sortableFields);
else
// default: all fields are sortable
return null;
Expand All @@ -185,10 +191,7 @@ export default class DataTable extends Component {

@action
updatePageSize(size) {
const updater = this.args.updatePageSize !== undefined
? this.args.updatePageSize
: this.args.view?.updatePageSize;

const updater = this.args.updatePageSize;
if( !updater ) {
console.error(`Could not update page size to ${size} because @updatePageSize was not supplied to data table`);
} else {
Expand All @@ -199,7 +202,7 @@ export default class DataTable extends Component {

@action
updateFilter(filter) {
const updater = this.args.updateFilter || this.args.view?.updateFilter;
const updater = this.args.updateFilter;

if( !updater ) {
console.error(`Could not update filter to '${filter}' because @updateFilter was not supplied to data table`);
Expand All @@ -211,10 +214,7 @@ export default class DataTable extends Component {

@action
updateSort(sort) {
const updater = this.args.updateSort !== undefined
? this.args.updateSort
: this.args.view?.updateSort;

const updater = this.args.updateSort;
if( !updater ) {
console.error(`Could not update sorting to '${sort}' because @updateSort was not supplied to data table`);
} else {
Expand All @@ -225,10 +225,7 @@ export default class DataTable extends Component {

@action
updatePage(page) {
const updater = this.args.updatePage !== undefined
? this.args.updatePage
: this.args.view?.updatePage;

const updater = this.args.updatePage;
if( !updater ) {
console.error(`Could not update page to ${page} because @updatePage was not supplied to data table`);
} else {
Expand All @@ -238,11 +235,13 @@ export default class DataTable extends Component {

@action
addItemToSelection(item) {
this.selection = [...new Set([item, ...this.selection])];
this.removeItemFromSelection(item); // in case the item was already selected
this.selection = [...this.selection, item]; // create new array to trigger setter if `selection`
}
@action
removeItemFromSelection(item) {
this.selection = this.selection.filter((x) => x !== item);
const byPath = this.args.selectionProperty;
this.selection = this.selection.filter((x) => get(x, byPath) !== get(item, byPath));
}
@action
clearSelection() {
Expand Down
13 changes: 7 additions & 6 deletions addon/components/data-table/data-cell.hbs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{{!-- Used in: data-table/data-cells --}}
{{yield (hash
firstColumn=@firstColumn
otherColumns=@otherColumns
item=@wrapper.item
rowLink=@wrapper.rowLink
rowLinkModel=@wrapper.rowLinkModel
firstColumnField=@firstColumnField
otherColumnFields=@otherColumnFields
item=@item
rowLink=@rowLink
rowLinkModel=@rowLinkModel
rowClicked=@rowClicked
label=@column.label
fields=@fields
isCustom=this.isCustom
hasCustomFields=this.hasCustomFields
attribute=@column.attribute
renderCustomBlock=this.renderCustomBlock
value=(get @wrapper.item @column.attribute))}}
value=(get @item @column.attribute))}}
22 changes: 12 additions & 10 deletions addon/components/data-table/data-cells.hbs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{{!-- Used in: data-table/row --}}
{{yield (hash
fields=@dataTable.fields
firstColumn=this.firstColumn
otherColumns=this.otherColumns
wrapper=@wrapper
item=@wrapper.item
rowLink=@wrapper.rowLink
rowLinkModel=@wrapper.rowLinkModel
firstColumnField=this.firstColumnField
otherColumnFields=this.otherColumnFields
item=@item
rowLink=@rowLink
rowLinkModel=@rowLinkModel
rowClicked=@rowClicked
fields=@fields
DataCell=(component
"data-table/data-cell"
firstColumn=this.firstColumn
otherColumns=this.otherColumns
wrapper=@wrapper
firstColumnField=this.firstColumnField
otherColumnFields=this.otherColumnFields
item=@item
rowLink=@rowLink
rowLinkModel=@rowLinkModel
rowClicked=@rowClicked
fields=@fields))}}
4 changes: 2 additions & 2 deletions addon/components/data-table/data-cells.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Component from '@glimmer/component';

export default class DataTableDataCellsComponent extends Component {
get firstColumn() {
get firstColumnField() {
return this.args.fields?.[0] || null;
}

get otherColumns() {
get otherColumnFields() {
if (this.args.fields?.length) {
let [, ...fields] = this.args.fields;
return fields;
Expand Down
7 changes: 4 additions & 3 deletions addon/components/data-table/data-table-content-body.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
isLoading=@dataTable.isLoading
content=@content
offset=this.offset
wrappedItems=this.wrappedItems
enableLineNumbers=@enableLineNumbers
hasClickRowAction=(and (or @onClickRow @rowLink) true)
onClickRow=@onClickRow
toggleSelected=this.updateSelection
selection=@dataTable.selection
enableSelection=@enableSelection
Expand All @@ -19,10 +17,13 @@
dataTable=@dataTable
enableLineNumbers=@enableLineNumbers
enableSelection=@enableSelection
selectionProperty=@selectionProperty
selection=@dataTable.selection
offset=this.offset
hasClickRowAction=(and (or @onClickRow @rowLink) true)
onClickRow=this.onClickRow
onClickRow=@onClickRow
linkedRoutes=@linkedRoutes
rowLink=@rowLink
rowLinkModelProperty=@rowLinkModelProperty
fields=@fields
toggleSelected=this.updateSelection))}}
39 changes: 3 additions & 36 deletions addon/components/data-table/data-table-content-body.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { cached } from '@glimmer/tracking';
import { get } from '@ember/object';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import Component from '@glimmer/component';

export default class DataTableContentBodyComponent extends Component {
@service router;

get offset() {
var offset = 1; //to avoid having 0. row
Expand All @@ -17,40 +13,11 @@ export default class DataTableContentBodyComponent extends Component {
return offset;
}

@cached
get wrappedItems() {
const selection = this.args.dataTable.selection || []; // TODO: should the dataTable ensure this is an array?
const content = this.args.content;
return content.map((item) => {
return {
item: item,
isSelected: selection.includes(item),
rowLink: this.args.rowLink,
rowLinkModel: this.rowLinkModel(item)
};
});
}

rowLinkModel(row) {
return this.args.rowLinkModelProperty
? get(row, this.args.rowLinkModelProperty)
: row;
}

@action
updateSelection(selectedWrapper, event) {
updateSelection(item, event) {
if( event.target.checked )
this.args.dataTable.addItemToSelection(selectedWrapper.item);
this.args.dataTable.addItemToSelection(item);
else
this.args.dataTable.removeItemFromSelection(selectedWrapper.item);
}

@action
onClickRow(row) {
if ( this.args.onClickRow ) {
this.args.onClickRow(...arguments);
} else if ( this.args.rowLink ) {
this.router.transitionTo( this.args.rowLink, this.rowLinkModel(row) );
}
this.args.dataTable.removeItemFromSelection(item);
}
}
Loading