Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify working with columns #40

Merged
merged 4 commits into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/demos/kitchen-sink/demo/demo-a.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ import { tracked } from '@glimmer/tracking';
import { htmlSafe } from '@ember/template';

import { headlessTable } from 'ember-headless-table';
import { meta } from 'ember-headless-table/plugins';
import { meta, columns } from 'ember-headless-table/plugins';
import {
ColumnResizing,
isResizing, resizeHandle
Expand Down Expand Up @@ -139,7 +139,7 @@ export default class extends Component {
@tracked sorts = [];

get columns() {
return meta.forTable(this.table, ColumnReordering).columns;
return columns.for(this.table);
}

get data() {
Expand Down
6 changes: 2 additions & 4 deletions docs/plugins/column-reordering/demo/dema-a.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,11 @@
import Component from '@glimmer/component';

import { headlessTable } from 'ember-headless-table';
import { meta } from 'ember-headless-table/plugins';
import { meta, columns } from 'ember-headless-table/plugins';
import {
ColumnReordering,
moveLeft, moveRight
} from 'ember-headless-table/plugins/column-reordering';
import { ColumnVisibility } from 'ember-headless-table/plugins/column-visibility';

import { DATA } from 'docs-app/sample-data';

Expand All @@ -54,12 +53,11 @@ export default class extends Component {
data: () => DATA,
plugins: [
ColumnReordering,
ColumnVisibility,
],
});

get columns() {
return meta.forTable(this.table, ColumnReordering).columns;
return columns.for(this.table);
}

/**
Expand Down
2 changes: 0 additions & 2 deletions docs/plugins/column-resizing/demo/demo-a.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ export default class extends Component {
],
data: () => DATA,
plugins: [
ColumnVisibility,
ColumnReordering,
ColumnResizing,
],
});
Expand Down
6 changes: 0 additions & 6 deletions docs/plugins/column-resizing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,16 @@ API Documentation available [here][api-docs]

## Usage

Because this plugin operates or visible columns,
the `ColumnVisibility` plugin is required.

```js
import { headlessTable } from 'ember-headless-table';
import { ColumnResizing, resizeHandle } from 'ember-headless-table/plugins/column-resizing';
import { ColumnVisibility } from 'ember-headless-table/plugins/column-visibility';

// ...
// in a class
table = headlessTable(this, {
columns: () => [ /* ... */ ],
data: () => [ /* ... */ ],
plugins: [
ColumnVisibility,
ColumnResizing,
],
})
Expand Down Expand Up @@ -55,7 +50,6 @@ See the API Documentation [here][api-docs] for the full list of options and desc
table = headlessTable(this, {
columns: () => [ /* ... */ ],
plugins: [
ColumnVisibility,
ColumnResizing.with(() => ({ handlePosition: 'right' })),
],
})
Expand Down
4 changes: 2 additions & 2 deletions docs/plugins/column-visibility/demo/demo-a.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
import Component from '@glimmer/component';

import { headlessTable } from 'ember-headless-table';
import { meta } from 'ember-headless-table/plugins';
import { meta, columns } from 'ember-headless-table/plugins';
import { ColumnVisibility, hide, show } from 'ember-headless-table/plugins/column-visibility';

import { DATA } from 'docs-app/sample-data';
Expand All @@ -65,7 +65,7 @@ export default class extends Component {
});

get columns() {
return meta.forTable(this.table, ColumnVisibility).visibleColumns;
return columns.for(this.table);
}

