├── 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 | Cancel
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 | Search
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 |
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 |
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 |
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 |
14 | {{#if this.hasMultiplePages}}
15 |
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 | Export
37 | Print
38 |
39 |
40 | Export
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 | Age input
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 | Info
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 | [](https://travis-ci.org/mu-semtech/ember-data-table)
3 | [](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 |
--------------------------------------------------------------------------------