Skip to content

Commit e46ce50

Browse files
committed
feat(plugin): implement row selection plugin
1 parent 907b9bc commit e46ce50

File tree

8 files changed

+400
-3
lines changed

8 files changed

+400
-3
lines changed
+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
This demonstrates how to use the RowSelection plugin to enable multiple row selection.
2+
If single-row selection is desired, that can be handled in userspace, by managing the selection data differently (see other demos).
3+
4+
To select a row, click it. To deselect a row, click it again.
5+
6+
```hbs template
7+
<div class="h-full overflow-auto" {{this.table.modifiers.container}}>
8+
<table>
9+
<thead>
10+
<tr>
11+
<td></td>
12+
{{#each this.table.columns as |column|}}
13+
<th {{this.table.modifiers.columnHeader column}} class="relative group">
14+
{{column.name}}
15+
</th>
16+
{{else}}
17+
<th>
18+
No columns are visible
19+
</th>
20+
{{/each}}
21+
</tr>
22+
</thead>
23+
<tbody>
24+
{{#each this.table.rows as |row|}}
25+
<tr {{this.table.modifiers.row row}} class="{{if (this.isSelected row) 'bg-surface-inner'}}">
26+
<td>
27+
<button {{on 'click' (fn this.toggle row)}}>Toggle</button>
28+
</td>
29+
{{#each this.table.columns as |column|}}
30+
<td>
31+
{{column.getValueForRow row}}
32+
</td>
33+
{{/each}}
34+
</tr>
35+
{{/each}}
36+
</tbody>
37+
</table>
38+
</div>
39+
```
40+
```js component
41+
import Component from '@glimmer/component';
42+
43+
import { headlessTable } from 'ember-headless-table';
44+
import { meta } from 'ember-headless-table/plugins';
45+
import { TrackedSet } from 'tracked-built-ins';
46+
import { RowSelection, toggle, isSelected } from 'ember-headless-table/plugins/row-selection';
47+
48+
import { DATA } from 'docs-app/sample-data';
49+
50+
export default class extends Component {
51+
selection = new TrackedSet();
52+
53+
table = headlessTable(this, {
54+
columns: () => [
55+
{ name: 'column A', key: 'A' },
56+
{ name: 'column B', key: 'B' },
57+
{ name: 'column C', key: 'C' },
58+
{ name: 'column D', key: 'D' },
59+
],
60+
data: () => DATA,
61+
plugins: [
62+
RowSelection.with(() => {
63+
return {
64+
selection: this.selection,
65+
onSelect: (data) => this.selection.add(data),
66+
onDeselect: (data) => this.selection.delete(data),
67+
};
68+
}),
69+
],
70+
});
71+
72+
/**
73+
* Plugin Integration - all of this can be removed in strict mode, gjs/gts
74+
*
75+
* This syntax looks weird, but it's read as:
76+
* [property on this component] = [variable in scope]
77+
*/
78+
toggle = toggle;
79+
isSelected = isSelected;
80+
}

docs/plugins/row-selection/index.md

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Row selection
2+
3+
API Documentation available [here][api-docs]
4+
5+
[api-docs]: /api/modules/plugins_row_selection
6+
7+
## Usage
8+
9+
State for what is selected is managed by you, the consumer.
10+
This plugin provides helpful utilities and automatically wires up event listeners for each row.
11+
12+
### ColumnOptions
13+
14+
None
15+
16+
17+
### TableOptions
18+
19+
Required:
20+
- `selection` - a collection of what is already selected
21+
- `onSelect` - event handler for when a row is selected
22+
- `onDeselect` - event handler for when a row is deselected
23+
24+
Optional:
25+
- `key` - a function which will be passed to `onSelect` and `onDeselect` for helping manage "what" is selected. This should be the same data type as the individual elements within the `selection`
26+
27+
28+
See the API Documentation [here][api-docs] for the full list of options and descriptions.
29+
30+
### Preferences
31+
32+
None
33+
34+
### Accessibility
35+
36+
Without a focusable element to trigger a row selection,
37+
keyboard and screen reader users will not be able to select a row.
38+
When using this plugin, ensure that each row has a focusable element that interacts with the selection APIs for that row.
39+
40+
### Helpers + StrictMode
41+
42+
There are convenience helpers for aiding in more ergonomic template usage when using this plugin.
43+
44+
```gjs
45+
import { on } from '@ember/modifier';
46+
import { fn } from '@ember/helper';
47+
import { toggle, isSelected } from 'ember-headless-table/plugins/row-selection';
48+
49+
export const Rows =
50+
<template>
51+
<tbody>
52+
{{#each @table.rows as |row|}}
53+
<tr {{@table.modifiers.row row}} class="{{if (isSelected row) 'bg-surface-inner'}}">
54+
<td>
55+
<button {{on 'click' (fn toggle row)}}>Toggle</button>
56+
</td>
57+
{{#each @table.columns as |column|}}
58+
<td>
59+
{{column.getValueForRow row}}
60+
</td>
61+
{{/each}}
62+
</tr>
63+
{{/each}}
64+
</tbody>
65+
</template>
66+
```

ember-headless-table/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"./plugins/column-resizing": "./dist/plugins/column-resizing/index.js",
2424
"./plugins/column-visibility": "./dist/plugins/column-visibility/index.js",
2525
"./plugins/sticky-columns": "./dist/plugins/sticky-columns/index.js",
26+
"./plugins/row-selection": "./dist/plugins/row-selection/index.js",
2627
"./test-support": "./dist/test-support/index.js",
2728
"./addon-main.js": "./addon-main.js"
2829
},
@@ -46,6 +47,9 @@
4647
"plugins/sticky-columns": [
4748
"./dist/plugins/sticky-columns/index.d.ts"
4849
],
50+
"plugins/row-selection": [
51+
"./dist/plugins/row-selection/index.d.ts"
52+
],
4953
"test-support": [
5054
"./dist/test-support/index.d.ts"
5155
],

ember-headless-table/src/-private/table.ts

+10
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
139139
},
140140
{ eager: false }
141141
),
142+
143+
row: modifier(
144+
(element: HTMLElement, [row]: [Row<DataType>]): Destructor => {
145+
let modifiers = this.plugins.map((plugin) => plugin.rowModifier);
146+
let composed = composeFunctionModifiers(modifiers);
147+
148+
return composed(element, { row, table: this });
149+
},
150+
{ eager: false }
151+
),
142152
};
143153

144154
/**

ember-headless-table/src/plugins/-private/base.ts

+27-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Table } from '../../-private/table';
77
import type { ColumnReordering } from '../column-reordering';
88
import type { ColumnVisibility } from '../column-visibility';
99
import type { Class, Constructor } from '[private-types]';
10-
import type { Column } from '[public-types]';
10+
import type { Column, Row } from '[public-types]';
1111
import type {
1212
ColumnMetaFor,
1313
ColumnOptionsFor,
@@ -19,6 +19,7 @@ import type {
1919

2020
const TABLE_META = new Map<string, Map<Class<unknown>, any>>();
2121
const COLUMN_META = new WeakMap<Column, Map<Class<unknown>, any>>();
22+
const ROW_META = new WeakMap<Row, Map<Class<unknown>, any>>();
2223

2324
type InstanceOf<T> = T extends Class<infer Instance> ? Instance : T;
2425

@@ -250,6 +251,29 @@ export const meta = {
250251
});
251252
},
252253

254+
/**
255+
* @public
256+
*
257+
* For a given row and plugin, return the meta / state bucket for the
258+
* plugin<->row instance pair.
259+
*
260+
* Note that this requires the row instance to exist on the table.
261+
*/
262+
forRow<P extends BasePlugin<any>, Data = unknown>(
263+
row: Row<Data>,
264+
klass: Class<P>
265+
): RowMetaFor<SignatureFrom<P>> {
266+
return getPluginInstance(ROW_META, row, klass, () => {
267+
let plugin = row.table.pluginOf(klass);
268+
269+
assert(`[${klass.name}] cannot get plugin instance of unregistered plugin class`, plugin);
270+
assert(`<#${plugin.name}> plugin does not have meta specified`, plugin.meta);
271+
assert(`<#${plugin.name}> plugin does not specify row meta`, plugin.meta.row);
272+
273+
return new plugin.meta.row(row);
274+
});
275+
},
276+
253277
/**
254278
* @public
255279
*
@@ -413,10 +437,10 @@ export const options = {
413437
/**
414438
* @private
415439
*/
416-
function getPluginInstance<RootKey extends string | Column<any>, Instance>(
440+
function getPluginInstance<RootKey extends string | Column<any> | Row<any>, Instance>(
417441
map: RootKey extends string
418442
? Map<string, Map<Class<Instance>, Instance>>
419-
: WeakMap<Column, Map<Class<Instance>, Instance>>,
443+
: WeakMap<Column | Row, Map<Class<Instance>, Instance>>,
420444
rootKey: RootKey,
421445
mapKey: Class<Instance>,
422446
factory: () => Instance
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { meta } from '../-private/base';
2+
import { RowSelection } from './plugin';
3+
4+
import type { Row } from '../../-private/row';
5+
6+
export const isSelected = (row: Row) => meta.forRow(row, RowSelection).isSelected;
7+
export const select = (row: Row) => meta.forRow(row, RowSelection).select();
8+
export const deselect = (row: Row) => meta.forRow(row, RowSelection).deselect();
9+
export const toggle = (row: Row) => meta.forRow(row, RowSelection).toggle();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Public API
2+
export * from './helpers';
3+
export { RowSelection as Plugin } from './plugin';
4+
export { RowSelection } from './plugin';
5+
6+
// Public types
7+
export type { Signature } from './plugin';

0 commit comments

Comments
 (0)