20 | {{/demo.example}}
21 |
22 | {{demo.snippet name='docs-example-subcolumns.hbs'}}
23 | {{demo.snippet name='docs-example-subcolumns.js' label='component.js'}}
24 | {{/docs-demo}}
25 |
26 | ## Resizing and Reordering
27 |
28 | Subcolumns can be resized like any other column. When resizing a column with
29 | subcolumns, changes in width will be spread throughout the subcolumns.
30 | Subcolumns can only be reordered within their group - it is not currently
31 | possible to reorder move columns around arbitrarily.
32 |
33 | Columns do not need to have the same numbers of subcolumns, they can mix and
34 | match as much as you would like. This table's columns have generated completely
35 | randomly, demonstrating the flexibility of subcolumns.
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/examples/infinite-scroll/component.js:
--------------------------------------------------------------------------------
1 | import Component from '@ember/component';
2 | import { computed } from '@ember/object';
3 | import { task, timeout } from 'ember-concurrency';
4 | import { A } from '@ember/array';
5 |
6 | export default Component.extend({
7 | // BEGIN-SNIPPET docs-example-infinite-scroll.js
8 | // count of records loaded so far
9 | offset: 0,
10 |
11 | // count of records per "page"
12 | limit: 20,
13 |
14 | // substitute total count of records from API response meta data
15 | maxRows: 100,
16 |
17 | // center spinner horizontally in scroll viewport
18 | centerSpinner: true,
19 |
20 | canLoadMore: computed('offset', 'maxRows', function() {
21 | return this.offset < this.maxRows;
22 | }),
23 |
24 | columns: computed(function() {
25 | return [
26 | { name: 'ID', valuePath: 'id', width: 180 },
27 | { name: 'A', valuePath: 'a', width: 180 },
28 | { name: 'B', valuePath: 'b', width: 180 },
29 | { name: 'C', valuePath: 'c', width: 180 },
30 | ];
31 | }),
32 |
33 | rows: computed(function() {
34 | return A();
35 | }),
36 |
37 | didInsertElement() {
38 | this._super(...arguments);
39 | this.loadMore.perform();
40 | },
41 |
42 | // ember-concurrency task
43 | loadMore: task(function*() {
44 | let offset = this.offset;
45 | let limit = this.limit;
46 |
47 | if (!this.canLoadMore) {
48 | return;
49 | }
50 |
51 | // substitute paginated API request
52 | yield timeout(1000);
53 |
54 | let newRows = [];
55 | for (let i = 0; i < limit; i++) {
56 | newRows.push({ id: offset + i + 1, a: 'A', b: 'B', c: 'C' });
57 | }
58 |
59 | this.rows.pushObjects(newRows);
60 | this.set('offset', offset + limit);
61 | }).drop(),
62 |
63 | // END-SNIPPET
64 | });
65 |
--------------------------------------------------------------------------------
/addon/-private/meta-cache.js:
--------------------------------------------------------------------------------
1 | import { get } from '@ember/object';
2 |
3 | export function getOrCreate(obj, cache, Class) {
4 | if (cache.has(obj) === false) {
5 | cache.set(obj, Class.create ? Class.create() : new Class());
6 | }
7 |
8 | return cache.get(obj);
9 | }
10 |
11 | /**
12 | * Substitute for `Map` that allows non-identical object keys to share
13 | * identical values by specifying a key path for the associating keys.
14 | *
15 | * If no key path is specified, it behaves like a `Map`.
16 | *
17 | * @class MetaCache
18 | * @constructor
19 | * @param {Object} options
20 | */
21 | export default class MetaCache {
22 | constructor({ keyPath } = {}) {
23 | this.keyPath = keyPath;
24 |
25 | // in order to prevent memory leaks, we need to be able to clean the cache
26 | // manually when the table is destroyed or updated; this is why we use a
27 | // Map instead of WeakMap
28 | this._map = new Map();
29 | }
30 |
31 | get(obj) {
32 | let key = this._keyFor(obj);
33 | let entry = this._map.get(key);
34 | return entry ? entry[1] : entry;
35 | }
36 |
37 | getOrCreate(obj, Class) {
38 | return getOrCreate(obj, this, Class);
39 | }
40 |
41 | set(obj, meta) {
42 | let key = this._keyFor(obj);
43 | this._map.set(key, [obj, meta]);
44 | }
45 |
46 | has(obj) {
47 | let key = this._keyFor(obj);
48 | return this._map.has(key);
49 | }
50 |
51 | delete(obj) {
52 | let key = this._keyFor(obj);
53 | this._map.delete(key);
54 | }
55 |
56 | entries() {
57 | return this._map.values();
58 | }
59 |
60 | _keyFor(obj) {
61 | // falls back to `obj` as key if a legitimate key cannot be produced
62 | if (!obj || !this.keyPath) {
63 | return obj;
64 | }
65 |
66 | let key = get(obj, this.keyPath);
67 | return key ? key : obj;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/examples/sorting-empty-values/component.js:
--------------------------------------------------------------------------------
1 | import Component from '@ember/component';
2 | import { computed } from '@ember/object';
3 | import faker from 'faker';
4 | import { getRandomInt } from 'dummy/utils/generators';
5 |
6 | export default Component.extend({
7 | // BEGIN-SNIPPET docs-example-sorting-empty-values.js
8 | columns: computed(function() {
9 | return [
10 | { name: 'Product', valuePath: 'name' },
11 | { name: 'Material', valuePath: 'material' },
12 | { name: 'Price', valuePath: 'price' },
13 | { name: 'Sold', valuePath: 'sold' },
14 | { name: 'Unsold', valuePath: 'unsold' },
15 | { name: 'Total Revenue', valuePath: 'totalRevenue' },
16 | ];
17 | }),
18 |
19 | sortEmptyLast: true,
20 | // END-SNIPPET
21 |
22 | rows: computed(function() {
23 | let rows = [];
24 |
25 | for (let k = 0; k < 10; k++) {
26 | let sold = getRandomInt(100, 10);
27 | let unsold = getRandomInt(100, 10);
28 | let price = getRandomInt(50, 10);
29 | let totalRevenue = price * sold;
30 |
31 | let product = {
32 | name: faker.commerce.productName(),
33 | material: faker.commerce.productMaterial(),
34 | price: `$${price}`,
35 | sold,
36 | unsold,
37 | totalRevenue: `$${totalRevenue}`,
38 | };
39 |
40 | rows.push(product);
41 | }
42 |
43 | for (let k = 0; k < 5; k++) {
44 | let sold = getRandomInt(100, 10);
45 | let unsold = getRandomInt(100, 10);
46 | let price = getRandomInt(50, 10);
47 | let totalRevenue = price * sold;
48 |
49 | let product = {
50 | name: faker.commerce.productName(),
51 | material: '',
52 | price: `$${price}`,
53 | sold,
54 | unsold,
55 | totalRevenue: `$${totalRevenue}`,
56 | };
57 |
58 | rows.push(product);
59 | }
60 |
61 | return rows;
62 | }),
63 | });
64 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/examples/selection-modes/template.hbs:
--------------------------------------------------------------------------------
1 | {{! BEGIN-SNIPPET docs-example-selection-modes.hbs }}
2 |
3 |
4 |
5 |
6 |
16 |
17 |
18 |
19 |
Current selection
20 |
{{currentSelection}}
21 |
22 |
23 |
rowSelectionMode
24 |
25 |
26 |
27 |
28 |
29 |
checkboxSelectionMode
30 |
31 |
32 |
33 |
34 |
35 |
selectingChildrenSelectsParent
36 |
37 |
38 |
39 | {{! END-SNIPPET }}
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/guides/header/column-keys.md:
--------------------------------------------------------------------------------
1 | ## Column Keys
2 |
3 | Ember Table will update the table layout as the `columns` array changes. When
4 | objects are removed and re-added to the array, meta data about the state of
5 | the column is preserved and recovered. By default, Ember Table uses the objects
6 | themselves as keys to save and retrieve this data.
7 |
8 | If your application is written in a functional style, this can pose a problem.
9 | For example, if your columns are generated by a getter that depends on tracked
10 | properties, you may be frequently passing new columns to Ember Table that aren't
11 | really new at all.
12 |
13 | ```js
14 | get columns() {
15 | // `data` could be a tracked property that updates frequently
16 | return this.data.headers.map((header, index) => {
17 | heading: header.name,
18 | valuePath: `${index}`,
19 | // ...
20 | });
21 | }
22 | ```
23 |
24 | This can have unexpected consequences, as meta data about your columns
25 | is being lost each time `columns` is re-computed.
26 |
27 | To handle this scenario, you can specify a `columnKeyPath` property that
28 | identifies a "key" property on each column object. This acts like a primary key
29 | in a database, identifying when two objects represent the same underlying
30 | entity. When `columns` is updated, these keys are used to preserve the state
31 | of your table, even if the replacement column objects are not identical to the
32 | originals.
33 |
34 | ```js
35 | get columns() {
36 | return this.data.headers.map((header, index) => {
37 | heading: header.name,
38 | valuePath: `${index}`,
39 |
40 | // assumes headers have distinct names
41 | key: header.name,
42 |
43 | // ...
44 | });
45 | }
46 | ```
47 |
48 | ```hbs
49 |
50 |
54 |
55 |
56 |
57 | ```
58 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{outlet}}
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/tests/unit/-private/meta-cache-test.js:
--------------------------------------------------------------------------------
1 | import MetaCache from 'ember-table/-private/meta-cache';
2 | import { module, test } from 'qunit';
3 |
4 | let metaCache;
5 |
6 | module('Unit | Private | MetaCache', function(hooks) {
7 | hooks.beforeEach(function() {
8 | metaCache = new MetaCache();
9 | });
10 |
11 | test('it behaves like Map with no keyPath set', function(assert) {
12 | let a = { id: 1 };
13 | let b = { id: 1 };
14 |
15 | metaCache.set(a, 1);
16 | assert.strictEqual(metaCache.get(a), 1, 'gets value by added object key');
17 | assert.true(metaCache.has(a), 'contains added object key');
18 | assert.strictEqual(metaCache.get(b), undefined, 'gets undefined value when object key unknown');
19 | assert.false(metaCache.has(b), 'does not contain unknown object key');
20 |
21 | metaCache.delete(a);
22 | assert.strictEqual(metaCache.get(a), undefined, 'gets undefined value when object key removed');
23 | assert.false(metaCache.has(a), 'does not contain removed object key');
24 | });
25 |
26 | test('it uses keyPath for cache lookup', function(assert) {
27 | let a = { id: 1 };
28 | let b = { id: 1 };
29 |
30 | metaCache.keyPath = 'id';
31 |
32 | metaCache.set(a, 1);
33 | assert.strictEqual(metaCache.get(a), 1, 'gets value by added object key');
34 | assert.true(metaCache.has(a), 'contains added object key');
35 | assert.strictEqual(metaCache.get(b), 1, 'gets same value with equivalent object key');
36 | assert.true(metaCache.has(b), 'contains equivalent object key');
37 |
38 | metaCache.delete(a);
39 | assert.strictEqual(metaCache.get(a), undefined, 'gets undefined value when object key removed');
40 | assert.false(metaCache.has(a), 'does not contain removed object key');
41 | assert.strictEqual(
42 | metaCache.get(b),
43 | undefined,
44 | 'gets undefined value when equivalent key removed'
45 | );
46 | assert.false(metaCache.has(b), 'does not contain equivalent object key');
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/docs/guides/header/subcolumns.js:
--------------------------------------------------------------------------------
1 | import Controller from '@ember/controller';
2 | import { A as emberA } from '@ember/array';
3 | import { computed } from '@ember/object';
4 | import { generateRows, generateColumn } from 'dummy/utils/generators';
5 |
6 | const COLUMN_COUNT = 4;
7 |
8 | export default Controller.extend({
9 | rows: computed(function() {
10 | return generateRows(100);
11 | }),
12 |
13 | // BEGIN-SNIPPET docs-example-subcolumns.js
14 | simpleColumns: computed(function() {
15 | return [
16 | {
17 | name: 'A',
18 | subcolumns: [
19 | { name: 'A A', valuePath: 'A A' },
20 | { name: 'A B', valuePath: 'A B' },
21 | { name: 'A C', valuePath: 'A C' },
22 | ],
23 | },
24 | {
25 | name: 'B',
26 | subcolumns: [
27 | { name: 'B A', valuePath: 'B A' },
28 | { name: 'B B', valuePath: 'B B' },
29 | { name: 'B C', valuePath: 'B C' },
30 | ],
31 | },
32 | {
33 | name: 'C',
34 | subcolumns: [
35 | { name: 'C A', valuePath: 'C A' },
36 | { name: 'C B', valuePath: 'C B' },
37 | { name: 'C C', valuePath: 'C C' },
38 | ],
39 | },
40 | ];
41 | }),
42 | // END-SNIPPET
43 |
44 | complexColumns: computed(function() {
45 | let columns = emberA();
46 |
47 | for (let i = 0; i < COLUMN_COUNT; i++) {
48 | let column = generateColumn(i, { subcolumns: [] });
49 |
50 | if (Math.random() > 0.5) {
51 | for (let j = 0; j < COLUMN_COUNT - 1; j++) {
52 | let subcolumn = generateColumn([i, j], { subcolumns: [] });
53 |
54 | if (Math.random() > 0.5) {
55 | for (let k = 0; k < COLUMN_COUNT - 2; k++) {
56 | subcolumn.subcolumns.push(generateColumn([i, j, k]));
57 | }
58 | }
59 |
60 | column.subcolumns.push(subcolumn);
61 | }
62 | }
63 |
64 | columns.pushObject(column);
65 | }
66 |
67 | return columns;
68 | }),
69 | });
70 |
--------------------------------------------------------------------------------
/tests/dummy/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Ember Table
7 |
8 |
9 |
10 | {{content-for "head"}}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
39 |
40 |
41 | {{content-for "head-footer"}}
42 |
43 |
44 | {{content-for "body"}}
45 |
46 |
47 |
48 |
49 | {{content-for "body-footer"}}
50 |
51 |
52 |
--------------------------------------------------------------------------------
/tests/dummy/app/styles/tables.scss:
--------------------------------------------------------------------------------
1 | .demo-options {
2 | & > * + * {
3 | padding-left: 2em;
4 | }
5 | padding-top: 0.5em;
6 | padding-bottom: 0.5em;
7 |
8 | &:not(:last-child) {
9 | padding-bottom: 1em;
10 | }
11 |
12 | label {
13 | white-space: nowrap;
14 | }
15 | }
16 |
17 | .demo-options-heading {
18 | text-transform: uppercase;
19 | }
20 |
21 | .demo-current-selection {
22 | font-weight: bold;
23 | }
24 |
25 | .demo-options-group {
26 | h4 {
27 | font-weight: normal;
28 | font-size: larger;
29 | }
30 |
31 | display: flex;
32 |
33 | & *:first-child {
34 | flex-basis: 40%;
35 | }
36 |
37 | & > * + * {
38 | padding-left: 2em;
39 | }
40 | }
41 |
42 | .demo-container {
43 | position: relative;
44 | width: 100%;
45 | height: 400px;
46 |
47 | &.small {
48 | height: 200px;
49 | }
50 |
51 | &.fixed-width {
52 | width: 800px;
53 | }
54 | }
55 |
56 | .vertical-borders {
57 | td,
58 | th {
59 | &:not(:first-child) {
60 | box-sizing: border-box;
61 | border-left: 1px solid #ddd;
62 | }
63 | }
64 | }
65 |
66 | .et-table {
67 | td,
68 | th {
69 | background: #ffffff;
70 | }
71 | }
72 |
73 | /* Custom component classes */
74 | .custom-component-container {
75 | position: relative;
76 | width: 360px;
77 | height: 300px;
78 | }
79 |
80 | .cell-image {
81 | height: 40px;
82 | width: 40px;
83 | }
84 |
85 | .custom-row {
86 | background: #00ff00;
87 | }
88 |
89 | .custom-footer {
90 | background-color: #00ff00;
91 | }
92 |
93 | .info-table {
94 | margin: 2em auto;
95 |
96 | th {
97 | background-color: #f1f5f8;
98 | }
99 |
100 | th,
101 | td {
102 | padding: 0.5em;
103 | border: 1px solid #555;
104 |
105 | &.center {
106 | text-align: center;
107 | }
108 |
109 | &.highlight {
110 | background-color: #96c178;
111 | }
112 | }
113 |
114 | td:not(:first-child) {
115 | font-weight: bold;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/addon/-private/utils/observer.js:
--------------------------------------------------------------------------------
1 | import { gte } from 'ember-compatibility-helpers';
2 | import { assert } from '@ember/debug';
3 |
4 | // eslint-disable-next-line no-restricted-imports
5 | import { observer as emberObserver } from '@ember/object';
6 |
7 | import {
8 | // eslint-disable-next-line no-restricted-imports
9 | addObserver as emberAddObserver,
10 | // eslint-disable-next-line no-restricted-imports
11 | removeObserver as emberRemoveObserver,
12 | } from '@ember/object/observers';
13 |
14 | const USE_ASYNC_OBSERVERS = gte('3.13.0');
15 |
16 | function asyncObserver(...args) {
17 | let fn = args.pop();
18 | let dependentKeys = args;
19 | let sync = false;
20 |
21 | // eslint-disable-next-line ember/no-observers
22 | return emberObserver({ dependentKeys, fn, sync });
23 | }
24 |
25 | function asyncAddObserver(...args) {
26 | let obj, path, target, method;
27 | let sync = false;
28 | obj = args[0];
29 | path = args[1];
30 | assert(
31 | `Expected 3 or 4 args for addObserver, got ${args.length}`,
32 | args.length === 3 || args.length === 4
33 | );
34 | if (args.length === 3) {
35 | target = null;
36 | method = args[2];
37 | } else if (args.length === 4) {
38 | target = args[2];
39 | method = args[3];
40 | }
41 |
42 | // eslint-disable-next-line ember/no-observers
43 | return emberAddObserver(obj, path, target, method, sync);
44 | }
45 |
46 | function asyncRemoveObserver(...args) {
47 | let obj, path, target, method;
48 | let sync = false;
49 | obj = args[0];
50 | path = args[1];
51 | assert(
52 | `Expected 3 or 4 args for addObserver, got ${args.length}`,
53 | args.length === 3 || args.length === 4
54 | );
55 | if (args.length === 3) {
56 | target = null;
57 | method = args[2];
58 | } else {
59 | target = args[2];
60 | method = args[3];
61 | }
62 | return emberRemoveObserver(obj, path, target, method, sync);
63 | }
64 |
65 | export const observer = USE_ASYNC_OBSERVERS ? asyncObserver : emberObserver;
66 | export const addObserver = USE_ASYNC_OBSERVERS ? asyncAddObserver : emberAddObserver;
67 | export const removeObserver = emberRemoveObserver ? asyncRemoveObserver : emberRemoveObserver;
68 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/guides/main/styling-the-table.md:
--------------------------------------------------------------------------------
1 | # Styling the Table [WIP]
2 |
3 | Ember Table by default only ships with the minimum CSS necessary for it to
4 | function. This includes the `sticky` positioning of the header, footer, and
5 | fixed columns, along with a few other things.
6 |
7 | You can style the table by directly styling the `.ember-table` class, and the
8 | HTML structure beneath it. The examples in this documentation app use Addepar's
9 | own [CSS framework](https://github.com/Addepar/addepar-style-toolbox), which is
10 | tailored to our specific needs. You can check out the [table styles](https://github.com/Addepar/addepar-style-toolbox/blob/master/styles/onyx/components/table/_core.scss)
11 | applied there for inspiration.
12 |
13 | In the future, this page will contain a class list and outline of the base HTML
14 | for Ember Table.
15 |
16 | ## Reorder indicators
17 |
18 | When reordering the columns, two elements are created to be customized and allow the user to understand what he is doing.
19 |
20 | A first `.et-reorder-main-indicator` element is created, which basically is a ghost copy of the header cell currently grabbed. By default, it has no CSS property, giving a `position: absolute;` so this element is positioned on the `` is recommended.
21 |
22 | A second `.et-reorder-drop-indicator` element is created, which is the target header cell. It has two CSS classes `.et-reorder-direction-left` and `.et-reorder-direction-right` depending on the current drop position to show whether the drop will be directed on the left or on the right. Styling these will give the user a more intuitive experience of how they is reordering their table.
23 |
24 | ## Styling Examples
25 |
26 | Here is an example of using CSS Flex properties to create a layout that has a fixed page header and footer and a content area that is split for a search input area on the left and a data table on the right: [CSS Flex](https://ember-twiddle.com/4cb616452e3316ddcec242192fc4a96c?openFiles=templates.application.hbs%2C). The same layout using Bootstrap classes: [Bootstrap Flex](https://ember-twiddle.com/d27c9f154050688518a7ca9a0b055a26?openFiles=templates.application.hbs%2C). This version is also somewhat responsive, so change your window sizes to see it respond.
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/styles/ember-table/default.scss:
--------------------------------------------------------------------------------
1 | /************************************************************************************************/
2 | /* Table default style */
3 | /************************************************************************************************/
4 | $table-header-background-color: #f8f8f8;
5 | $table-border-color: #dcdcdc;
6 | $table-hover-color: #e5edf8;
7 |
8 | .ember-table {
9 | border: solid 1px #dddddd;
10 |
11 | th,
12 | td {
13 | white-space: nowrap;
14 | text-overflow: ellipsis;
15 | overflow: hidden;
16 | font-size: 20px;
17 | padding: 4px 10px;
18 | }
19 |
20 | tbody {
21 | td {
22 | border-top: none;
23 | border-left: none;
24 | border-bottom: $table-border-color 1px dotted;
25 | border-right: $table-border-color 1px solid;
26 | background-color: #ffffff;
27 |
28 | &:last-child {
29 | border-right: none;
30 | }
31 |
32 | &.is-fixed-right {
33 | border-left: solid 1px $table-border-color;
34 | }
35 | }
36 | }
37 |
38 | th,
39 | tfoot td {
40 | padding: 5px 0 3px;
41 | background-color: $table-header-background-color;
42 | font-family: 'Univers LT W01 65 Bold';
43 | line-height: 1.4;
44 | font-weight: bold;
45 | text-align: center;
46 | box-sizing: border-box;
47 | }
48 |
49 | tfoot td {
50 | border-top: 1px solid $table-border-color;
51 | border-right: solid 1px $table-border-color;
52 |
53 | &:last-child {
54 | border-right: none;
55 | }
56 | }
57 |
58 | thead th {
59 | border-bottom: 1px solid $table-border-color;
60 | border-right: solid 1px $table-border-color;
61 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
62 | position: relative;
63 | overflow: hidden;
64 |
65 | &:last-child {
66 | border-right: none;
67 | }
68 |
69 | &.is-fixed-right {
70 | border-left: solid 1px $table-border-color;
71 |
72 | .et-header-resize-area {
73 | left: 0;
74 | }
75 | }
76 | }
77 |
78 | tr:hover {
79 | th {
80 | cursor: pointer;
81 | }
82 |
83 | td {
84 | background-color: $table-hover-color;
85 | }
86 | }
87 |
88 | tr.is-selected {
89 | td,
90 | th {
91 | background-color: #227ecb;
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/guides/main/basic-table.md:
--------------------------------------------------------------------------------
1 | # A Basic Table
2 |
3 | Tables can get very complicated, and it's not easy to have a table API that is
4 | powerful, flexible, and succinct. Ember Table makes this tradeoff by providing a
5 | very flexible API, but at the cost of being fairly verbose. Because this table
6 | is meant for power users who need a lot of functionality and flexibility, this
7 | tradeoff generally makes sense.
8 |
9 | The table has a set of sane defaults. If you don't need much customization,
10 | setting up a minimal instance of Ember Table will only require you to define a
11 | header and a body, with columns and rows passed to it.
12 |
13 | {{#docs-demo as |demo|}}
14 | {{#demo.example}}
15 |
24 | {{/demo.example}}
25 |
26 | {{demo.snippet name='docs-example-basic-table.hbs'}}
27 | {{demo.snippet label='component.js' name='docs-example-basic-table.js'}}
28 | {{/docs-demo}}
29 |
30 | ## High Level Structure
31 |
32 | At a high level, the structure of Ember Table is meant to mimic the structure of
33 | HTML tables directly. This allows you to customize each element in the table;
34 | you can add class names, setup actions, and handle events anywhere.
35 |
36 | This example demonstrates the same table as above, but with each level yielded.
37 |
38 | {{#docs-demo as |demo|}}
39 | {{#demo.example name='expanded'}}
40 |
19 | {{/demo.example}}
20 |
21 | {{demo.snippet name='docs-example-fixed-columns.hbs'}}
22 | {{demo.snippet name='docs-example-fixed-columns.js' label='component.js'}}
23 | {{/docs-demo}}
24 |
25 | ## Multiple Fixed Columns and Ordering
26 |
27 | Multiple columns may be fixed to either side of the table. Fixed columns _must_
28 | be placed contiguously at the start or end of the `columns` array. If columns
29 | are marked as fixed and are out of order, Ember Table will sort the columns
30 | array directly to fix the ordering.
31 |
32 | {{#docs-demo as |demo|}}
33 | {{#demo.example name='out-of-order-fixed-columns'}}
34 |
22 | {{/demo.example}}
23 |
24 | {{demo.snippet name='docs-example-scroll-indicators.hbs'}}
25 | {{demo.snippet name='docs-example-scroll-indicators.js' label='component.js'}}
26 | {{/docs-demo}}
27 |
28 | ## Horizontal Scroll Indicators with Fixed Columns
29 |
30 | Horizontal indicators will respect fixed columns, appearing inside of
31 | them when they are present, or at the edges of the table when they are not.
32 |
33 | {{#docs-demo as |demo|}}
34 | {{#demo.example name='scroll-indicators-with-fixed'}}
35 |
75 | {{/demo.example}}
76 |
77 | {{demo.snippet name='docs-example-scroll-indicators-with-footer.hbs'}}
78 | {{demo.snippet name='docs-example-scroll-indicators-with-footer.js' label='component.js'}}
79 | {{/docs-demo}}
80 |
--------------------------------------------------------------------------------
/addon/-private/utils/reorder-indicators.js:
--------------------------------------------------------------------------------
1 | import { getOuterClientRect, getInnerClientRect } from './element';
2 |
3 | function createElement(mainClass, dimensions) {
4 | let element = document.createElement('div');
5 |
6 | element.classList.add(mainClass);
7 |
8 | for (let key in dimensions) {
9 | element.style[key] = `${dimensions[key]}px`;
10 | }
11 |
12 | return element;
13 | }
14 |
15 | class ReorderIndicator {
16 | constructor(container, scale, element, bounds, mainClass, child) {
17 | this.container = container;
18 | this.element = element;
19 | this.bounds = bounds;
20 | this.child = child;
21 |
22 | let scrollTop = this.container.scrollTop;
23 | let scrollLeft = this.container.scrollLeft;
24 |
25 | let { top: containerTop, left: containerLeft } = getInnerClientRect(this.container, scale);
26 |
27 | let { top: elementTop, left: elementLeft, width: elementWidth } = getOuterClientRect(
28 | this.element
29 | );
30 |
31 | let top = (elementTop - containerTop) * scale + scrollTop;
32 | let left = (elementLeft - containerLeft) * scale + scrollLeft;
33 | let width = elementWidth * scale;
34 |
35 | this.originLeft = left;
36 | this.indicatorElement = createElement(mainClass, { top, left, width });
37 |
38 | if (child) {
39 | this.indicatorElement.appendChild(child);
40 | }
41 |
42 | this.container.appendChild(this.indicatorElement);
43 | this._left = left;
44 | }
45 |
46 | destroy() {
47 | this.container.removeChild(this.indicatorElement);
48 | }
49 |
50 | set width(newWidth) {
51 | this.indicatorElement.style.width = `${newWidth}px`;
52 | }
53 |
54 | get left() {
55 | return this._left;
56 | }
57 |
58 | set left(newLeft) {
59 | let { leftBound, rightBound } = this.bounds;
60 |
61 | let width = this.indicatorElement.offsetWidth;
62 |
63 | if (newLeft < leftBound) {
64 | newLeft = leftBound;
65 | } else if (newLeft + width > rightBound) {
66 | newLeft = rightBound - width;
67 | }
68 |
69 | if (newLeft < this.originLeft) {
70 | this.indicatorElement.classList.remove('et-reorder-direction-right');
71 | this.indicatorElement.classList.add('et-reorder-direction-left');
72 | } else {
73 | this.indicatorElement.classList.remove('et-reorder-direction-left');
74 | this.indicatorElement.classList.add('et-reorder-direction-right');
75 | }
76 |
77 | this.indicatorElement.style.left = `${newLeft}px`;
78 | this._left = newLeft;
79 | }
80 | }
81 |
82 | export class MainIndicator extends ReorderIndicator {
83 | constructor(container, scale, element, bounds) {
84 | let child = element.cloneNode(true);
85 |
86 | super(container, scale, element, bounds, 'et-reorder-main-indicator', child);
87 | }
88 | }
89 |
90 | export class DropIndicator extends ReorderIndicator {
91 | constructor(container, scale, element, bounds) {
92 | super(container, scale, element, bounds, 'et-reorder-drop-indicator');
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/guides/body/occlusion.md:
--------------------------------------------------------------------------------
1 | # Occlusion
2 |
3 | Rendering lots and lots of HTML is really expensive for the browser, much more
4 | expensive than tracking those things in Javascript. Occlusion is a technique
5 | where we only render the HTML that is visible to the user at a given time. This
6 | allows us to load and present much more data than would otherwise be possible.
7 |
8 | Ember Table uses [vertical-collection](https://github.com/html-next/vertical-collection)
9 | by default to occlude rows of the table. This allows the table to render tens of
10 | thousands of rows without any performance hiccups.
11 |
12 | The occlusion also accounts for variable row heights automatically - no need to
13 | have static row heights, or to know the row heights in advance.
14 |
15 | ## Configuring Occlusion
16 |
17 | You can pass some parameters to the table body to fine tune the occlusion
18 | settings. The current options are:
19 |
20 | * `estimateRowHeight`: Vertical-collection figures out what your row heights
21 | are by measuring them after they have rendered. The first time each row is
22 | rendered, it assumes the row's height will be whatever value is provided by
23 | the `estimateRowHeight` in pixels (defaults to `30`). A more accurate estimate
24 | is always better, as it means vertical-collection will have less work to do
25 | if the "guess" was incorrect.
26 |
27 | * `staticHeight`: This field is a boolean flag that defaults to `false`. If you
28 | enable this field, vertical-collection will assume that all of the rows'
29 | heights are _exactly_ the value of `estimateRowHeight`. This will mean less
30 | work for vertical-collection and will be slightly more performant.
31 |
32 | Vertical-collection will **not** apply style changes to your rows if you
33 | pass `staticHeight=true`. You are responsible for ensuring that your rows are
34 | styled to always be the same as `estimateRowHeight`.
35 |
36 | * `key`: This key is the property used by the vertical-collection to determine
37 | whether an array mutation is an append, prepend, or complete replacement. It
38 | defaults to the object identity `"@identity"`.
39 |
40 | * `containerSelector`: A selector string used by the vertical-collection to select
41 | the element from which to calculate the viewable height. It defaults to the
42 | table id `"#{tableId}"`.
43 |
44 | {{#docs-demo as |demo|}}
45 | {{#demo.example}}
46 | {{! BEGIN-SNIPPET docs-example-occlusion.hbs }}
47 |
17 | {{/demo.example}}
18 |
19 | {{demo.snippet name='docs-example-rows.hbs'}}
20 | {{demo.snippet label='component.js' name='docs-example-rows.js'}}
21 | {{/docs-demo}}
22 |
23 | The value passed to each cell in the table is determined by the `valuePath` of
24 | the `column` object. A simplified version of this in handlebars would look like
25 | this:
26 |
27 | ```hbs
28 | {{#each this.rows as |row|}}
29 |
30 | {{#each columns as |column|}}
31 |
32 | {{yield (get row column.valuePath)}}
33 |
34 | {{/each}}
35 |
36 | {{/each}}
37 | ```
38 |
39 | ## Trees and Children
40 |
41 | By default, Ember Table handles trees of rows. Each row can have a `children`
42 | property which is another array of rows. Children are treated the same way as
43 | parents - cells will attempt to find a value by getting the value at the value
44 | path on the child.
45 |
46 | If you want to disable the tree behavior, you can pass `enableTree=false` to
47 | the table body.
48 |
49 | {{#docs-demo as |demo|}}
50 | {{#demo.example name="trees"}}
51 | {{! BEGIN-SNIPPET docs-example-tree-rows.hbs }}
52 |
53 |
57 |
58 |
59 |
60 |
61 |
62 |
66 |
67 |
68 | {{! END-SNIPPET }}
69 | {{/demo.example}}
70 |
71 | {{demo.snippet label='component.js' name='docs-example-tree-rows.js'}}
72 | {{demo.snippet name='docs-example-tree-rows.hbs'}}
73 | {{/docs-demo}}
74 |
75 | ## Collapsing Rows
76 |
77 | Trees with children are collapsible by default. You can set the `isCollapsed`
78 | property directly on rows to control the collapse state of rows externally. If
79 | you set `isCollapsed`, the table will update it when the user collapses or
80 | uncollapses a row. Otherwise, it will keep the state internally only.
81 |
82 | If you want to disable collapsing, you can pass `enableCollapse=false` to the
83 | table body.
84 |
85 | If you want to disable collapsing at a row level, you can pass
86 | `disableCollapse=true` to the row.
87 |
88 | {{#docs-demo as |demo|}}
89 | {{#demo.example name="collapse"}}
90 | {{! BEGIN-SNIPPET docs-example-rows-with-collapse.hbs }}
91 |
92 |
96 |
97 |
98 |
99 |
100 |
101 |
105 |
106 |
107 | {{! END-SNIPPET }}
108 | {{/demo.example}}
109 |
110 | {{demo.snippet label='component.js' name='docs-example-rows-with-collapse.js'}}
111 | {{demo.snippet name='docs-example-rows-with-collapse.hbs'}}
112 | {{/docs-demo}}
113 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/guides/main/table-meta-data.md:
--------------------------------------------------------------------------------
1 | # Table Meta Data
2 |
3 | So far you've seen how Ember Table revolves around three central concepts:
4 |
5 | 1. The `rowValue`, which is one of the rows that are provided to the body
6 | 2. The `columnValue`, which is one of the columns that are provided to the
7 | header
8 | 3. The `cellValue`, which is produced by using the `columnValue` to lookup a
9 | value on the `rowValue`
10 |
11 | These are the fundamental building blocks of any table, so it makes sense that
12 | they would be what is given to you when using the table API.
13 |
14 | You'll also find that Ember Table provides a meta object that is associated
15 | with each of these. These meta objects are yielded after the main
16 | cell/column/row values at the cell level, and are generally accessible wherever
17 | their corresponding values are:
18 |
19 | ```hbs
20 |
21 | ```
22 |
23 | ## What are meta objects?
24 |
25 | The meta objects are unique POJOs that are associated with a corresponding
26 | value. That is to say, for every `cell`, `column`, and `row` in the table, there
27 | are corresponding `cellMeta`, `columnMeta`, and `rowMeta` objects.
28 |
29 | `columnMeta` and `rowMeta` objects are used by the table to accomplish some
30 | internal bookkeeping such as collapse and selection state, but you are free to
31 | use these objects to store whatever meta information you would like in the
32 | table.
33 |
34 | `rowsCount` is also yielded by the cell component. This count is a reflection
35 | of how many rows the user can currently see by scrolling through the table. It
36 | is typically smaller than the total number of rows passed into, say, the
37 | `ember-tbody` component, because it excludes rows that have been hidden by
38 | collapsing a parent.
39 |
40 | ## What are they used for?
41 |
42 | Complex data tables have lots of functionality that requires some amount of
43 | state to be tracked. This state is generally unique to the table, and oftentimes
44 | related to a particular cell, column, or row. A good example of this is cell
45 | selection, like in Excel.
46 |
47 | When you click a cell in Excel, the row, column, and cell are all marked as
48 | active to show the user where they are in the table. Ember Table does _not_ have
49 | this functionality out of the box - let's see how we would implement it with
50 | meta objects:
51 |
52 |
53 | {{main/table-meta-data/cell-selection}}
54 |
55 | ## Accessing row indices in templates
56 |
57 | Meta objects can be used in templates to render conditional markdown based on
58 | the index of the current row.
59 |
60 | {{#docs-demo as |demo|}}
61 | {{#demo.example name="row-indices"}}
62 |
95 | {{/demo.example}}
96 |
97 | {{demo.snippet name='table-meta-data-row-indices.hbs'}}
98 | {{demo.snippet name='table-meta-data-row-indices.css'}}
99 | {{/docs-demo}}
100 |
--------------------------------------------------------------------------------
/addon/components/ember-tr/component.js:
--------------------------------------------------------------------------------
1 | import Component from '@ember/component';
2 | import { computed } from '@ember/object';
3 | import { readOnly } from '@ember/object/computed';
4 |
5 | import { closest } from '../../-private/utils/element';
6 |
7 | import layout from './template';
8 | import { SELECT_MODE } from '../../-private/collapse-tree';
9 |
10 | /**
11 | The table row component. This component manages row level concerns, and yields
12 | an API object that contains the cell component, the cell/column/row values,
13 | and the cell/column/row meta objects. It is used in both the header and the
14 | body, mirroring the structure of native HTML tables.
15 |
16 | ```hbs
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ```
31 |
32 | @yield {object} row - the API object yielded by the table row
33 | @yield {Component} row.cell - The table cell contextual component
34 |
35 | @yield {any} row.cellValue - The value for the currently yielded cell
36 | @yield {object} row.cellMeta - The meta for the currently yielded cell
37 |
38 | @yield {object} row.columnValue - The value for the currently yielded column
39 | @yield {object} row.columnMeta - The meta for the currently yielded column
40 |
41 | @yield {object} row.rowValue - The value for the currently yielded row
42 | @yield {object} row.rowMeta - The meta for the currently yielded row
43 |
44 | @class
45 | @public
46 | */
47 | export default Component.extend({
48 | layout,
49 | tagName: 'tr',
50 | classNames: ['et-tr'],
51 | classNameBindings: ['isEven:is-even:is-odd', 'isGroupSelected', 'isSelectable', 'isSelected'],
52 |
53 | /**
54 | The API object passed in by the table body, header, or footer
55 | @argument api
56 | @required
57 | @type object
58 | */
59 | api: null,
60 |
61 | /**
62 | Action sent when the user clicks this element
63 | @argument onClick
64 | @type Action?
65 | */
66 | onClick: null,
67 |
68 | /**
69 | Action sent when the user double clicks this element
70 | @argument onDoubleClick
71 | @type Action?
72 | */
73 | onDoubleClick: null,
74 |
75 | rowValue: readOnly('api.rowValue'),
76 |
77 | rowMeta: readOnly('api.rowMeta'),
78 |
79 | cells: readOnly('api.cells'),
80 |
81 | rowSelectionMode: readOnly('api.rowSelectionMode'),
82 |
83 | rowToggleMode: readOnly('api.rowToggleMode'),
84 |
85 | isHeader: readOnly('api.isHeader'),
86 |
87 | isSelected: readOnly('rowMeta.isSelected'),
88 |
89 | isGroupSelected: readOnly('rowMeta.isGroupSelected'),
90 |
91 | isEven: computed('rowMeta.index', function() {
92 | let index = this.rowMeta?.index ?? 0;
93 | return index % 2 === 0;
94 | }),
95 |
96 | isSelectable: computed('rowSelectionMode', function() {
97 | let rowSelectionMode = this.get('rowSelectionMode');
98 |
99 | return rowSelectionMode === SELECT_MODE.MULTIPLE || rowSelectionMode === SELECT_MODE.SINGLE;
100 | }),
101 |
102 | click(event) {
103 | let rowSelectionMode = this.get('rowSelectionMode');
104 | let inputParent = closest(event.target, 'input, button, label, a, select');
105 |
106 | if (!inputParent) {
107 | let rowMeta = this.get('rowMeta');
108 |
109 | if (rowMeta && rowSelectionMode === SELECT_MODE.MULTIPLE) {
110 | let toggle = event.ctrlKey || event.metaKey || this.get('rowToggleMode');
111 | let range = event.shiftKey;
112 |
113 | rowMeta.select({ toggle, range });
114 | } else if (rowMeta && rowSelectionMode === SELECT_MODE.SINGLE) {
115 | rowMeta.select({ single: true });
116 | }
117 | }
118 |
119 | this.sendEventAction('onClick', event);
120 | },
121 |
122 | doubleClick(event) {
123 | this.sendEventAction('onDoubleClick', event);
124 | },
125 |
126 | sendEventAction(action, event) {
127 | let rowValue = this.get('rowValue');
128 | let rowMeta = this.get('rowMeta');
129 |
130 | let closureAction = this[action];
131 |
132 | closureAction?.({
133 | event,
134 | rowValue,
135 | rowMeta,
136 | });
137 | },
138 | });
139 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/why-ember-table.md:
--------------------------------------------------------------------------------
1 | # Why Ember Table?
2 |
3 | ## Tables are Hard
4 |
5 | They've been with us, for better or for worse, since the early
6 | days of computing, helping us sort through mounds of data and generate TPS
7 | reports. Excel macros are practically Turing complete at this point, and just
8 | about every UX pattern imaginable has been explored and implemented in a table
9 | in some application somewhere.
10 |
11 | Building that functionality into your Single Page App is a long, tiresome
12 | process, and as the datasets scale it just gets harder. There are a lot of
13 | different off the shelf tables out there, but customizing them is difficult and
14 | time consuming, and if they don't implement the feature you need there's no easy
15 | way to add it.
16 |
17 | ## What makes Ember Table different?
18 |
19 | Most table components try to abstract the complexity of the table away from
20 | users. They provide a few top components that are meant to make declaring a
21 | table simple, avoiding the nested HTML structure of traditional tables. After
22 | all, who wants to deal with figuring out the right ordering of `tbody`, `th`,
23 | and `td` tags to get everything working?
24 |
25 | The truth, however, is that tables _are_ complicated, and most attempts to
26 | reduce that complexity through abstraction actually make the problem worse. You
27 | end up having to pass tens or hundreds of configuration options to your table
28 | component to get it to work the way you want.
29 |
30 | Ember Table has decided to do the opposite. The structure of a table component
31 | mirrors the structure of an actual HTML table, giving you the ability to
32 | customize every level of your table using standard Ember templates, components,
33 | and actions:
34 |
35 | ```hbs
36 |
37 |
38 |
39 |
40 |
41 |
42 | Header
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | Cell
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Footer
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | Header
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | Cell
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | Footer
87 |
88 |
89 |
90 |
91 | ```
92 |
93 | Our stance is that rather than attempting to hide this complexity from you, we
94 | would rather give you full flexibility. It's very likely that you will want to
95 | customize the behavior of your table, and conversely, it's very _unlikely_ that
96 | you will have hundreds or thousands of data tables sprinkled throughout your
97 | app. Data tables tend to be used in a few central locations, so having a
98 | flexible API that allows you to easily flesh out those (relatively) few use
99 | cases is very valuable.
100 |
101 | ## Feature Complete OOTB
102 |
103 | Ember Table doesn't stop at giving you a solid API to build on, it also provides
104 | many features that you would expect a modern data table to have, including:
105 |
106 | - Fixed headers and footers
107 | - Fixed columns
108 | - Row selection
109 | - Row sorting
110 | - Tree tables (with group collapsing)
111 | - Column resizing and reordering
112 | - Nested subcolumns (e.g. to create pivot tables)
113 | - Scalability - Can render thousands of rows performantly
114 |
115 | By default, Ember Table is a feature complete data table solution that you can
116 | drop in to your app. We aim to support most standard data table features, and
117 | provide escape hatches where possible.
118 |
119 | ## Lightweight
120 |
121 | Ember Table is also a relatively lightweight table solution, currently weighing
122 | in at `22kb` (minified and gzipped) with plenty of extra weight to shed. Compare
123 | that to standalone table solutions like ag-grid (`152kb`) or HandsOnTable
124 | (`196kb`) and you can see how a table solution that integrates with the
125 | framework directly can save quite a few bytes.
126 |
127 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-table",
3 | "version": "6.0.0-9",
4 | "description": "An addon to support large data set and a number of features around table.",
5 | "keywords": [
6 | "ember-addon"
7 | ],
8 | "license": "BSD-3-Clause",
9 | "author": "",
10 | "directories": {
11 | "doc": "doc",
12 | "test": "tests"
13 | },
14 | "repository": "https://github.com/Addepar/ember-table",
15 | "scripts": {
16 | "build": "ember build",
17 | "docs:deploy": "ember try:one ember-default-docs --- ember deploy production",
18 | "lint": "concurrently \"yarn:lint:*(!fix)\" --names \"lint:\"",
19 | "lint:files": "ls-lint",
20 | "lint:js": "eslint . --cache",
21 | "lint:js:fix": "eslint . --fix",
22 | "lint:sass": "concurrently \"yarn:lint:sass:*\"",
23 | "lint:sass:sass-lint": "sass-lint -c .sass-lint.yml --verbose --no-exit",
24 | "lint:sass:prettier": "prettier --list-different '{addon,app}/styles/**/*.scss' 'tests/dummy/app/styles/**/*.scss'",
25 | "release": "release-it",
26 | "start": "ember serve",
27 | "test": "ember test",
28 | "test:ember-compatibility": "ember try:one"
29 | },
30 | "dependencies": {
31 | "@babel/core": "^7.0.0-0",
32 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.13",
33 | "@babel/plugin-proposal-numeric-separator": "^7.12.13",
34 | "@babel/plugin-proposal-object-rest-spread": "^7.12.13",
35 | "@babel/plugin-proposal-optional-catch-binding": "^7.12.13",
36 | "@babel/plugin-proposal-optional-chaining": "^7.12.13",
37 | "@html-next/vertical-collection": "^4.0.0",
38 | "css-element-queries": "^0.4.0",
39 | "ember-classy-page-object": "^0.8.0",
40 | "ember-cli-babel": "^7.12.0",
41 | "ember-cli-htmlbars": "^6.0.0",
42 | "ember-cli-node-assets": "^0.2.2",
43 | "ember-cli-version-checker": "^5.1.2",
44 | "ember-compatibility-helpers": "^1.2.6",
45 | "ember-raf-scheduler": "^0.3.0",
46 | "ember-test-selectors": "^7.1.0",
47 | "hammerjs": "^2.0.8"
48 | },
49 | "devDependencies": {
50 | "@addepar/eslint-config": "^4.0.2",
51 | "@addepar/prettier-config": "^1.0.0",
52 | "@addepar/sass-lint-config": "^2.0.1",
53 | "@addepar/style-toolbox": "~0.8.1",
54 | "@babel/eslint-parser": "^7.27.1",
55 | "@babel/plugin-proposal-decorators": "^7.27.1",
56 | "@ember/optional-features": "^2.0.0",
57 | "@ember/string": "^3.0.0",
58 | "@ember/test-helpers": "^5.2.2",
59 | "@embroider/test-setup": "^4.0.0",
60 | "@glimmer/component": "^1.1.2",
61 | "@ls-lint/ls-lint": "^2.2.3",
62 | "@tsconfig/ember": "^1.0.1",
63 | "@types/ember__component": "^4.0.10",
64 | "broccoli-asset-rev": "^3.0.0",
65 | "concurrently": "^9.1.2",
66 | "ember-a11y-testing": "^7.1.2",
67 | "ember-auto-import": "^2.4.2",
68 | "ember-cli": "~3.28.0",
69 | "ember-cli-dependency-checker": "^3.2.0",
70 | "ember-cli-inject-live-reload": "^2.0.1",
71 | "ember-cli-sass": "^10.0.0",
72 | "ember-cli-sri": "^2.1.0",
73 | "ember-cli-terser": "^4.0.0",
74 | "ember-disable-prototype-extensions": "^1.1.2",
75 | "ember-faker": "^1.5.0",
76 | "ember-load-initializers": "^2.0.0",
77 | "ember-math-helpers": "~2.11.3",
78 | "ember-qunit": "^9.0.3",
79 | "ember-radio-button": "^2.0.0",
80 | "ember-resolver": "^8.0.2",
81 | "ember-source": "~3.28.0",
82 | "ember-truth-helpers": "^3.0.0",
83 | "ember-try": "^4.0.0",
84 | "eslint": "^8.57.1",
85 | "eslint-config-prettier": "^10.1.5",
86 | "eslint-plugin-ember": "^12.5.0",
87 | "eslint-plugin-n": "^17.18.0",
88 | "eslint-plugin-prettier": "^5.4.0",
89 | "eslint-plugin-qunit": "^8.1.2",
90 | "husky": "^1.3.1",
91 | "latest-version": "^9.0.0",
92 | "loader.js": "^4.2.3",
93 | "qunit": "^2.24.1",
94 | "qunit-dom": "^3.4.0",
95 | "release-it": "^15.5.0",
96 | "sass": "^1.26.10",
97 | "sass-lint": "^1.13.1",
98 | "typescript": "^4.8.4",
99 | "webpack": "^5.0.0"
100 | },
101 | "engines": {
102 | "node": ">= 14.*"
103 | },
104 | "ember": {
105 | "edition": "octane"
106 | },
107 | "ember-addon": {
108 | "configPath": "tests/dummy/config"
109 | },
110 | "homepage": "https://Addepar.github.io/ember-table",
111 | "resolutions": {
112 | "@ember/test-waiters": "^3.0.2",
113 | "prettier": "1.18.2"
114 | },
115 | "typesVersions": {
116 | "*": {
117 | "*": [
118 | "types/*"
119 | ]
120 | }
121 | },
122 | "volta": {
123 | "node": "18.20.5",
124 | "yarn": "1.22.19"
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/addon/components/-private/row-wrapper.js:
--------------------------------------------------------------------------------
1 | import Component from '@ember/component';
2 | import hbs from 'htmlbars-inline-precompile';
3 |
4 | import EmberObject, { get, setProperties, computed, defineProperty } from '@ember/object';
5 | import { alias } from '@ember/object/computed';
6 | import { A as emberA } from '@ember/array';
7 |
8 | import { notifyPropertyChange } from '../../-private/utils/ember';
9 | import { objectAt } from '../../-private/utils/array';
10 | import { observer } from '../../-private/utils/observer';
11 |
12 | const CellWrapper = EmberObject.extend({
13 | /* eslint-disable-next-line ember/no-observers, ember-best-practices/no-observers */
14 | columnValueValuePathDidChange: observer('columnValue.valuePath', function() {
15 | let columnValuePath = get(this, 'columnValue.valuePath');
16 | let cellValue = columnValuePath ? alias(`rowValue.${columnValuePath}`) : null;
17 |
18 | defineProperty(this, 'cellValue', cellValue);
19 | notifyPropertyChange(this, 'cellValue');
20 | }),
21 |
22 | cellMeta: computed('rowMeta', 'columnValue', function() {
23 | let rowMeta = get(this, 'rowMeta');
24 | let columnValue = get(this, 'columnValue');
25 |
26 | if (!rowMeta._cellMetaCache.has(columnValue)) {
27 | rowMeta._cellMetaCache.set(columnValue, EmberObject.create());
28 | }
29 |
30 | return rowMeta._cellMetaCache.get(columnValue);
31 | }),
32 | });
33 |
34 | const layout = hbs`{{yield this.api}}`;
35 |
36 | export default Component.extend({
37 | layout,
38 | tagName: '',
39 |
40 | canSelect: undefined,
41 | checkboxSelectionMode: undefined,
42 | columnMetaCache: undefined,
43 | columns: undefined,
44 | rowMetaCache: undefined,
45 | rowSelectionMode: undefined,
46 | rowToggleMode: undefined,
47 | rowValue: undefined,
48 | rowsCount: undefined,
49 |
50 | init() {
51 | this._super(...arguments);
52 |
53 | this._cells = emberA([]);
54 | },
55 |
56 | destroy() {
57 | this._cells.forEach(cell => cell.destroy());
58 |
59 | this._super(...arguments);
60 | },
61 |
62 | api: computed(
63 | 'rowValue',
64 | 'rowMeta',
65 | 'cells',
66 | 'canSelect',
67 | 'rowSelectionMode',
68 | 'rowToggleMode',
69 | 'rowsCount',
70 | function() {
71 | let rowValue = this.get('rowValue');
72 | let rowMeta = this.get('rowMeta');
73 | let cells = this.get('cells');
74 | let canSelect = this.get('canSelect');
75 | let rowSelectionMode = canSelect ? this.get('rowSelectionMode') : 'none';
76 | let rowToggleMode = this.get('rowToggleMode');
77 | let rowsCount = this.get('rowsCount');
78 |
79 | return { rowValue, rowMeta, cells, rowSelectionMode, rowToggleMode, rowsCount };
80 | }
81 | ),
82 |
83 | rowMeta: computed('rowValue', function() {
84 | let rowValue = this.get('rowValue');
85 | let rowMetaCache = this.get('rowMetaCache');
86 |
87 | return rowMetaCache.get(rowValue);
88 | }),
89 |
90 | cells: computed(
91 | 'rowValue',
92 | 'rowMeta',
93 | 'columns.[]',
94 | 'canSelect',
95 | 'checkboxSelectionMode',
96 | 'rowSelectionMode',
97 | function() {
98 | let columns = this.get('columns');
99 | let numColumns = get(columns, 'length');
100 |
101 | let rowValue = this.get('rowValue');
102 | let rowMeta = this.get('rowMeta');
103 | let rowsCount = this.get('rowsCount');
104 | let canSelect = this.get('canSelect');
105 | let checkboxSelectionMode = canSelect ? this.get('checkboxSelectionMode') : 'none';
106 | let rowSelectionMode = canSelect ? this.get('rowSelectionMode') : 'none';
107 |
108 | let { _cells } = this;
109 |
110 | if (numColumns !== _cells.length) {
111 | while (_cells.length < numColumns) {
112 | _cells.pushObject(CellWrapper.create());
113 | }
114 |
115 | while (_cells.length > numColumns) {
116 | _cells.popObject().destroy();
117 | }
118 | }
119 |
120 | _cells.forEach((cell, i) => {
121 | let columnValue = objectAt(columns, i);
122 | let columnMeta = this.get('columnMetaCache').get(columnValue);
123 |
124 | // eslint-disable-next-line ember/no-side-effects, ember-best-practices/no-side-effect-cp
125 | setProperties(cell, {
126 | checkboxSelectionMode,
127 | columnMeta,
128 | columnValue,
129 | rowMeta,
130 | rowSelectionMode,
131 | rowValue,
132 | rowsCount,
133 | });
134 | });
135 |
136 | return _cells;
137 | }
138 | ),
139 | });
140 |
--------------------------------------------------------------------------------
/tests/acceptance/docs-test.js:
--------------------------------------------------------------------------------
1 | import { module, test as qunitTest, skip as qunitSkip } from 'qunit';
2 | import { visit, currentURL, click } from '@ember/test-helpers';
3 | import { setupApplicationTest } from 'ember-qunit';
4 | import config from 'dummy/config/environment';
5 | import TablePage from 'ember-table/test-support/pages/ember-table';
6 |
7 | let skip = (msg, ...args) =>
8 | qunitSkip(`Skip because ember-cli-addon-docs is not installed. ${msg}`, ...args);
9 | let test = config.ADDON_DOCS_INSTALLED ? qunitTest : skip;
10 |
11 | // The nav that contains buttons to show each snippet in a
12 | // `{{docs.demo}}`. See https://github.com/ember-learn/ember-cli-addon-docs/blob/a00a28e33acea463d82c64fa0a712913d70de3f1/addon/components/docs-demo/template.hbs#L11
13 | const DOCS_DEMO_SNIPPET_NAV_SELECTOR = '.docs-demo__snippets-nav';
14 |
15 | module('Acceptance | docs', function(hooks) {
16 | setupApplicationTest(hooks);
17 |
18 | test('visiting / redirects to /docs', async function(assert) {
19 | await visit('/');
20 |
21 | assert.strictEqual(currentURL(), '/docs');
22 | });
23 |
24 | test('pages linked to by /docs nav all render', async function(assert) {
25 | await visit('/docs');
26 |
27 | let nav = this.element.querySelector('nav');
28 | assert.true(!!nav, 'nav exists');
29 |
30 | let links = Array.from(nav.querySelectorAll('a')).filter(link =>
31 | link.getAttribute('href').startsWith('/docs')
32 | );
33 | assert.true(links.length > 0, `${links.length} nav links found`);
34 | for (let link of links) {
35 | let href = link.getAttribute('href');
36 | await visit(href);
37 | assert.true(true, `Visited ${href} successfully`);
38 |
39 | let buttonCount = 0;
40 | let docsNavs = Array.from(this.element.querySelectorAll(DOCS_DEMO_SNIPPET_NAV_SELECTOR));
41 | for (let nav of docsNavs) {
42 | let buttons = Array.from(nav.querySelectorAll('button'));
43 | for (let button of buttons) {
44 | await click(button);
45 | buttonCount++;
46 | }
47 | }
48 | assert.true(true, `Clicked ${buttonCount} snippet buttons on "${href}"`);
49 |
50 | await visit('/docs'); // start over
51 | }
52 | });
53 |
54 | test('subcolumns docs renders cell content', async function(assert) {
55 | let DemoTable = TablePage.extend({
56 | scope: '[data-test-demo="docs-example-subcolumns"] [data-test-ember-table]',
57 | });
58 |
59 | await visit('/docs/guides/header/subcolumns');
60 | let table = new DemoTable();
61 | assert.strictEqual(
62 | table.header.headers.objectAt(0).text,
63 | 'A',
64 | 'first header cell renders correctly'
65 | );
66 | assert.strictEqual(
67 | table.body.rows.objectAt(0).cells.objectAt(0).text,
68 | 'A A',
69 | 'first body cell renders correclty'
70 | );
71 | });
72 |
73 | test('autogenerated API docs are present', async function(assert) {
74 | await visit('/docs');
75 |
76 | let nav = this.element.querySelector('nav');
77 | assert.true(!!nav, 'nav exists');
78 |
79 | let navItems = Array.from(nav.querySelectorAll('li'));
80 |
81 | let expectedNavItems = ['API REFERENCE', ''];
82 |
83 | expectedNavItems.forEach(expectedText => {
84 | assert.true(
85 | navItems.some(li => li.innerText.includes(expectedText)),
86 | `"${expectedText}" nav item is exists`
87 | );
88 | });
89 | });
90 |
91 | test('sorting: 2-state sorting works as expected', async function(assert) {
92 | await visit('/docs/guides/header/sorting');
93 | let DemoTable = TablePage.extend({
94 | scope: '[data-test-demo="docs-example-2-state-sortings"] [data-test-ember-table]',
95 | });
96 |
97 | let table = new DemoTable();
98 | let header = table.headers.objectAt(0);
99 |
100 | assert.false(header.sortIndicator.isPresent, 'precond - sortIndicator is not present');
101 |
102 | await header.click();
103 | assert.true(
104 | // eslint-disable-next-line qunit/no-assert-logical-expression
105 | header.sortIndicator.isPresent && header.sortIndicator.isDescending,
106 | 'sort descending'
107 | );
108 |
109 | await header.click();
110 | assert.true(
111 | // eslint-disable-next-line qunit/no-assert-logical-expression
112 | header.sortIndicator.isPresent && header.sortIndicator.isAscending,
113 | 'sort ascending'
114 | );
115 |
116 | await header.click();
117 | assert.true(
118 | // eslint-disable-next-line qunit/no-assert-logical-expression
119 | header.sortIndicator.isPresent && header.sortIndicator.isDescending,
120 | 'sort cycles back to descending'
121 | );
122 | });
123 | });
124 |
--------------------------------------------------------------------------------