/**
Expand Down
1 change: 0 additions & 1 deletion docs/plugins/sticky-column/demo/demo-a.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export default class extends Component {
plugins: [
StickyColumns,
ColumnResizing,
ColumnVisibility,
],
});

Expand Down
8 changes: 5 additions & 3 deletions docs/plugins/sticky-column/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ API Documentation available [here][api-docs]

```js
import { headlessTable } from 'ember-headless-table';
import { ColumnVisibility } from 'ember-headless-table/plugins/column-visibility';
import { StickyColumns } from 'ember-headless-table/plugins/sticky-columns';
import { ColumnResizing } from 'ember-headless-table/plugins/column-resizing';

Expand All @@ -26,13 +25,16 @@ import { ColumnResizing } from 'ember-headless-table/plugins/column-resizing';
],
data: () => [ /* ... */ ],
plugins: [
ColumnReordering,
ColumnVisibility,
ColumnResizing,
StickyColumns,
],
})
```

Note that the `ColumnResizing` plugin is required because `StickyColumns` needs a guarantee
that a `columnWidth` implementation exists so that columns may become sticky beyond just the
far left and far right columns.

### ColumnOptions

- `sticky`
Expand Down
1 change: 1 addition & 0 deletions docs/plugins/writing-your-own.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The key properties to look at are:
- `headerCellModifier` - for each `<th>`
- `rowModifier` - for each `<tr>`
- `reset` -- a hook that the table will call on your plugin if you have state to revert to
- `columns` -- for overriding / altering column order / visibility / grouping / etc

With these capabilities, features for tables may be built in a way that relieves implementation complexity on the consumer, such as:

Expand Down
17 changes: 17 additions & 0 deletions ember-headless-table/src/-private/interfaces/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,23 @@ export interface Plugin<Signature = unknown> {
* If the plugin has state, this should be used to reset that state
*/
reset?: () => void;

/**
* @public
* @kind Table Hook
*
* A plugin may change the columns order, visibility, etc.
* By implementing this getter, this plugin's
* `columns` property will be used by other plugins via
* the `columns.for(table, RequestingPlugin)` api.
*
* For the end-consumer, they may choose to do
* `columns.for(table)`, which will aggregate all column modifications
* from all plugins.
*
* As always, `table.columns` is the way to get the unmodified list of columns.
*/
columns?: Column<any>[];
}

/**
Expand Down
11 changes: 2 additions & 9 deletions ember-headless-table/src/-private/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {

/**
* @private
*
* used by other private APIs
*/
get config() {
return this.args.named;
Expand Down Expand Up @@ -234,15 +236,6 @@ export class Table<DataType = unknown> extends Resource<Signature<DataType>> {
},
});

/**
* @private
*
* TODO: what's this for?
*/
get value() {
return this;
}

/**
* @private
*/
Expand Down
191 changes: 191 additions & 0 deletions ember-headless-table/src/plugins/-private/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,197 @@ export const preferences = {
},
};

/**
* if a `requester` is not provided,
* Get the columns for the table, considering any and all plugins that could modify columns.
*
* If you are an end-consumer of ember-headless-table, this is the function to use.
* If you are a plugin-author, you'll want to pass your plugin class as the second parameter.
*
* For a given plugin, `requester`, determine what columns should be returned.
* Since multiple plugins could be used in a table, there is an implicit hierarchy of
* column modifications that can occur from each of those plugins.
*
* If a plugin defines other plugins as either *requirements* or *optional requirements*,
* and that upstream plugin defines a `columns` property, then those columns will be returned here.
*
* This works recursively up the plugin tree up until a plugin has no requirements, and then
* all columns from the table are returned.
*/
function columnsFor<DataType = any>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the code that powers all the refactors in this PR.

It's also some of the messier code.
I wasn't really in a headspace to do recursive trees, so this is what works for now.

If we add another plugin that needs to tweak columns rendered, then we'll have to do this proper

table: Table<DataType>,
requester?: Plugin<any> | undefined
): Column<DataType>[] {
assert(`First argument passed to columns.for must be an instance of Table`, table[TABLE_KEY]);

let visibility = findPlugin(table.plugins, 'columnVisibility');
let reordering = findPlugin(table.plugins, 'columnOrder');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to work on #152 and stumbled across this after some digging...

Is this how the column order plugin ends up getting the columns with visibility already applied?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there is a graph of plugins determined at lookup time. You can choose:

  • get all columns
  • get columns for the plugin (subject to the graph)
  • get user land columns (after all plugins are applied)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that the column order plugin needs to be aware of individual column's visibility values but I think it still needs to be able to loop over all columns...

Looking at those things you can choose, after a bunch of clicking through the code I think the APIs are:

  • get all columns: table.columns (assuming you have a reference to table, otherwise you'll need to thread it down
  • get columns for the plugin (subject to the graph): columns.for(table, PluginClass) (again, you need a reference to the table
  • get user land columns (after all plugins are applied): I haven't needed this one yet so I haven't found it...

Does that seem right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ye! 🎉


// TODO: actually resolve the graph, rather than use the hardcoded feature names
// atm, this only "happens" to work based on expectations of
// of the currently implemented plugins' capabilities and implied hierarchy.

if (requester) {
assert(
`[${requester.name}] requested columns from the table, but the plugin, ${requester.name}, ` +
`is not used in this table`,
table.plugins.some((plugin) => plugin instanceof (requester as Class<Plugin>))
);

if (visibility && visibility.constructor === requester) {
return table.columns.values();
}

if (reordering && reordering.constructor === requester) {
if (visibility) {
assert(
`<#${visibility.name}> defined a 'columns' property, but did not return valid data.`,
visibility.columns && Array.isArray(visibility.columns)
);

