├── app ├── .gitkeep ├── components │ ├── data-table.js │ ├── text-search.js │ ├── th-sortable.js │ ├── data-table-menu.js │ ├── number-pagination.js │ ├── data-table-content.js │ ├── data-table-content-body.js │ ├── data-table-content-header.js │ ├── data-table-menu-general.js │ ├── data-table-menu-selected.js │ └── default-data-table-content-body.js └── styles │ └── ember-data-table.scss ├── addon ├── .gitkeep ├── templates │ └── components │ │ ├── data-table-menu-general.hbs │ │ ├── th-sortable.hbs │ │ ├── data-table-menu.hbs │ │ ├── data-table-menu-selected.hbs │ │ ├── text-search.hbs │ │ ├── data-table-content-header.hbs │ │ ├── default-data-table-content-body.hbs │ │ ├── data-table-content.hbs │ │ ├── data-table-content-body.hbs │ │ ├── data-table.hbs │ │ └── number-pagination.hbs ├── mixins │ ├── default-query-params.js │ ├── route.js │ └── serializer.js └── components │ ├── data-table-menu-general.js │ ├── data-table-menu.js │ ├── data-table-content.js │ ├── data-table-content-header.js │ ├── data-table-menu-selected.js │ ├── default-data-table-content-body.js │ ├── text-search.js │ ├── data-table-content-body.js │ ├── number-pagination.js │ ├── data-table.js │ └── th-sortable.js ├── vendor └── .gitkeep ├── tests ├── helpers │ ├── .gitkeep │ ├── start-app.js │ └── module-for-acceptance.js ├── unit │ ├── .gitkeep │ └── mixins │ │ ├── serializer-test.js │ │ ├── default-query-params-test.js │ │ └── route-test.js ├── integration │ ├── .gitkeep │ └── components │ │ ├── text-search-test.js │ │ ├── th-sortable-test.js │ │ ├── number-pagination-test.js │ │ ├── data-table-menu-test.js │ │ ├── data-table-test.js │ │ ├── data-table-content-test.js │ │ ├── default-data-table-content-body-test.js │ │ ├── data-table-menu-general-test.js │ │ ├── data-table-menu-selected-test.js │ │ ├── data-table-content-header-test.js │ │ └── data-table-content-body-test.js ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── application.js │ │ ├── styles │ │ │ └── app.scss │ │ ├── router.js │ │ ├── app.js │ │ ├── index.html │ │ └── templates │ │ │ └── application.hbs │ ├── public │ │ └── robots.txt │ └── config │ │ ├── optional-features.json │ │ ├── ember-cli-update.json │ │ ├── targets.js │ │ └── environment.js ├── test-helper.js ├── .jshintrc └── index.html ├── .npmrc ├── _config.yml ├── .watchmanconfig ├── .bowerrc ├── blueprints └── ember-data-table │ ├── files │ └── app │ │ ├── styles │ │ └── app.scss │ │ └── serializers │ │ └── application.js │ └── index.js ├── .prettierrc.js ├── .template-lintrc.js ├── index.js ├── config ├── environment.js ├── release.js └── ember-try.js ├── .ember-cli ├── .prettierignore ├── .eslintignore ├── .editorconfig ├── .gitignore ├── .npmignore ├── .jshintrc ├── testem.js ├── CONTRIBUTING.md ├── ember-cli-build.js ├── LICENSE.md ├── .eslintrc.js ├── MODULE_REPORT.md ├── .travis.yml ├── package.json └── README.md /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-dinky -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /blueprints/ember-data-table/files/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import 'ember-data-table'; 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/components/data-table.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/data-table'; 2 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /app/components/text-search.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/text-search'; 2 | -------------------------------------------------------------------------------- /app/components/th-sortable.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/th-sortable'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name, 5 | }; 6 | -------------------------------------------------------------------------------- /app/components/data-table-menu.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/data-table-menu'; 2 | -------------------------------------------------------------------------------- /app/components/number-pagination.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/number-pagination'; 2 | -------------------------------------------------------------------------------- /app/components/data-table-content.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/data-table-content'; 2 | -------------------------------------------------------------------------------- /addon/templates/components/data-table-menu-general.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.data-table.selectionIsEmpty}} 2 | {{yield}} 3 | {{/if}} 4 | -------------------------------------------------------------------------------- /app/components/data-table-content-body.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/data-table-content-body'; 2 | -------------------------------------------------------------------------------- /app/components/data-table-content-header.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/data-table-content-header'; 2 | -------------------------------------------------------------------------------- /app/components/data-table-menu-general.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/data-table-menu-general'; 2 | -------------------------------------------------------------------------------- /app/components/data-table-menu-selected.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/data-table-menu-selected'; 2 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (/* environment, appConfig */) { 4 | return {}; 5 | }; 6 | -------------------------------------------------------------------------------- /app/components/default-data-table-content-body.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-data-table/components/default-data-table-content-body'; 2 | -------------------------------------------------------------------------------- /addon/mixins/default-query-params.js: -------------------------------------------------------------------------------- 1 | import Mixin from '@ember/object/mixin'; 2 | 3 | export default Mixin.create({ 4 | page: 0, 5 | size: 10, 6 | filter: '', 7 | }); 8 | -------------------------------------------------------------------------------- /addon/templates/components/th-sortable.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if this.order}}[{{this.order}}]{{/if}} 3 | {{this.label}} 4 | 5 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import 'ember-data-table'; 2 | 3 | // Blockquote from MaterializeCSS 4 | blockquote { 5 | margin: 20px 0; 6 | padding-left: 1.5rem; 7 | border-left: 5px solid #F44336; 8 | } 9 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /addon/components/data-table-menu-general.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import layout from '../templates/components/data-table-menu-general'; 3 | 4 | export default Component.extend({ 5 | layout, 6 | }); 7 | -------------------------------------------------------------------------------- /addon/templates/components/data-table-menu.hbs: -------------------------------------------------------------------------------- 1 | {{yield (hash 2 | general=(component "data-table-menu-general" data-table=this.data-table) 3 | selected=(component "data-table-menu-selected" data-table=this.data-table) 4 | )}} 5 | -------------------------------------------------------------------------------- /addon/components/data-table-menu.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import layout from '../templates/components/data-table-menu'; 3 | 4 | export default Component.extend({ 5 | layout, 6 | classNames: ['data-table-menu'], 7 | }); 8 | -------------------------------------------------------------------------------- /blueprints/ember-data-table/files/app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import DataTableSerializerMixin from 'ember-data-table/mixins/serializer'; 3 | 4 | export default DS.JSONAPISerializer.extend(DataTableSerializerMixin, { 5 | 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'dummy/config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () {}); 10 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /addon/templates/components/data-table-menu-selected.hbs: -------------------------------------------------------------------------------- 1 | {{#unless this.data-table.selectionIsEmpty}} 2 | {{this.selectionCount}} item(s) selected 3 | 4 | {{yield (slice 0 this.selectionCount this.data-table.selection) this.data-table}} 5 | {{/unless}} 6 | -------------------------------------------------------------------------------- /addon/components/data-table-content.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { alias } from '@ember/object/computed'; 3 | import layout from '../templates/components/data-table-content'; 4 | 5 | export default Component.extend({ 6 | layout, 7 | classNames: ['data-table-content'], 8 | tableClass: alias('data-table.tableClass'), 9 | }); 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /package.json.ember-try 23 | -------------------------------------------------------------------------------- /addon/templates/components/text-search.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.auto}} 2 | 3 | {{else}} 4 | 5 | 6 | {{/if}} 7 | -------------------------------------------------------------------------------- /blueprints/ember-data-table/index.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | description: '', 4 | 5 | normalizeEntityName: function () {}, 6 | 7 | // locals: function(options) { 8 | // // Return custom template variables here. 9 | // return { 10 | // foo: options.entity.options.foo 11 | // }; 12 | // } 13 | 14 | afterInstall: function () {}, 15 | }; 16 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /addon/components/data-table-content-header.js: -------------------------------------------------------------------------------- 1 | import { oneWay } from '@ember/object/computed'; 2 | import { alias } from '@ember/object/computed'; 3 | import Component from '@ember/component'; 4 | import layout from '../templates/components/data-table-content-header'; 5 | 6 | export default Component.extend({ 7 | layout, 8 | tagName: 'thead', 9 | sort: alias('data-table.sort'), 10 | fields: oneWay('data-table.parsedFields'), 11 | }); 12 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /addon/templates/components/data-table-content-header.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if this.enableSelection}} 3 | {{!-- Checkbox --}} 4 | {{/if}} 5 | {{#if this.enableLineNumbers}} 6 | {{!-- Linenumbers --}} 7 | {{/if}} 8 | {{#if (has-block)}} 9 | {{yield}} 10 | {{else}} 11 | {{#each this.fields as |field|}} 12 | {{th-sortable field=field label=field currentSorting=this.sort}} 13 | {{/each}} 14 | {{/if}} 15 | 16 | -------------------------------------------------------------------------------- /tests/unit/mixins/serializer-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import SerializerMixin from 'ember-data-table/mixins/serializer'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Mixin | serializer', function () { 6 | // Replace this with your real tests. 7 | test('it works', function (assert) { 8 | let SerializerObject = EmberObject.extend(SerializerMixin); 9 | let subject = SerializerObject.create(); 10 | assert.ok(subject); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.28.5", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [] 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | npm-debug.log 20 | yarn-error.log 21 | testem.log 22 | 23 | # ember-try 24 | .node_modules.ember-try/ 25 | bower.json.ember-try 26 | package.json.ember-try 27 | 28 | *~ 29 | package-lock.json 30 | -------------------------------------------------------------------------------- /tests/unit/mixins/default-query-params-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import DefaultQueryParamsMixin from 'ember-data-table/mixins/default-query-params'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Mixin | default query params', function () { 6 | // Replace this with your real tests. 7 | test('it works', function (assert) { 8 | let DefaultQueryParamsObject = EmberObject.extend(DefaultQueryParamsMixin); 9 | let subject = DefaultQueryParamsObject.create(); 10 | assert.ok(subject); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /addon/templates/components/default-data-table-content-body.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.firstColumn}} 2 | {{#if this.linkedRoute}} 3 | 4 | 5 | {{get this.item this.firstColumn}} 6 | 7 | 8 | {{else}} 9 | {{get this.item this.firstColumn}} 10 | {{/if}} 11 | {{/if}} 12 | {{#each this.otherColumns as |field|}} 13 | 14 | {{!-- This should be based on the type of the field --}} 15 | {{get this.item field}} 16 | 17 | {{/each}} 18 | {{yield}} 19 | 20 | -------------------------------------------------------------------------------- /addon/components/data-table-menu-selected.js: -------------------------------------------------------------------------------- 1 | import { reads } from '@ember/object/computed'; 2 | import Component from '@ember/component'; 3 | import layout from '../templates/components/data-table-menu-selected'; 4 | 5 | export default Component.extend({ 6 | layout, 7 | init: function () { 8 | this._super(...arguments); 9 | this.set('data-table.enableSelection', true); 10 | }, 11 | selectionCount: reads('data-table.selection.length'), 12 | actions: { 13 | clearSelection() { 14 | this.get('data-table').clearSelection(); 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { merge } from '@ember/polyfills'; 3 | import Application from '../../app'; 4 | import config from '../../config/environment'; 5 | 6 | export default function startApp(attrs) { 7 | let attributes = merge({}, config.APP); 8 | attributes.autoboot = true; 9 | attributes = merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | return run(() => { 12 | let application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | return application; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.gitignore 18 | /.prettierignore 19 | /.prettierrc.js 20 | /.template-lintrc.js 21 | /.travis.yml 22 | /.watchmanconfig 23 | /bower.json 24 | /config/ember-try.js 25 | /CONTRIBUTING.md 26 | /ember-cli-build.js 27 | /testem.js 28 | /tests/ 29 | /yarn-error.log 30 | /yarn.lock 31 | .gitkeep 32 | 33 | # ember-try 34 | /.node_modules.ember-try/ 35 | /bower.json.ember-try 36 | /package.json.ember-try 37 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esversion": 6, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /tests/integration/components/text-search-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | text search', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | await render(hbs`{{text-search}}`); 14 | 15 | assert.dom('.data-table-search').exists({ count: 1 }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /config/release.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | // var RSVP = require('rsvp'); 3 | 4 | // For details on each option run `ember help release` 5 | module.exports = { 6 | // local: true, 7 | // remote: 'some_remote', 8 | // annotation: "Release %@", 9 | // message: "Bumped version to %@", 10 | // manifest: [ 'package.json', 'bower.json', 'someconfig.json' ], 11 | // publish: true, 12 | // strategy: 'date', 13 | // format: 'YYYY-MM-DD', 14 | // timezone: 'America/Los_Angeles', 15 | // 16 | // beforeCommit: function(project, versions) { 17 | // return new RSVP.Promise(function(resolve, reject) { 18 | // // Do custom things here... 19 | // }); 20 | // } 21 | }; 22 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/integration/components/th-sortable-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | th sortable', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | await render(hbs`{{th-sortable field='title'}}`); 14 | 15 | assert.dom('.sortable').exists({ count: 1 }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'rsvp'; 2 | import { module } from 'qunit'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | export default function (name, options = {}) { 7 | module(name, { 8 | beforeEach() { 9 | this.application = startApp(); 10 | 11 | if (options.beforeEach) { 12 | return options.beforeEach.apply(this, arguments); 13 | } 14 | }, 15 | 16 | afterEach() { 17 | let afterEach = 18 | options.afterEach && options.afterEach.apply(this, arguments); 19 | return resolve(afterEach).then(() => destroyApp(this.application)); 20 | }, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd my-addon` 7 | * `npm install` 8 | 9 | ## Linting 10 | 11 | * `npm run lint` 12 | * `npm run lint:fix` 13 | 14 | ## Running tests 15 | 16 | * `ember test` – Runs the test suite on the current Ember version 17 | * `ember test --server` – Runs the test suite in "watch mode" 18 | * `ember try:each` – Runs the test suite against multiple Ember versions 19 | 20 | ## Running the dummy application 21 | 22 | * `ember serve` 23 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 24 | 25 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 26 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | // Ember's browser support policy is changing, and IE11 support will end in 10 | // v4.0 onwards. 11 | // 12 | // See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy 13 | // 14 | // If you need IE11 support on a version of Ember that still offers support 15 | // for it, uncomment the code block below. 16 | // 17 | // const isCI = Boolean(process.env.CI); 18 | // const isProduction = process.env.EMBER_ENV === 'production'; 19 | // 20 | // if (isCI || isProduction) { 21 | // browsers.push('ie 11'); 22 | // } 23 | 24 | module.exports = { 25 | browsers, 26 | }; 27 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 2 | 3 | module.exports = function (defaults) { 4 | let app = new EmberAddon(defaults, { 5 | 'ember-cli-babel': { 6 | includePolyfill: true, 7 | }, 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | const { maybeEmbroider } = require('@embroider/test-setup'); 18 | return maybeEmbroider(app, { 19 | skipBabel: [ 20 | { 21 | package: 'qunit', 22 | }, 23 | ], 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /addon/components/default-data-table-content-body.js: -------------------------------------------------------------------------------- 1 | import { A } from '@ember/array'; 2 | import { computed } from '@ember/object'; 3 | import { oneWay } from '@ember/object/computed'; 4 | import Component from '@ember/component'; 5 | import layout from '../templates/components/default-data-table-content-body'; 6 | 7 | export default Component.extend({ 8 | layout, 9 | tagName: '', 10 | allFields: oneWay('data-table.parsedFields'), 11 | firstColumn: computed('data-table.parsedFields', function () { 12 | const parsedFields = A(this.get('data-table.parsedFields')); 13 | return parsedFields.get('firstObject'); 14 | }), 15 | otherColumns: computed('data-table.parsedFields', function () { 16 | let fields; 17 | [, ...fields] = this.get('data-table.parsedFields'); 18 | return fields; 19 | }), 20 | linkedRoute: oneWay('data-table.link'), 21 | }); 22 | -------------------------------------------------------------------------------- /tests/integration/components/number-pagination-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | number pagination', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | this.set('page', 0); 14 | this.set('links', { 15 | first: { number: 1 }, 16 | last: { number: 10 }, 17 | }); 18 | await render(hbs`{{number-pagination page=this.page links=this.links}}`); 19 | 20 | assert.dom('.data-table-pagination').exists({ count: 1 }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/integration/components/data-table-menu-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | data table menu', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | await render(hbs`{{data-table-menu}}`); 14 | 15 | assert.dom('*').hasText(''); 16 | 17 | // Template block usage: 18 | await render(hbs` 19 | {{#data-table-menu}} 20 | template block text 21 | {{/data-table-menu}} 22 | `); 23 | 24 | assert.dom('*').hasText('template block text'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/integration/components/data-table-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | data table', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | this.set('content', []); 14 | this.set('content.meta', { 15 | pagination: { 16 | first: { number: 1 }, 17 | last: { number: 10 }, 18 | }, 19 | }); 20 | 21 | await render(hbs`{{data-table content=this.content enableSizes=false}}`); 22 | 23 | assert.dom('.data-table-content').exists({ count: 1 }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/integration/components/data-table-content-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | data table content', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | await render(hbs`{{data-table-content}}`); 14 | assert 15 | .dom('table.data-table') 16 | .exists({ count: 1 }, 'displays 1 data table'); 17 | 18 | assert.dom('*').hasText(''); 19 | 20 | // Template block usage: 21 | await render(hbs` 22 | {{#data-table-content}} 23 | template block text 24 | {{/data-table-content}} 25 | `); 26 | 27 | assert.dom('*').hasText('template block text'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /addon/templates/components/data-table-content.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable table-groups }} 2 | 3 | {{#if (has-block)}} 4 | {{yield (hash 5 | header=(component "data-table-content-header" enableSelection=this.enableSelection enableLineNumbers=this.enableLineNumbers data-table=this.data-table) 6 | body=(component "data-table-content-body" content=this.content enableSelection=this.enableSelection enableLineNumbers=this.enableLineNumbers noDataMessage=this.noDataMessage onClickRow=(optional this.onClickRow) data-table=this.data-table) 7 | )}} 8 | {{else}} 9 | {{component "data-table-content-header" enableSelection=this.enableSelection enableLineNumbers=this.enableLineNumbers data-table=this.data-table}} 10 | {{component "data-table-content-body" content=this.content enableSelection=this.enableSelection enableLineNumbers=this.enableLineNumbers noDataMessage=this.noDataMessage onClickRow=(optional this.onClickRow) data-table=this.data-table}} 11 | {{/if}} 12 |
13 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "location", 6 | "setTimeout", 7 | "$", 8 | "-Promise", 9 | "define", 10 | "console", 11 | "visit", 12 | "exists", 13 | "fillIn", 14 | "click", 15 | "keyEvent", 16 | "triggerEvent", 17 | "find", 18 | "findWithAssert", 19 | "wait", 20 | "DS", 21 | "andThen", 22 | "currentURL", 23 | "currentPath", 24 | "currentRouteName" 25 | ], 26 | "node": false, 27 | "browser": false, 28 | "boss": true, 29 | "curly": true, 30 | "debug": false, 31 | "devel": false, 32 | "eqeqeq": true, 33 | "evil": true, 34 | "forin": false, 35 | "immed": false, 36 | "laxbreak": false, 37 | "newcap": true, 38 | "noarg": true, 39 | "noempty": false, 40 | "nonew": false, 41 | "nomen": false, 42 | "onevar": false, 43 | "plusplus": false, 44 | "regexp": false, 45 | "undef": true, 46 | "sub": true, 47 | "strict": false, 48 | "white": false, 49 | "eqnull": true, 50 | "esversion": 6, 51 | "unused": true 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /addon/templates/components/data-table-content-body.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.data-table.isLoading}} 2 | Loading... 3 | {{else}} 4 | {{#if this.content}} 5 | {{#each this.wrappedItems as |wrapper index|}} 6 | 7 | {{#if this.enableSelection}} 8 | 9 | 10 | 11 | {{/if}} 12 | {{#if this.enableLineNumbers}} 13 | {{add index this.offset}} 14 | {{/if}} 15 | {{#if (has-block)}} 16 | {{yield wrapper.item}} 17 | {{else}} 18 | {{default-data-table-content-body item=wrapper.item data-table=this.data-table}} 19 | {{/if}} 20 | 21 | {{/each}} 22 | {{else}} 23 |

{{this.noDataMessage}}

24 | {{/if}} 25 | {{/if}} 26 | -------------------------------------------------------------------------------- /tests/integration/components/default-data-table-content-body-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module( 7 | 'Integration | Component | default data table content body', 8 | function (hooks) { 9 | setupRenderingTest(hooks); 10 | 11 | test('it renders', async function (assert) { 12 | // Set any properties with this.set('myProperty', 'value'); 13 | // Handle any actions with this.on('myAction', function(val) { ... }); 14 | 15 | this.set('data-table', { 16 | parsedFields: ['firstName', 'lastName', 'age'], 17 | }); 18 | 19 | await render( 20 | hbs`{{default-data-table-content-body data-table=this.data-table}}` 21 | ); 22 | 23 | assert.dom().hasText(''); 24 | 25 | // Template block usage: 26 | await render(hbs` 27 | 28 | template block text 29 | 30 | `); 31 | assert.dom().hasText('template block text'); 32 | }); 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /tests/unit/mixins/route-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable ember/no-new-mixins,ember/no-mixins */ 2 | 3 | import EmberObject from '@ember/object'; 4 | import RouteMixin from 'ember-data-table/mixins/route'; 5 | import { module, test } from 'qunit'; 6 | 7 | module('Unit | Mixin | route', function () { 8 | test('it (deep) merges the response of mergeQueryOptions method with the query param options', function (assert) { 9 | assert.expect(2); 10 | 11 | let RouteObject = EmberObject.extend(RouteMixin, { 12 | modelName: 'test', 13 | mergeQueryOptions() { 14 | return { 15 | foo: 'bar', 16 | page: { 17 | size: 5, 18 | }, 19 | }; 20 | }, 21 | }); 22 | 23 | let mockStore = { 24 | query: (modelName, queryOptions) => { 25 | assert.strictEqual(modelName, 'test'); 26 | assert.deepEqual(queryOptions, { 27 | sort: 'name', 28 | page: { 29 | size: 5, 30 | number: 0, 31 | }, 32 | foo: 'bar', 33 | }); 34 | }, 35 | }; 36 | 37 | let mockRoute = RouteObject.create(); 38 | mockRoute.store = mockStore; 39 | mockRoute.model({ sort: 'name', page: 0, size: 20 }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true, 11 | }, 12 | }, 13 | plugins: ['ember'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended', 17 | 'plugin:prettier/recommended', 18 | ], 19 | env: { 20 | browser: true, 21 | }, 22 | rules: {}, 23 | overrides: [ 24 | // node files 25 | { 26 | files: [ 27 | './.eslintrc.js', 28 | './.prettierrc.js', 29 | './.template-lintrc.js', 30 | './ember-cli-build.js', 31 | './index.js', 32 | './testem.js', 33 | './blueprints/*/index.js', 34 | './config/**/*.js', 35 | './tests/dummy/config/**/*.js', 36 | ], 37 | parserOptions: { 38 | sourceType: 'script', 39 | }, 40 | env: { 41 | browser: false, 42 | node: true, 43 | }, 44 | plugins: ['node'], 45 | extends: ['plugin:node/recommended'], 46 | }, 47 | { 48 | // Test files: 49 | files: ['tests/**/*-test.{js,ts}'], 50 | extends: ['plugin:qunit/recommended'], 51 | }, 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /addon/mixins/route.js: -------------------------------------------------------------------------------- 1 | /*jshint unused:false */ 2 | /* eslint-disable ember/no-new-mixins */ 3 | 4 | import Mixin from '@ember/object/mixin'; 5 | import merge from 'lodash/merge'; 6 | 7 | export default Mixin.create({ 8 | queryParams: { 9 | filter: { refreshModel: true }, 10 | page: { refreshModel: true }, 11 | size: { refreshModel: true }, 12 | sort: { refreshModel: true }, 13 | }, 14 | mergeQueryOptions() { 15 | return {}; 16 | }, 17 | model(params) { 18 | const options = { 19 | sort: params.sort, 20 | page: { 21 | number: params.page, 22 | size: params.size, 23 | }, 24 | }; 25 | // TODO: sending an empty filter param to backend returns [] 26 | if (params.filter) { 27 | options['filter'] = params.filter; 28 | } 29 | merge(options, this.mergeQueryOptions(params)); 30 | return this.store.query(this.modelName, options); 31 | }, 32 | actions: { 33 | loading(transition) { 34 | let controller = this.controllerFor(this.routeName); 35 | controller.set('isLoadingModel', true); 36 | transition.promise.finally(function () { 37 | controller.set('isLoadingModel', false); 38 | }); 39 | 40 | return true; // bubble the loading event 41 | }, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /addon/components/text-search.js: -------------------------------------------------------------------------------- 1 | import { isEqual } from '@ember/utils'; 2 | import { cancel, debounce } from '@ember/runloop'; 3 | import { observer } from '@ember/object'; 4 | import { oneWay } from '@ember/object/computed'; 5 | import Component from '@ember/component'; 6 | import layout from '../templates/components/text-search'; 7 | 8 | export default Component.extend({ 9 | layout, 10 | filter: '', 11 | classNames: ['data-table-search'], 12 | internalValue: oneWay('filter'), 13 | auto: true, 14 | placeholder: 'Search', 15 | init() { 16 | this._super(...arguments); 17 | this.set('value', this.filter); 18 | }, 19 | onValueChange: observer('value', function () { 20 | this._valuePid = debounce(this, this._setFilter, this.wait); 21 | }), 22 | onFilterChange: observer('filter', function () { 23 | // update value if filter is update manually outsite this component 24 | if ( 25 | !this.isDestroying && 26 | !this.isDestroyed && 27 | !isEqual(this.filter, this.value) 28 | ) { 29 | this.set('value', this.filter); 30 | } 31 | }), 32 | _setFilter() { 33 | if (!this.isDestroying && !this.isDestroyed) { 34 | this.set('filter', this.value); 35 | } 36 | }, 37 | willDestroy() { 38 | this._super(...arguments); 39 | cancel(this._valuePid); 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /addon/templates/components/data-table.hbs: -------------------------------------------------------------------------------- 1 | {{#if (has-block)}} 2 |
3 | {{#if this.enableSearch}} 4 | {{text-search filter=this.filter auto=this.autoSearch wait=this.searchDebounceTime}} 5 | {{/if}} 6 | {{yield (hash 7 | menu=(component "data-table-menu" data-table=this) 8 | ) 9 | this}} 10 |
11 | {{yield (hash 12 | content=(component "data-table-content" content=this.content noDataMessage=this.noDataMessage enableSelection=this.enableSelection enableLineNumbers=this.enableLineNumbers onClickRow=(optional this.onClickRow) data-table=this) 13 | ) 14 | this}} 15 | {{else}} 16 | {{#if this.enableSearch}} 17 |
18 |
19 | {{text-search filter=this.filter auto=this.autoSearch}} 20 |
21 |
22 | {{/if}} 23 | {{component "data-table-content" content=this.content noDataMessage=this.noDataMessage enableSelection=this.enableSelection enableLineNumbers=this.enableLineNumbers onClickRow=(optional this.onClickRow) data-table=this}} 24 | {{/if}} 25 | 26 | {{#if this.content}} 27 | {{number-pagination 28 | page=this.page size=this.size nbOfItems=this.content.length sizeOptions=this.sizeOptions 29 | total=this.content.meta.count links=this.content.meta.pagination}} 30 | {{/if}} 31 | -------------------------------------------------------------------------------- /addon/templates/components/number-pagination.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | Displaying {{this.startItem}}-{{this.endItem}} 4 | {{#if this.total}} of {{this.total}}{{/if}} 5 | {{#if this.sizeOptions}} 6 | | 7 | per page 12 | {{/if}} 13 |
14 | {{#if this.hasMultiplePages}} 15 |
16 | 17 | 18 | 23 | 24 | 25 |
26 | {{/if}} 27 |
28 | -------------------------------------------------------------------------------- /MODULE_REPORT.md: -------------------------------------------------------------------------------- 1 | ## Module Report 2 | ### Unknown Global 3 | 4 | **Global**: `Ember.Logger` 5 | 6 | **Location**: `tests/dummy/app/controllers/application.js` at line 44 7 | 8 | ```js 9 | actions: { 10 | test(row) { 11 | Ember.Logger.info("Hi, you reached the test action for row: " + JSON.stringify(row)); 12 | }, 13 | menuTest() { 14 | ``` 15 | 16 | ### Unknown Global 17 | 18 | **Global**: `Ember.Logger` 19 | 20 | **Location**: `tests/dummy/app/controllers/application.js` at line 47 21 | 22 | ```js 23 | }, 24 | menuTest() { 25 | Ember.Logger.info("Hi, you reached the general menu test action"); 26 | }, 27 | selectionTest(selection, datatable) { 28 | ``` 29 | 30 | ### Unknown Global 31 | 32 | **Global**: `Ember.Logger` 33 | 34 | **Location**: `tests/dummy/app/controllers/application.js` at line 51 35 | 36 | ```js 37 | selectionTest(selection, datatable) { 38 | datatable.clearSelection(); 39 | Ember.Logger.info("Hi, you reached the selection test action for selection: " + JSON.stringify(selection)); 40 | selection.forEach(function(item) { 41 | item.set('age', item.get('age') + 1); 42 | ``` 43 | 44 | ### Unknown Global 45 | 46 | **Global**: `Ember.Logger` 47 | 48 | **Location**: `tests/dummy/app/controllers/application.js` at line 57 49 | 50 | ```js 51 | }, 52 | clickRow(row) { 53 | Ember.Logger.info("Custom row click action on item " + JSON.stringify(row)); 54 | } 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false, 17 | }, 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/integration/components/data-table-menu-general-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | data table menu general', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | await render(hbs`{{data-table-menu-general}}`); 14 | 15 | assert.dom('*').hasText(''); 16 | }); 17 | 18 | test('it renders block only if data table selection is empty', async function (assert) { 19 | // Set any properties with this.set('myProperty', 'value'); 20 | // Handle any actions with this.on('myAction', function(val) { ... }); 21 | 22 | this.set('data-table', { selectionIsEmpty: true }); 23 | // Template block usage: 24 | await render(hbs` 25 | {{#data-table-menu-general data-table=this.data-table}} 26 | template block text 27 | {{/data-table-menu-general}} 28 | `); 29 | assert.dom('*').hasText('template block text'); 30 | 31 | this.set('data-table', { selectionIsEmpty: false }); 32 | // Template block usage: 33 | await render(hbs` 34 | {{#data-table-menu-general data-table=this.data-table}} 35 | template block text 36 | {{/data-table-menu-general}} 37 | `); 38 | 39 | assert.dom('*').hasText(''); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /addon/components/data-table-content-body.js: -------------------------------------------------------------------------------- 1 | import { set } from '@ember/object'; 2 | import { computed } from '@ember/object'; 3 | import Component from '@ember/component'; 4 | import layout from '../templates/components/data-table-content-body'; 5 | 6 | export default Component.extend({ 7 | tagName: 'tbody', 8 | init() { 9 | this._super(...arguments); 10 | if (!this['data-table']) this.set('data-table', {}); 11 | if (!this['content']) this.set('content', []); 12 | }, 13 | layout, 14 | offset: computed('data-table.{page,size}', function () { 15 | var offset = 1; //to avoid having 0. row 16 | var page = this.get('data-table.page'); 17 | var size = this.get('data-table.size'); 18 | if (page && size) { 19 | offset += page * size; 20 | } 21 | return offset; 22 | }), 23 | wrappedItems: computed( 24 | 'content', 25 | 'content.[]', 26 | 'data-table.selection.[]', 27 | function () { 28 | const selection = this.get('data-table.selection') || []; 29 | const content = this.content || []; 30 | return content.map(function (item) { 31 | return { item: item, isSelected: selection.includes(item) }; 32 | }); 33 | } 34 | ), 35 | actions: { 36 | updateSelection(selectedWrapper, event) { 37 | set(selectedWrapper, 'isSelected', event.target.checked); 38 | this.wrappedItems.forEach((wrapper) => { 39 | if (wrapper.isSelected) { 40 | this.get('data-table').addItemToSelection(wrapper.item); 41 | } else { 42 | this.get('data-table').removeItemFromSelection(wrapper.item); 43 | } 44 | }); 45 | }, 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "12" 7 | 8 | dist: xenial 9 | 10 | addons: 11 | chrome: stable 12 | 13 | cache: 14 | directories: 15 | - $HOME/.npm 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | 22 | branches: 23 | only: 24 | - master 25 | # npm version tags 26 | - /^v\d+\.\d+\.\d+/ 27 | 28 | jobs: 29 | fast_finish: true 30 | allow_failures: 31 | - env: EMBER_TRY_SCENARIO=ember-canary 32 | 33 | include: 34 | # runs linting and tests with current locked deps 35 | - stage: "Tests" 36 | name: "Tests" 37 | script: 38 | - npm run lint 39 | - npm run test:ember 40 | 41 | - stage: "Additional Tests" 42 | name: "Floating Dependencies" 43 | install: 44 | - npm install --no-package-lock 45 | script: 46 | - npm run test:ember 47 | 48 | # we recommend new addons test the current and previous LTS 49 | # as well as latest stable release (bonus points to beta/canary) 50 | - env: EMBER_TRY_SCENARIO=ember-lts-3.24 51 | - env: EMBER_TRY_SCENARIO=ember-lts-3.28 52 | - env: EMBER_TRY_SCENARIO=ember-release 53 | - env: EMBER_TRY_SCENARIO=ember-beta 54 | - env: EMBER_TRY_SCENARIO=ember-canary 55 | - env: EMBER_TRY_SCENARIO=ember-default-with-jquery 56 | - env: EMBER_TRY_SCENARIO=ember-classic 57 | - env: EMBER_TRY_SCENARIO=embroider-safe 58 | - env: EMBER_TRY_SCENARIO=embroider-optimized 59 | 60 | script: 61 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 62 | -------------------------------------------------------------------------------- /addon/components/number-pagination.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import { gt } from '@ember/object/computed'; 3 | import Component from '@ember/component'; 4 | import layout from '../templates/components/number-pagination'; 5 | 6 | export default Component.extend({ 7 | layout, 8 | classNames: ['data-table-pagination'], 9 | currentPage: computed('page', { 10 | get() { 11 | return this.page ? parseInt(this.page) + 1 : 1; 12 | }, 13 | set(key, value) { 14 | this.set('page', value - 1); 15 | return value; 16 | }, 17 | }), 18 | firstPage: computed('links.first.number', function () { 19 | return this.get('links.first.number') || 1; 20 | }), 21 | lastPage: computed('links.last.number', function () { 22 | const max = this.get('links.last.number') || -1; 23 | return max ? max + 1 : max; 24 | }), 25 | isFirstPage: computed('firstPage', 'currentPage', function () { 26 | return this.firstPage == this.currentPage; 27 | }), 28 | isLastPage: computed('lastPage', 'currentPage', function () { 29 | return this.lastPage == this.currentPage; 30 | }), 31 | hasMultiplePages: gt('lastPage', 0), 32 | startItem: computed('size', 'currentPage', function () { 33 | return this.size * (this.currentPage - 1) + 1; 34 | }), 35 | endItem: computed('startItem', 'nbOfItems', function () { 36 | return this.startItem + this.nbOfItems - 1; 37 | }), 38 | pageOptions: computed('firstPage', 'lastPage', function () { 39 | const nbOfPages = this.lastPage - this.firstPage + 1; 40 | return Array.from( 41 | new Array(nbOfPages), 42 | (val, index) => this.firstPage + index 43 | ); 44 | }), 45 | actions: { 46 | changePage(link) { 47 | this.set('page', link['number'] || 0); 48 | }, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /addon/mixins/serializer.js: -------------------------------------------------------------------------------- 1 | import Mixin from '@ember/object/mixin'; 2 | 3 | export default Mixin.create({ 4 | /** 5 | Parse the links in the JSONAPI response and convert to a meta-object 6 | */ 7 | normalizeQueryResponse(store, clazz, payload) { 8 | const result = this._super(...arguments); 9 | result.meta = result.meta || {}; 10 | 11 | if (payload.links) { 12 | result.meta.pagination = this.createPageMeta(payload.links); 13 | } 14 | if (payload.meta) { 15 | result.meta.count = payload.meta.count; 16 | } 17 | 18 | return result; 19 | }, 20 | 21 | /** 22 | Transforms link URLs to objects containing metadata 23 | E.g. 24 | { 25 | previous: '/streets?page[number]=1&page[size]=10&sort=name 26 | next: '/streets?page[number]=3&page[size]=10&sort=name 27 | } 28 | 29 | will be converted to 30 | 31 | { 32 | previous: { number: 1, size: 10 }, 33 | next: { number: 3, size: 10 } 34 | } 35 | */ 36 | createPageMeta(data) { 37 | let meta = {}; 38 | 39 | Object.keys(data).forEach((type) => { 40 | const link = data[type]; 41 | meta[type] = {}; 42 | 43 | if (link) { 44 | //extracts from '/path?foo=bar&baz=foo' the string: foo=bar&baz=foo 45 | const query = link.split(/\?(.+)/)[1] || ''; 46 | 47 | query.split('&').forEach((pairs) => { 48 | const [param, value] = pairs.split('='); 49 | 50 | if (decodeURIComponent(param) === 'page[number]') { 51 | meta[type].number = parseInt(value); 52 | } else if (decodeURIComponent(param) === 'page[size]') { 53 | meta[type].size = parseInt(value); 54 | } 55 | }); 56 | } 57 | }); 58 | 59 | return meta; 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /addon/components/data-table.js: -------------------------------------------------------------------------------- 1 | import { typeOf } from '@ember/utils'; 2 | import { computed, observer } from '@ember/object'; 3 | import { bool, equal, oneWay } from '@ember/object/computed'; 4 | import Component from '@ember/component'; 5 | import layout from '../templates/components/data-table'; 6 | 7 | export default Component.extend({ 8 | init() { 9 | this._super(...arguments); 10 | if (this.selection === undefined) this.set('selection', []); 11 | }, 12 | layout, 13 | noDataMessage: 'No data', 14 | isLoading: false, 15 | lineNumbers: false, 16 | searchDebounceTime: 2000, 17 | enableLineNumbers: bool('lineNumbers'), 18 | enableSelection: oneWay('hasMenu'), 19 | selectionIsEmpty: equal('selection.length', 0), 20 | enableSizes: true, 21 | size: 5, 22 | sizeOptions: computed('size', 'sizes', 'enableSizes', function () { 23 | if (!this.enableSizes) { 24 | return null; 25 | } else { 26 | const sizeOptions = this.sizes || [5, 10, 25, 50, 100]; 27 | if (!sizeOptions.includes(this.size)) { 28 | sizeOptions.push(this.size); 29 | } 30 | sizeOptions.sort((a, b) => a - b); 31 | return sizeOptions; 32 | } 33 | }), 34 | hasMenu: false, // set from inner component, migth fail with nested if 35 | enableSearch: computed('filter', function () { 36 | return this.filter || this.filter === ''; 37 | }), 38 | autoSearch: true, 39 | filterChanged: observer('filter', function () { 40 | this.set('page', 0); 41 | }), 42 | sizeChanged: observer('size', function () { 43 | this.set('page', 0); 44 | }), 45 | parsedFields: computed('fields', function () { 46 | const fields = this.fields; 47 | if (typeOf(fields) === 'string') { 48 | return fields.split(' '); 49 | } else { 50 | return fields || []; 51 | } 52 | }), 53 | addItemToSelection(item) { 54 | this.selection.addObject(item); 55 | }, 56 | removeItemFromSelection(item) { 57 | this.selection.removeObject(item); 58 | }, 59 | clearSelection() { 60 | this.selection.clear(); 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import ArrayProxy from '@ember/array/proxy'; 3 | import Controller from '@ember/controller'; 4 | import DefaultQueryParams from 'ember-data-table/mixins/default-query-params'; 5 | 6 | var ApplicationController = Controller.extend(DefaultQueryParams, { 7 | model: ArrayProxy.create({ 8 | content: [ 9 | EmberObject.create({ 10 | firstName: 'John', 11 | lastName: 'Doe', 12 | age: 20, 13 | created: Date.now(), 14 | modified: Date.now(), 15 | }), 16 | EmberObject.create({ 17 | firstName: 'Jane', 18 | lastName: 'Doe', 19 | age: 25, 20 | created: Date.now(), 21 | modified: Date.now(), 22 | }), 23 | ], 24 | meta: { 25 | count: 63, 26 | pagination: { 27 | first: { 28 | number: 0, 29 | size: 5, 30 | }, 31 | prev: { 32 | number: 1, 33 | size: 5, 34 | }, 35 | self: { 36 | number: 2, 37 | size: 5, 38 | }, 39 | next: { 40 | number: 3, 41 | size: 5, 42 | }, 43 | last: { 44 | number: 12, 45 | size: 5, 46 | }, 47 | }, 48 | }, 49 | }), 50 | page: 2, 51 | size: 5, 52 | sort: 'first-name', 53 | actions: { 54 | test(row) { 55 | console.info( 56 | 'Hi, you reached the test action for row: ' + JSON.stringify(row) 57 | ); 58 | }, 59 | menuTest() { 60 | console.info('Hi, you reached the general menu test action'); 61 | }, 62 | selectionTest(selection, datatable) { 63 | datatable.clearSelection(); 64 | console.info( 65 | 'Hi, you reached the selection test action for selection: ' + 66 | JSON.stringify(selection) 67 | ); 68 | selection.forEach(function (item) { 69 | item.set('age', item.get('age') + 1); 70 | }); 71 | }, 72 | clickRow(row) { 73 | console.info('Custom row click action on item ' + JSON.stringify(row)); 74 | }, 75 | }, 76 | }); 77 | 78 | export default ApplicationController; 79 | -------------------------------------------------------------------------------- /addon/components/th-sortable.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import Component from '@ember/component'; 3 | import layout from '../templates/components/th-sortable'; 4 | 5 | export default Component.extend({ 6 | layout: layout, 7 | tagName: 'th', 8 | classNames: ['sortable'], 9 | classNameBindings: ['isSorted:sorted'], 10 | dasherizedField: computed('field', function () { 11 | return this.field.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 12 | }), 13 | /** 14 | Inverses the sorting parameter 15 | E.g. inverseSorting('title') returns '-title' 16 | inverseSorting('-title') returns 'title' 17 | */ 18 | _inverseSorting(sorting) { 19 | if (sorting.substring(0, 1) === '-') { 20 | return sorting.substring(1); 21 | } else { 22 | return '-' + sorting; 23 | } 24 | }, 25 | isSorted: computed('dasherizedField', 'currentSorting', function () { 26 | return ( 27 | this.currentSorting === this.dasherizedField || 28 | this.currentSorting === this._inverseSorting(this.dasherizedField) 29 | ); 30 | }), 31 | order: computed('dasherizedField', 'currentSorting', function () { 32 | if (this.currentSorting === this.dasherizedField) { 33 | return 'asc'; 34 | } else if (this.currentSorting === `-${this.dasherizedField}`) { 35 | return 'desc'; 36 | } else { 37 | return ''; 38 | } 39 | }), 40 | 41 | actions: { 42 | /** 43 | Sets the current sorting parameter. 44 | Note: the current sorting parameter may contain another field than the given field. 45 | In case the given field is currently sorted ascending, change to descending. 46 | In case the given field is currently sorted descending, clean the sorting. 47 | Else, set the sorting to ascending on the given field. 48 | */ 49 | inverseSorting() { 50 | if (this.order === 'asc') { 51 | this.set('currentSorting', this._inverseSorting(this.currentSorting)); 52 | } else if (this.order === 'desc') { 53 | this.set('currentSorting', ''); 54 | } else { 55 | // if currentSorting is not set to this field 56 | this.set('currentSorting', this.dasherizedField); 57 | } 58 | }, 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | scenarios: [ 9 | { 10 | name: 'ember-lts-3.24', 11 | npm: { 12 | devDependencies: { 13 | 'ember-source': '~3.24.3', 14 | }, 15 | }, 16 | }, 17 | { 18 | name: 'ember-lts-3.28', 19 | npm: { 20 | devDependencies: { 21 | 'ember-source': '~3.28.0', 22 | }, 23 | }, 24 | }, 25 | { 26 | name: 'ember-release', 27 | npm: { 28 | devDependencies: { 29 | 'ember-source': await getChannelURL('release'), 30 | }, 31 | }, 32 | }, 33 | { 34 | name: 'ember-beta', 35 | npm: { 36 | devDependencies: { 37 | 'ember-source': await getChannelURL('beta'), 38 | }, 39 | }, 40 | }, 41 | { 42 | name: 'ember-canary', 43 | npm: { 44 | devDependencies: { 45 | 'ember-source': await getChannelURL('canary'), 46 | }, 47 | }, 48 | }, 49 | { 50 | name: 'ember-default-with-jquery', 51 | env: { 52 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 53 | 'jquery-integration': true, 54 | }), 55 | }, 56 | npm: { 57 | devDependencies: { 58 | '@ember/jquery': '^1.1.0', 59 | }, 60 | }, 61 | }, 62 | { 63 | name: 'ember-classic', 64 | env: { 65 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 66 | 'application-template-wrapper': true, 67 | 'default-async-observers': false, 68 | 'template-only-glimmer-components': false, 69 | }), 70 | }, 71 | npm: { 72 | devDependencies: { 73 | 'ember-source': '~3.28.0', 74 | }, 75 | ember: { 76 | edition: 'classic', 77 | }, 78 | }, 79 | }, 80 | embroiderSafe(), 81 | embroiderOptimized(), 82 | ], 83 | }; 84 | }; 85 | -------------------------------------------------------------------------------- /app/styles/ember-data-table.scss: -------------------------------------------------------------------------------- 1 | // Table style from MaterializeCSS 2 | $table-border-color: #d0d0d0; 3 | $table-striped-color: #f2f2f2; 4 | 5 | table, th, td { 6 | border: none; 7 | } 8 | 9 | table { 10 | width:100%; 11 | display: table; 12 | border-collapse: collapse; 13 | 14 | &.bordered > thead > tr, 15 | &.bordered > tbody > tr { 16 | border-bottom: 1px solid $table-border-color; 17 | } 18 | 19 | &.striped > tbody { 20 | > tr:nth-child(odd) { 21 | background-color: $table-striped-color; 22 | } 23 | 24 | > tr > td { 25 | border-radius: 0; 26 | } 27 | } 28 | 29 | &.highlight > tbody > tr { 30 | transition: background-color .25s ease; 31 | &:hover { 32 | background-color: $table-striped-color; 33 | } 34 | } 35 | 36 | &.centered { 37 | thead tr th, tbody tr td { 38 | text-align: center; 39 | } 40 | } 41 | 42 | } 43 | 44 | thead { 45 | border-top: 1px solid $table-border-color; 46 | border-bottom: 1px solid $table-border-color; 47 | } 48 | 49 | td, th{ 50 | padding: 15px 5px; 51 | display: table-cell; 52 | text-align: left; 53 | vertical-align: middle; 54 | border-radius: 2px; 55 | } 56 | 57 | .data-table-header { 58 | padding: 10px 5px; 59 | 60 | &.selected { 61 | background-color: #FFEBEE; 62 | } 63 | } 64 | 65 | .data-table-menu { 66 | color: #F44336; 67 | 68 | a { 69 | padding: 0 1rem; 70 | } 71 | 72 | .item-count { 73 | padding-left: 10px; 74 | padding-right: 15px; 75 | } 76 | } 77 | 78 | table.data-table { 79 | font-size: 80%; 80 | color: rgb(13%,13%,13%); 81 | 82 | thead { 83 | color: rgb(46%,46%,46%); 84 | .sorted { 85 | color: rgb(13%,13%,13%); 86 | } 87 | .sortable { 88 | &:not(.sorted):hover { 89 | span::before { 90 | color: rgb(62%,62%,62%); 91 | content: 'asc'; 92 | } 93 | } 94 | } 95 | } 96 | 97 | tbody { 98 | tr { 99 | &:hover { 100 | background-color: #eeeeee; 101 | } 102 | &.selected { 103 | background-color: #f5f5f5; 104 | } 105 | } 106 | } 107 | } 108 | 109 | /** Pagination */ 110 | .data-table-pagination { 111 | font-size: 80%; 112 | color: rgb(13%,13%,13%); 113 | } 114 | -------------------------------------------------------------------------------- /tests/integration/components/data-table-menu-selected-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, click } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | data table menu selected', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders block only if data table selection is not empty', async function (assert) { 10 | this.set('data-table', { selectionIsEmpty: true }); 11 | // Template block usage: 12 | await render(hbs` 13 | {{#data-table-menu-selected data-table=this.data-table}} 14 | template block text 15 | {{/data-table-menu-selected}} 16 | `); 17 | assert.dom('*').hasText(''); 18 | }); 19 | 20 | test('it renders selection count', async function (assert) { 21 | this.set('data-table', { selectionIsEmpty: false, selection: ['foo'] }); 22 | // Template block usage: 23 | await render(hbs` 24 | {{#data-table-menu-selected data-table=this.data-table}} 25 | template block text 26 | {{/data-table-menu-selected}} 27 | `); 28 | 29 | assert.dom('span.item-count').hasText('1 item(s) selected', 'item count 1'); 30 | 31 | this.set('data-table', { 32 | selectionIsEmpty: false, 33 | selection: ['foo', 'bar'], 34 | }); 35 | // Template block usage: 36 | await render(hbs` 37 | {{#data-table-menu-selected data-table=this.data-table}} 38 | template block text 39 | {{/data-table-menu-selected}} 40 | `); 41 | 42 | assert.dom('span.item-count').hasText('2 item(s) selected', 'item count 2'); 43 | }); 44 | 45 | test('calls clearSelection on cancel button click', async function (assert) { 46 | assert.expect(2); // 2 asserts in this test 47 | 48 | this.set('data-table', { selectionIsEmpty: false, selection: ['foo'] }); 49 | this.set('data-table.clearSelection', function () { 50 | assert.ok(true, 'data-table.clearSelection gets called'); 51 | }); 52 | // Template block usage: 53 | await render(hbs` 54 | {{#data-table-menu-selected data-table=this.data-table}} 55 | template block text 56 | {{/data-table-menu-selected}} 57 | `); 58 | 59 | assert.dom('button').hasText('Cancel', 'renders a cancel button'); 60 | await click('button'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/integration/components/data-table-content-header-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | data table content header', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | 13 | await render(hbs`{{data-table-content-header}}`); 14 | assert.dom('thead').exists({ count: 1 }); 15 | 16 | assert.dom('*').hasText(''); 17 | 18 | // Template block usage: 19 | await render(hbs` 20 | {{#data-table-content-header}} 21 | template block text 22 | {{/data-table-content-header}} 23 | `); 24 | 25 | assert.dom('*').hasText('template block text'); 26 | }); 27 | 28 | test('display column headers', async function (assert) { 29 | this.set('data-table', {}); 30 | this.set('data-table.parsedFields', ['firstName', 'lastName', 'age']); 31 | 32 | await render(hbs`{{data-table-content-header data-table=this.data-table}}`); 33 | 34 | assert.dom('tr').exists({ count: 1 }, 'displays 1 header row'); 35 | assert.dom('tr:first-child th').exists({ count: 3}, 'displays 3 column headers'); 36 | assert.dom('tr:first-child th:first-child').hasText( 37 | 'firstName', 38 | 'displays firstName as first header' 39 | ); 40 | assert.dom('tr:first-child th:nth-child(2)').hasText( 41 | 'lastName', 42 | 'displays lastName as second column header' 43 | ); 44 | assert.dom('tr:first-child th:nth-child(3)').hasText( 45 | 'age', 46 | 'displays age as third column header' 47 | ); 48 | }); 49 | 50 | test('add selection column header if enabled', async function (assert) { 51 | this.set('data-table', {}); 52 | this.set('data-table.parsedFields', ['firstName', 'lastName', 'age']); 53 | 54 | await render( 55 | hbs`{{data-table-content-header data-table=this.data-table enableSelection=true}}` 56 | ); 57 | 58 | assert.dom('tr').exists({ count: 1 }, 'displays 1 header row'); 59 | assert.dom('tr:first-child th').exists({ count: 4 }, 'displays 4 column headers'); 60 | assert.dom('tr:first-child th:first-child').hasText( 61 | '', 62 | 'displays selection as first header' 63 | ); 64 | }); 65 | 66 | test('add line number column header if enabled', async function (assert) { 67 | this.set('data-table', {}); 68 | this.set('data-table.parsedFields', ['firstName', 'lastName', 'age']); 69 | 70 | await render( 71 | hbs`{{data-table-content-header data-table=this.data-table enableLineNumbers=true}}` 72 | ); 73 | 74 | assert.dom('tr').exists({ count: 1 }, 'displays 1 header row'); 75 | assert.dom('tr:first-child th').exists({ count: 4 }, 'displays 4 column headers'); 76 | assert.dom('tr:first-child th:first-child').hasText( 77 | '', 78 | 'displays line number as first header' 79 | ); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-table", 3 | "version": "2.1.0", 4 | "description": "Data tables for Ember following Google Design specs", 5 | "keywords": [ 6 | "ember-addon", 7 | "mu-semtech" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mu-semtech/ember-data-table" 12 | }, 13 | "license": "MIT", 14 | "author": "Erika Pauwels", 15 | "directories": { 16 | "doc": "doc", 17 | "test": "tests" 18 | }, 19 | "scripts": { 20 | "build": "ember build --environment=production", 21 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", 22 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 23 | "lint:hbs": "ember-template-lint .", 24 | "lint:hbs:fix": "ember-template-lint . --fix", 25 | "lint:js": "eslint . --cache", 26 | "lint:js:fix": "eslint . --fix", 27 | "start": "ember serve", 28 | "test": "npm-run-all lint test:*", 29 | "test:ember": "ember test", 30 | "test:ember-compatibility": "ember try:each" 31 | }, 32 | "dependencies": { 33 | "ember-auto-import": "^1.12.0", 34 | "ember-cli-babel": "^7.26.10", 35 | "ember-cli-htmlbars": "^5.7.2", 36 | "ember-composable-helpers": "^5.0.0", 37 | "ember-math-helpers": "^2.18.0", 38 | "ember-truth-helpers": "^3.0.0", 39 | "lodash": "^4.17.21" 40 | }, 41 | "devDependencies": { 42 | "@ember/optional-features": "^2.0.0", 43 | "@ember/test-helpers": "^2.6.0", 44 | "@embroider/test-setup": "^0.48.1", 45 | "@glimmer/component": "^1.0.4", 46 | "@glimmer/tracking": "^1.0.4", 47 | "babel-eslint": "^10.1.0", 48 | "broccoli-asset-rev": "^3.0.0", 49 | "ember-cli": "~3.28.5", 50 | "ember-cli-dependency-checker": "^3.2.0", 51 | "ember-cli-inject-live-reload": "^2.1.0", 52 | "ember-cli-release": "^1.0.0-beta.2", 53 | "ember-cli-sass": "^10.0.1", 54 | "ember-cli-sri": "^2.1.1", 55 | "ember-cli-terser": "^4.0.2", 56 | "ember-cli-update": "^0.33.2", 57 | "ember-disable-prototype-extensions": "^1.1.3", 58 | "ember-export-application-global": "^2.0.1", 59 | "ember-load-initializers": "^2.1.2", 60 | "ember-maybe-import-regenerator": "^1.0.0", 61 | "ember-page-title": "^6.2.2", 62 | "ember-qunit": "^5.1.5", 63 | "ember-resolver": "^8.0.3", 64 | "ember-source": "~3.28.8", 65 | "ember-source-channel-url": "^3.0.0", 66 | "ember-template-lint": "^3.15.0", 67 | "ember-try": "^1.4.0", 68 | "eslint": "^7.32.0", 69 | "eslint-config-prettier": "^8.3.0", 70 | "eslint-plugin-ember": "^10.5.8", 71 | "eslint-plugin-node": "^11.1.0", 72 | "eslint-plugin-prettier": "^3.4.1", 73 | "eslint-plugin-qunit": "^6.2.0", 74 | "loader.js": "^4.7.0", 75 | "npm-run-all": "^4.1.5", 76 | "phantomjs-prebuilt": "^2.1.16", 77 | "prettier": "^2.5.1", 78 | "qunit": "^2.17.2", 79 | "qunit-dom": "^1.6.0", 80 | "sass": "^1.48.0" 81 | }, 82 | "engines": { 83 | "node": "12.* || 14.* || >= 16" 84 | }, 85 | "ember": { 86 | "edition": "octane" 87 | }, 88 | "ember-addon": { 89 | "configPath": "tests/dummy/config" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "Dummy"}} 2 | 3 |
4 |
5 |

Ember Data Table demo

6 |

Simple table

7 |
Generated table header and body based on given fields
8 | {{data-table content=this.model fields="firstName lastName age created modified" sort=this.sort page=this.page size=this.size filter=this.filter}} 9 | 10 |

Semi-complex table

11 |
Customized table header and body
12 | 13 | 14 | 15 | {{th-sortable field="firstName" currentSorting=this.sort label="First name"}} 16 | {{th-sortable field="lastName" currentSorting=this.sort label="Last name"}} 17 | Age 18 | {{th-sortable field="created" currentSorting=this.sort label="Created"}} 19 | Modified 20 | 21 | 22 | {{row.firstName}} 23 | {{row.lastName}} 24 | {{row.age}} 25 | {{row.created}} 26 | {{row.modified}} 27 | 28 | 29 | 30 | 31 |

Complex table

32 |
Customized table including an action menu on top
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {{th-sortable field="firstName" currentSorting=this.sort label="First name"}} 46 | {{th-sortable field="lastName" currentSorting=this.sort label="Last name"}} 47 | Ageinput 48 | {{th-sortable field="created" currentSorting=this.sort label="Created"}} 49 | Modified 50 | 51 | 52 | 53 | 54 | {{row.firstName}} 55 | {{row.lastName}} 56 | {{row.age}} 57 | {{row.created}} 58 | {{row.modified}} 59 | 60 | 61 | 62 | 63 | 64 |

Internal variables

65 |
    66 |
  • Sort: {{this.sort}}
  • 67 |
  • Page: {{this.page}}
  • 68 |
  • Size: {{this.size}}
  • 69 |
  • Filter: {{this.filter}}
  • 70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /tests/integration/components/data-table-content-body-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { click, render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Component | data table content body', function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test('it renders', async function (assert) { 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... }); 12 | await render(hbs`{{data-table-content-body}}`); 13 | assert.dom('tbody').exists({ count: 1 }); 14 | }); 15 | 16 | test('display rows', async function (assert) { 17 | this.set('content', [ 18 | { firstName: 'John', lastName: 'Doe', age: 20 }, 19 | { firstName: 'Jane', lastName: 'Doe', age: 21 }, 20 | ]); 21 | this.set('dataTable', {}); 22 | this.set('dataTable.parsedFields', ['firstName', 'lastName', 'age']); 23 | this.set('dataTable.selection', []); 24 | 25 | await render( 26 | hbs`{{data-table-content-body content=this.content data-table=this.dataTable}}` 27 | ); 28 | 29 | assert.dom('tr').exists({ count: 2 }, 'displays 2 rows'); 30 | assert.dom('tr:first-child td').exists({count: 3}, 'displays 3 columns') 31 | assert.dom('tr:first-child td:first-child').hasText('John', 'displays firstName in first column'); 32 | assert.dom('tr:first-child td:nth-child(2)').hasText('Doe', 'displays lastName in second column'); 33 | assert.dom('tr:first-child td:nth-child(3)').hasText('20', 'displays age in third column'); 34 | }); 35 | 36 | test('add checkboxes for selection if enabled', async function (assert) { 37 | const john = { firstName: 'John', lastName: 'Doe', age: 20 }; 38 | const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; 39 | const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; 40 | this.set('content', [john, jane, jeff]); 41 | this.set('data-table', {}); 42 | this.set('data-table.parsedFields', ['firstName', 'lastName', 'age']); 43 | this.set('data-table.selection', [jane]); 44 | 45 | await render( 46 | hbs`{{data-table-content-body content=this.content data-table=this.data-table enableSelection=true}}` 47 | ); 48 | 49 | assert.dom('tr:first-child td').exists({ count: 4 }, 'displays 4 columns'); 50 | assert.dom('tr.selected').exists({ count: 1 }, 'displays 1 selected row'); 51 | assert 52 | .dom('tr input[type="checkbox"]') 53 | .exists({ count: 3 }, 'displays a checkbox on each row'); 54 | assert 55 | .dom('tr input[type="checkbox"]:checked') 56 | .isChecked('displays 1 checked checkbox'); 57 | }); 58 | 59 | test('toggles selection if checkbox is clicked', async function (assert) { 60 | const john = { firstName: 'John', lastName: 'Doe', age: 20 }; 61 | const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; 62 | const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; 63 | this.set('content', [john, jane, jeff]); 64 | this.set('data-table', {}); 65 | this.set('data-table.parsedFields', ['firstName', 'lastName', 'age']); 66 | this.set('data-table.selection', [jane]); 67 | this.set('data-table.addItemToSelection', () => 68 | this.set('data-table.selection', [john, jane]) 69 | ); // mock function 70 | this.set('data-table.removeItemFromSelection', function () {}); // mock function 71 | 72 | await render( 73 | hbs`{{data-table-content-body content=this.content data-table=this.data-table enableSelection=true}}` 74 | ); 75 | 76 | assert 77 | .dom('tr input[type="checkbox"]:checked') 78 | .isChecked('displays 1 checked checkbox before selecting a row'); 79 | 80 | await click('tr:first-child input[type="checkbox"]'); 81 | 82 | assert 83 | .dom('tr input[type="checkbox"]:checked') 84 | .isChecked('displays 2 checked checkboxes after selecting a row'); 85 | }); 86 | 87 | test('add line numbers if enabled', async function (assert) { 88 | const john = { firstName: 'John', lastName: 'Doe', age: 20 }; 89 | const jane = { firstName: 'Jane', lastName: 'Doe', age: 21 }; 90 | const jeff = { firstName: 'Jeff', lastName: 'Doe', age: 22 }; 91 | this.set('content', [john, jane, jeff]); 92 | this.set('data-table', {}); 93 | this.set('data-table.parsedFields', ['firstName', 'lastName', 'age']); 94 | this.set('data-table.selection', []); 95 | 96 | await render( 97 | hbs`{{data-table-content-body content=this.content data-table=this.data-table enableLineNumbers=true}}` 98 | ); 99 | 100 | assert.dom('tr:first-child td').exists({ count: 4 }, 'displays 4 columns'); 101 | assert.dom('tr:first-child td:first-child').hasText('1', 'displays offset 1 on the first row'); 102 | assert.dom('tr:nth-child(2) td:first-child').hasText('2', 'displays offset 2 on the second row'); 103 | assert.dom('tr:nth-child(3) td:first-child').hasText('3', 'displays offset 3 on the third row'); 104 | 105 | this.set('data-table.page', 2); 106 | this.set('data-table.size', 5); 107 | await render( 108 | hbs`{{data-table-content-body content=this.content data-table=this.data-table enableLineNumbers=true}}` 109 | ); 110 | 111 | assert.dom('tr:first-child td').exists({ count: 4 }, 'displays 4 columns on page 3'); 112 | assert.dom('tr:first-child td:first-child').hasText('11', 'displays offset 11 on the first row on page 3'); 113 | assert.dom('tr:nth-child(2) td:first-child').hasText('12', 'displays offset 12 on the second row on page 3'); 114 | assert.dom('tr:nth-child(3) td:first-child').hasText('13', 'displays offset 13 on the third row of page 3'); 115 | }); 116 | 117 | test('displays no data message if there is no data', async function (assert) { 118 | // Set any properties with this.set('myProperty', 'value'); 119 | // Handle any actions with this.on('myAction', function(val) { ... }); 120 | this.set('noDataMessage', 'No data'); 121 | this.set('data-table', {}); 122 | this.set('data-table.parsedFields', ['firstName', 'lastName', 'age']); 123 | this.set('data-table.selection', []); 124 | 125 | await render( 126 | hbs`{{data-table-content-body noDataMessage=this.noDataMessage data-table=this.data-table}}` 127 | ); 128 | assert 129 | .dom('td.no-data-message') 130 | .exists({ count: 1 }, 'displays a no data message if no content'); 131 | assert 132 | .dom('td.no-data-message') 133 | .hasText('No data', 'displays message "No data" if no content'); 134 | 135 | this.set('content', []); 136 | await render( 137 | hbs`{{data-table-content-body content=this.content noDataMessage=this.noDataMessage data-table=this.data-table}}` 138 | ); 139 | assert 140 | .dom('td.no-data-message') 141 | .exists({ count: 1 }, 'displays a no data message if empty content'); 142 | assert 143 | .dom('td.no-data-message') 144 | .hasText('No data', 'displays message "No data" if empty content'); 145 | 146 | this.set('content', ['foo', 'bar']); 147 | await render( 148 | hbs`{{data-table-content-body content=this.content noDataMessage=this.noDataMessage data-table=this.data-table}}` 149 | ); 150 | assert 151 | .dom('td.no-data-message') 152 | .doesNotExist('displays no message when there is content'); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember Data Table 2 | [![Build Status](https://travis-ci.org/mu-semtech/ember-data-table.svg?branch=master)](https://travis-ci.org/mu-semtech/ember-data-table) 3 | [![npm version](https://badge.fury.io/js/ember-data-table.svg)](https://badge.fury.io/js/ember-data-table) 4 | 5 | Data table for Ember based on a JSONAPI compliant backend. 6 | 7 | Have a look at [ember-paper-data-table](https://github.com/mu-semtech/emper-paper-data-table) to get a data table styled with [ember-paper](https://github.com/miguelcobain/ember-paper). 8 | 9 | ## Installation 10 | If you're using Ember > v3.8 11 | ```bash 12 | ember install ember-data-table 13 | ``` 14 | 15 | For Ember < v3.8, use version 1.x of the addon 16 | ```bash 17 | ember install ember-data-table@1.2.2 18 | ``` 19 | 20 | ## Getting started 21 | Include the `DataTableRouteMixin` in the route which model you want to show in the data table. Configure the model name. 22 | 23 | ```javascript 24 | import Ember from 'ember'; 25 | import DataTableRouteMixin from 'ember-data-table/mixins/route'; 26 | 27 | export default Ember.Route.extend(DataTableRouteMixin, { 28 | modelName: 'blogpost' 29 | }); 30 | ``` 31 | 32 | Next, include the data table in your template: 33 | 34 | ```htmlbars 35 | {{data-table 36 | content=model 37 | fields="firstName lastName age created modified" 38 | isLoading=isLoadingModel 39 | filter=filter 40 | sort=sort 41 | page=page 42 | size=size 43 | }} 44 | ``` 45 | 46 | Note: the filtering, sorting and pagination isn't done at the frontend. By including the `DataTableRouteMixin` in the route each change to the `filter`, `sort`, `page` and `size` params will result in a new request to the backend. The `DataTableRouteMixin` also sets an isLoadingModel flag while the route's model is being loaded. 47 | 48 | Have a look at [Customizing the data table](https://github.com/erikap/ember-data-table#customizing-the-data-table) to learn how you can customize the data table's header and body. 49 | 50 | ## Data table component 51 | 52 | ### Specification 53 | 54 | The following parameters can be passed to the data-table component: 55 | 56 | | Parameter | Required | Default | Description | 57 | |-----------|----------|---------|-------------| 58 | | content | x | | a list of resources to be displayed in the table | 59 | | fields | | | names of the model fields to show as columns (seperated by whitespace) | 60 | | isLoading | | false | shows a spinner instead of the table content if true | 61 | | filter | | | current value of the text search | 62 | | sort | | | field by which the data is currently sorted | 63 | | page | | | number of the page that is currently displayed | 64 | | size | | | number of items shown on one page | 65 | | enableSizes | | true | flag to enable page size options dropdown | 66 | | sizes | | [5, 10, 25, 50, 100] | array of page size options (numbers) | 67 | | link | | | name of the route the first column will link to. The selected row will be passed as a parameter. | 68 | | onClickRow | | | action sent when a row is clicked. Takes the clicked item as a parameter. | 69 | | autoSearch | | true | whether filter value is updated automatically while typing (with a debounce) or user must click a search button explicitly to set the filter value. 70 | | noDataMessage | | No data | message to be shown when there is no content | 71 | | lineNumbers | | false | display a line number per table row (default: false). Must be true or false. | 72 | | searchDebounceTime | | 2000 | debounce time of the search action of the data table. Must be integer. | 73 | 74 | By default the data table will make each column sortable. The search text box is only shown if the `filter` parameter is bound. Pagination is only shown if the pagination metadata is set on the model (see the [Ember Data Table Serializer mixin](https://github.com/mu-semtech/ember-data-table#serializer)). 75 | 76 | ### Customizing the data table 77 | The way the data is shown in the table can be customized by defining a `content` block instead of a `fields` parameter. 78 | 79 | ```htmlbars 80 | {{#data-table content=model filter=filter sort=sort page=page size=size onClickRow=(action "clickRow") as |t|}} 81 | {{#t.content as |c|}} 82 | {{#c.header}} 83 | {{th-sortable field='firstName' currentSorting=sort label='First name'}} 84 | {{th-sortable field='lastName' currentSorting=sort label='Last name'}} 85 | Age 86 | {{th-sortable field='created' currentSorting=sort label='Created'}} 87 | Modified 88 | {{/c.header}} 89 | {{#c.body as |row|}} 90 | {{row.firstName}} 91 | {{row.lastName}} 92 | {{row.age}} 93 | {{moment-format row.created}} 94 | {{moment-format row.modified}} 95 | {{/c.body}} 96 | {{/t.content}} 97 | {{/data-table}} 98 | ``` 99 | Have a look at the [helper components](https://github.com/mu-semtech/ember-data-table#helper-components). 100 | 101 | ### Adding actions to the data table 102 | The user can add actions on top of the data table by providing a `menu` block. 103 | ```htmlbars 104 | {{#data-table content=model filter=filter sort=sort page=page size=size isLoading=isLoadingModel as |t|}} 105 | {{#t.menu as |menu|}} 106 | {{#menu.general}} 107 | {{#paper-button onClick=(action "export") accent=true noInk=true}}Export{{/paper-button}} 108 | {{#paper-button onClick=(action "print") accent=true noInk=true}}Print{{/paper-button}} 109 | {{/menu.general}} 110 | {{#menu.selected as |selection datatable|}} 111 | {{#paper-button onClick=(action "delete" selection table) accent=true noInk=true}}Delete{{/paper-button}} 112 | {{/menu.selected}} 113 | {{/t.menu}} 114 | {{#t.content as |c|}} 115 | ... 116 | {{/t.content}} 117 | {{/data-table}} 118 | ``` 119 | The menu block consists of a `general` and a `selected` block. The `menu.general` is shown by default. The `menu.selected` is shown when one or more rows in the data table are selected. 120 | 121 | When applying an action on a selection, the currently selected rows can be provided to the action by the `selection` parameter. The user must reset the selection by calling `clearSelection()` on the data table. 122 | E.g. 123 | ```javascript 124 | actions: 125 | myAction(selection, datatable) { 126 | console.log("Hi, you reached my action for selection: " + JSON.stringify(selection)); 127 | datatable.clearSelection(); 128 | } 129 | ``` 130 | 131 | ## Helper components 132 | The following components may be helpful when customizing the data table: 133 | * [Sortable header](https://github.com/mu-semtech/ember-data-table#sortable-header) 134 | 135 | ### Sortable header 136 | The `th-sortable` component makes a column in the data table sortable. It displays a table header `` element including an ascending/descending sorting icon in case the table is currently sorted by the given column. 137 | 138 | ```htmlbars 139 | {{th-sortable field='firstName' currentSorting=sort label='First name'}} 140 | ``` 141 | 142 | The following parameters are passed to the `th-sortable` component: 143 | 144 | | Parameter | Required | Description | 145 | |-----------|----------|-------------| 146 | | field | x | name of the model field in the column | 147 | | label | x | label to be shown in the column's table header | 148 | | currentSorting | x | current sorting (field and order) of the data according to [the JSONAPI specification](http://jsonapi.org/format/#fetching-sorting) | 149 | 150 | Note: the data table will update the `currentSorting` variable, but the user needs to handle the reloading of the data. The [Ember Data Table Route mixin](https://github.com/mu-semtech/ember-data-table#route) may be of use. 151 | 152 | ## Mixins 153 | The following mixins may be helpful to use with the data table: 154 | * [Serializer mixin](https://github.com/mu-semtech/ember-data-table#serializer) 155 | * [Route mixin](https://github.com/mu-semtech/ember-data-table#route) 156 | * [Default Query Params mixin](https://github.com/mu-semtech/ember-data-table#default-query-params) 157 | 158 | ### Serializer 159 | Upon installation, the `DataTableSerializerMixin` is automatically included in your application serializer to add parsing of the filter, sortig and pagination meta data from the links in the [JSONAPI](http://jsonapi.org) responses. The data is stored in [Ember's model metadata](https://guides.emberjs.com/v2.9.0/models/handling-metadata/). 160 | 161 | To include the `DataTableSerializerMixin` in your application, add the mixin to your application serializer: 162 | ```javascript 163 | import DS from 'ember-data'; 164 | import DataTableSerializerMixin from 'ember-data-table/mixins/serializer'; 165 | 166 | export default DS.JSONAPISerializer.extend(DataTableSerializerMixin, { 167 | 168 | }); 169 | ``` 170 | 171 | E.g. 172 | ```javascript 173 | meta: { 174 | count: 42 175 | }, 176 | links: { 177 |  previous: '/posts?page[number]=1&page[size]=10' 178 |  next: '/posts?page[number]=3&page[size]=10' 179 | } 180 | ``` 181 | will be parsed to 182 | ```javascript 183 | meta: { 184 | count: 42, 185 | pagination: { 186 | previous: { number: 1, size: 10 }, 187 | next: { number: 3, size: 10 } 188 | } 189 | } 190 | ``` 191 | 192 | ### Route 193 | The route providing data for the `data-table` component often looks similar. The model hook needs to query a list of resources of a specific model from the server. This list needs to be reloaded when the sorting, page or page size changes. The `DataTableRouteMixin` provides a default implementation of this behaviour. Just include the mixin in your route and specify the model to be queried as `modelName`. 194 | 195 | ```javascript 196 | import Ember from 'ember'; 197 | import DataTableRouteMixin from 'ember-data-table/mixins/route'; 198 | 199 | export default Ember.Route.extend(DataTableRouteMixin, { 200 | modelName: 'post' 201 | }); 202 | ``` 203 | 204 | The `DataTableRouteMixin` specifies the `filter`, `page`, `sort` and `size` variables as `queryParams` of the route with the `refreshModel` flag set to `true`. As such the data is reloaded when one of the variables changes. A user can add custom options to be passed in the query to the server by defining a `mergeQueryOptions(parms)` function in the route. The function must return an object with the options to be merged. 205 | 206 | ```javascript 207 | import Ember from 'ember'; 208 | import DataTableRouteMixin from 'ember-data-table/mixins/route'; 209 | 210 | export default Ember.Route.extend(DataTableRouteMixin, { 211 | modelName: 'post', 212 | mergeQueryOptions(params) { 213 | return { included: 'author' }; 214 | } 215 | }); 216 | ``` 217 | 218 | Note: if the `mergeQueryOptions` returns a filter option on a specific field (e.g. `title`), the nested key needs to be provided as a string. Otherwise the `filter` param across all fields will be overwritten breaking the general search. 219 | 220 | E.g. 221 | ```javascript 222 | mergeQueryOptions(params) { 223 | return { 224 | included: 'author', 225 | 'filter[title]': params.title 226 | }; 227 | } 228 | ``` 229 | 230 | The `DataTableRouteMixin` also sets the `isLoadingModel` flag on the controller while the route's model is being loaded. Passing this flag to the data table's `isLoading` property will show a spinner while data is loaded. 231 | 232 | ### Default Query Params 233 | The `DefaultQueryParams` mixin provides sensible defaults for the `page` (default: 0), `size` (default: 25) and `filter` (default: '') query parameters. The mixin can be mixed in a controller that uses the `page` and `filter` query params. 234 | 235 | ```javascript 236 | import Ember from 'ember'; 237 | import DefaultQueryParamsMixin from 'ember-data-table/mixins/default-query-params'; 238 | 239 | export default Ember.Controller.extend(DefaultQueryParamsMixin, { 240 | ... 241 | }); 242 | ``` 243 | 244 | Note: if you want the search text field to be enabled on a data table, the filter parameter may not be `undefined`. Therefore you must initialize it on an empty query string (as done by the `DefaultQueryParams` mixin). 245 | --------------------------------------------------------------------------------