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

Persist column width preferences #211

Merged
merged 5 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions .changeset/hot-bats-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ember-headless-table": minor
---

Persists resized column widths to preferences
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: should mention the newly added save action and related asserts, i.e. will error if the column width is not a string.

3 changes: 3 additions & 0 deletions docs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,8 @@
"ember-headless-table": {
"injected": true
}
},
"volta": {
"extends": "../package.json"
}
}
2 changes: 1 addition & 1 deletion docs/plugins/column-resizing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ See the API Documentation [here][api-docs] for the full list of options and desc

### Preferences

Nothing is present in the preferences object at this time.
The width will be stored in preferences, per column.

### Accessibility

Expand Down
23 changes: 23 additions & 0 deletions ember-headless-table/src/plugins/-private/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ function columnsFor<DataType = any>(

Copy link
Contributor

Choose a reason for hiding this comment

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

This method upsets me. lol (I wrote it)

It was meant to be temporary.

I need to see how TanStack Table handles their column dependency calculations

let visibility = findPlugin(table.plugins, 'columnVisibility');
let reordering = findPlugin(table.plugins, 'columnOrder');
let sizing = findPlugin(table.plugins, 'columnResizing');

// TODO: actually resolve the graph, rather than use the hardcoded feature names
// atm, this only "happens" to work based on expectations of
Expand All @@ -274,6 +275,10 @@ function columnsFor<DataType = any>(
table.plugins.some((plugin) => plugin instanceof (requester as Class<Plugin>))
);

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

if (visibility && visibility.constructor === requester) {
return table.columns.values();
}
Expand Down Expand Up @@ -309,6 +314,15 @@ function columnsFor<DataType = any>(
return visibility.columns;
}

if (sizing) {
assert(
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: do we need to test for this new assert?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we currently test for any of the other asserts in this function

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

return sizing.columns;
}

return table.columns.values();
}

Expand All @@ -334,6 +348,15 @@ function columnsFor<DataType = any>(
return visibility.columns;
}

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

return sizing.columns;
}

return table.columns.values();
}

Expand Down
2 changes: 2 additions & 0 deletions ember-headless-table/src/plugins/column-resizing/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ class ResizeHandle extends Modifier<{ Args: { Positional: [Column] } }> {
this.token = undefined;
}

this.meta.save();

/**
* No need to listen if we aren't dragging
*/
Expand Down
63 changes: 61 additions & 2 deletions ember-headless-table/src/plugins/column-resizing/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,29 @@ import { cached, tracked } from '@glimmer/tracking';
import { assert } from '@ember/debug';
import { action } from '@ember/object';

import { preferences } from '[public-plugin-types]';

import { BasePlugin, columns, meta, options } from '../-private/base';
import { applyStyles } from '../-private/utils';
import { getAccurateClientHeight, getAccurateClientWidth, totalGapOf } from './utils';

import type { ColumnApi } from '[public-plugin-types]';
import type { ColumnApi, PluginPreferences } from '[public-plugin-types]';
import type { Column, Table } from '[public-types]';

interface ColumnResizePreferences extends PluginPreferences {
columns: {
[columnKey: string]: {
width?: number;
};
};
}

declare module 'ember-headless-table/plugins' {
interface Registry {
ColumnResize?: ColumnResizePreferences;
}
}