return visibility.columns;
}

return table.columns.values();
}

if (reordering) {
assert(
`<#${reordering.name}> defined a 'columns' property, but did not return valid data.`,
reordering.columns && Array.isArray(reordering.columns)
);

return reordering.columns;
}

if (visibility) {
assert(
`<#${visibility.name}> defined a 'columns' property, but did not return valid data.`,
visibility.columns && Array.isArray(visibility.columns)
);

return visibility.columns;
}

return table.columns.values();
}

/**
* This flow is the inverse of when we have a requester
*/

if (reordering) {
assert(
`<#${reordering.name}> defined a 'columns' property, but did not return valid data.`,
reordering.columns && Array.isArray(reordering.columns)
);

return reordering.columns;
}

if (visibility) {
assert(
`<#${visibility.name}> defined a 'columns' property, but did not return valid data.`,
visibility.columns && Array.isArray(visibility.columns)
);

return visibility.columns;
}

return table.columns.values();
}

export const columns = {
for: columnsFor,

/**
* for a given current or reference column, return the column that
* is immediately next, or to the right of that column.
*
* If a plugin class is provided, the hierarchy of column list modifications
* will be respected.
*/
next: <Data = unknown>(
current: Column<Data>,
requester?: Plugin<any>
): Column<Data> | undefined => {
let columns = requester ? columnsFor(current.table, requester) : columnsFor(current.table);

let referenceIndex = columns.indexOf(current);

assert(
`index of reference column must be >= 0. column likely not a part of the table`,
referenceIndex >= 0
);

/**
* There can be nothing after the last column
*/
if (referenceIndex >= columns.length - 1) {
return undefined;
}

return columns[referenceIndex + 1];
},

/**
* for a given current or reference column, return the column that
* is immediately previous, or to the left of that column.
*
* If a plugin class is provided, the hierarchy of column list modifications
* will be respected.
*/
previous: <Data = unknown>(
current: Column<Data>,
requester?: Plugin<any>
): Column<Data> | undefined => {
let columns = requester ? columnsFor(current.table, requester) : columnsFor(current.table);
let referenceIndex = columns.indexOf(current);

assert(
`index of reference column must be >= 0. column likely not a part of the table`,
referenceIndex >= 0
);

/**
* There can be nothing before the first column
*/
if (referenceIndex === 0) {
return undefined;
}

return columns[referenceIndex - 1];
},
/**
* for a given current or reference column, return the columns that
* should appear before, or to the left of that column.
*
* if a plugin class is provided, the hierarchy of column list modifications
* will be respected.
*/
before: <Data = unknown>(current: Column<Data>, requester?: Plugin<any>): Column<Data>[] => {
let columns = requester ? columnsFor(current.table, requester) : columnsFor(current.table);

let referenceIndex = columns.indexOf(current);

return columns.slice(0, referenceIndex);
},
/**
* for a given current or reference column, return the columns that
* should appear after, or to the right of that column.
*
* if a plugin class is provided, the hierarchy of column list modifications
* will be respected.
*/
after: <Data = unknown>(current: Column<Data>, requester?: Plugin<any>): Column<Data>[] => {
let columns = requester ? columnsFor(current.table, requester) : columnsFor(current.table);

let referenceIndex = columns.indexOf(current);

return columns.slice(referenceIndex + 1);
},
};

export const meta = {
/**
* @public
Expand Down
Loading