export interface ColumnOptions {
/**
* Force a starting width
Expand Down Expand Up @@ -94,6 +110,15 @@ export class ColumnResizing extends BasePlugin<Signature> {
let tableMeta = meta.forTable(this.table, ColumnResizing);

tableMeta.reset();

for (let column of this.table.columns) {
let defaultValue = options.forColumn(column, ColumnResizing)?.width;
let current = meta.forColumn(column, ColumnResizing).width;

if (defaultValue !== current) {
preferences.forTable(this.table, ColumnResizing).delete('width');
}
}
}
}

Expand Down Expand Up @@ -135,12 +160,24 @@ export class ColumnMeta {
};
}

get key() {
return this.column.key;
}

get minWidth() {
return this.options.minWidth;
}

get initialWidth() {
return this.options.width;
let savedWidth = preferences.forColumn(this.column, ColumnResizing).get('width');

if (!savedWidth) {
return this.options.width;
}

assert('saved width must be a string', typeof savedWidth === 'string');
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit (non-blocking): do we need to test for this assert?


return parseInt(savedWidth, 10);
}

get canShrink() {
Expand Down Expand Up @@ -192,6 +229,11 @@ export class ColumnMeta {
resize(delta: number) {
this.tableMeta.resizeColumn(this.column, delta);
}

@action
save() {
this.tableMeta.saveColWidths(this.tableMeta.visibleColumnMetas);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: do we need to test for this save action?

}
}

/**
Expand Down Expand Up @@ -270,6 +312,23 @@ export class TableMeta {
return this.visibleColumnMetas.reduce((acc, column) => (acc += column.width ?? 0), 0);
}

@action
saveColWidths(visibleColumnMetas: ColumnMeta[]) {
let availableColumns = columns.for(this.table, ColumnResizing);

for (let column of visibleColumnMetas) {
let columnToUpdate = availableColumns.find((c) => {
return c.key === column.key;
});

assert('column must exist', columnToUpdate);

let colPreferences = preferences.forColumn(columnToUpdate, ColumnResizing);

colPreferences.set('width', column.width.toString());
Copy link
Contributor

Choose a reason for hiding this comment

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

should this width be a percentage so that when the table is loaded on a different sized browser, the columns have the same way of being sized?

I could see the argument either way.
Though, with fixed-column sizes across dynamic-sized browsers / devices, I'd think we'd only want to save the width of the columns that have specifically been adjusted by the user, so that dynamic widths can still be flexible.

like,

| set width | set width | dynamic width | dynamic width | dynamic width | set width |

}
}

@action
reset() {
if (!this.scrollContainerWidth) return;
Expand Down
3 changes: 3 additions & 0 deletions ember-headless-table/src/test-support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export function createHelpers(selectors: Selectors) {

triggerEvent(element, 'mousedown', { clientX: startX, button: 0 });
triggerEvent(element, 'mousemove', { clientX: targetX, button: 0 });

await new Promise((resolve) => setTimeout(resolve, 50));
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this do?

Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious as it seems like something I could reuse in the future...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a workaround that ensures that the calculation of the column width styles are complete and the DOM is updated before the test continues.

It's tracked to fix in #215

but is a necessary workaround for now.


triggerEvent(element, 'mouseup', { clientX: targetX, button: 0 });

await settled();
Expand Down
123 changes: 122 additions & 1 deletion test-app/tests/plugins/column-resizing/rendering-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { on } from '@ember/modifier';
// @ts-ignore
import { fn } from '@ember/helper';

import { headlessTable, type ColumnConfig } from 'ember-headless-table';
import { headlessTable, type ColumnConfig, type PreferencesData } from 'ember-headless-table';
import { ColumnResizing, resizeHandle, hasResizeHandle } from 'ember-headless-table/plugins/column-resizing';
import { ColumnVisibility } from 'ember-headless-table/plugins/column-visibility';
import { ColumnReordering, moveLeft, moveRight } from 'ember-headless-table/plugins/column-reordering';
Expand Down Expand Up @@ -188,6 +188,127 @@ module('Plugins | resizing', function (hooks) {
});
});

module('with a preferences adapter', function (hooks) {
let preferences: null | PreferencesData = {};

class DefaultOptions extends Context {
table = headlessTable(this, {
columns: () => this.columns,
data: () => [] as unknown[],
plugins: [ColumnResizing],
preferences: {
key: 'test-preferences',
adapter: {
persist: (_key: string, data: PreferencesData) => {
preferences = data;
},
restore: (key: string) => {
return {
"plugins": {
"ColumnResizing": {
"columns": {
"A": {
"width": "300"
},
"B": {
"width": "250"
},
"C": {
"width": "250"
},
"D": {
"width": "200"
},
},
"table": {}
},
}
}
}
}
}
});
}

hooks.beforeEach(function () {
preferences = null;
ctx = new DefaultOptions();
setOwner(ctx, this.owner);
});

test('it restores column widths from preferences', async function (assert) {
await render(
<template>
<TestComponentA @ctx={{ctx}} />
</template>
)

const [columnA, columnB, columnC, columnD] = getColumns();

debugAssert(`columnA doesn't exist`, columnA);
debugAssert(`columnB doesn't exist`, columnB);
debugAssert(`columnC doesn't exist`, columnC);
debugAssert(`columnD doesn't exist`, columnD);

assert.equal(width(columnA), 300, 'col A has expected width');
assert.equal(width(columnB), 250, 'col B has expected width');
assert.equal(width(columnC), 250, 'col C has expected width');
assert.equal(width(columnD), 200, 'col D has expected width');
});

test('it resizes each column', async function (assert) {
// Columns are set to equal widths so each of the four columns
// will initially be 250px wide in the 1000px wide container
ctx.setContainerWidth(1000);
await render(
<template>
<TestComponentA @ctx={{ctx}} />
</template>
)

const [columnA, columnB, columnC, columnD] = getColumns();

debugAssert(`columnA doesn't exist`, columnA);
debugAssert(`columnB doesn't exist`, columnB);
debugAssert(`columnC doesn't exist`, columnC);
debugAssert(`columnD doesn't exist`, columnD);

await requestAnimationFrameSettled();

await assertChanges(
() => dragRight(columnB, 50),
[
{ value: () => width(columnA), by: 50, msg: 'width of A increased by 50' },
{ value: () => width(columnB), by: -50, msg: 'width of B decreased by 50' },
{ value: () => width(columnC), by: 0, msg: 'width of C unchanged' },
{ value: () => width(columnD), by: 0, msg: 'width of D unchanged' },
]
);

assert.deepEqual(preferences, {
"plugins": {
"ColumnResizing": {
"columns": {
"A": {
"width": "350"
},
"B": {
"width": "200"
},
"C": {
"width": "250"
},
"D": {
"width": "200"
},
},
"table": {}
}
}
});
});
});

module('with options that affect resize behavior', function (hooks) {
module('handlePosition (default)', function (hooks) {
class DefaultOptions extends Context {
Expand Down