From 9763877e329882a8dcd6457e9d818cf40094952d Mon Sep 17 00:00:00 2001 From: Luke Schierer <2224044+lschierer@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:02:45 -0500 Subject: [PATCH] fix(lit-table): dynamic data updates in the Lit Table Adapter (#5884) * this fixes an issue I discussed in discord where with the lit table adapter, updating a data array did not get reflected by the table. It is a one-line change to the TableController, and a new example that demonstrates the difference. * Update packages/lit-table/src/index.ts per suggestion from @kadoshms Co-authored-by: Mor Kadosh --------- Co-authored-by: Luke Schierer Co-authored-by: Mor Kadosh --- examples/lit/sorting-dynamic-data/.gitignore | 5 + examples/lit/sorting-dynamic-data/README.md | 6 + examples/lit/sorting-dynamic-data/index.html | 14 ++ .../lit/sorting-dynamic-data/package.json | 21 ++ examples/lit/sorting-dynamic-data/src/main.ts | 235 ++++++++++++++++++ .../lit/sorting-dynamic-data/src/makeData.ts | 52 ++++ .../lit/sorting-dynamic-data/tsconfig.json | 25 ++ .../lit/sorting-dynamic-data/vite.config.js | 15 ++ packages/lit-table/src/index.ts | 2 + 9 files changed, 375 insertions(+) create mode 100644 examples/lit/sorting-dynamic-data/.gitignore create mode 100644 examples/lit/sorting-dynamic-data/README.md create mode 100644 examples/lit/sorting-dynamic-data/index.html create mode 100644 examples/lit/sorting-dynamic-data/package.json create mode 100644 examples/lit/sorting-dynamic-data/src/main.ts create mode 100644 examples/lit/sorting-dynamic-data/src/makeData.ts create mode 100644 examples/lit/sorting-dynamic-data/tsconfig.json create mode 100644 examples/lit/sorting-dynamic-data/vite.config.js diff --git a/examples/lit/sorting-dynamic-data/.gitignore b/examples/lit/sorting-dynamic-data/.gitignore new file mode 100644 index 0000000000..d451ff16c1 --- /dev/null +++ b/examples/lit/sorting-dynamic-data/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/lit/sorting-dynamic-data/README.md b/examples/lit/sorting-dynamic-data/README.md new file mode 100644 index 0000000000..b168d3c4b1 --- /dev/null +++ b/examples/lit/sorting-dynamic-data/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `yarn` +- `npm run start` or `yarn start` diff --git a/examples/lit/sorting-dynamic-data/index.html b/examples/lit/sorting-dynamic-data/index.html new file mode 100644 index 0000000000..9807874fae --- /dev/null +++ b/examples/lit/sorting-dynamic-data/index.html @@ -0,0 +1,14 @@ + + + + + + Vite App + + + +
+ + + + diff --git a/examples/lit/sorting-dynamic-data/package.json b/examples/lit/sorting-dynamic-data/package.json new file mode 100644 index 0000000000..36276a3dc6 --- /dev/null +++ b/examples/lit/sorting-dynamic-data/package.json @@ -0,0 +1,21 @@ +{ + "name": "tanstack-lit-table-example-sorting-dynamic-data", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite" + }, + "dependencies": { + "@faker-js/faker": "^8.4.1", + "@tanstack/lit-table": "^8.20.5", + "lit": "^3.1.4" + }, + "devDependencies": { + "@rollup/plugin-replace": "^5.0.7", + "typescript": "5.4.5", + "vite": "^5.3.2" + } +} diff --git a/examples/lit/sorting-dynamic-data/src/main.ts b/examples/lit/sorting-dynamic-data/src/main.ts new file mode 100644 index 0000000000..7e1583c8c3 --- /dev/null +++ b/examples/lit/sorting-dynamic-data/src/main.ts @@ -0,0 +1,235 @@ +import { customElement } from 'lit/decorators.js' +import { html, LitElement, PropertyValueMap } from 'lit' +import { repeat } from 'lit/directives/repeat.js' +import { state } from 'lit/decorators/state.js' +import { + ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + SortingFn, + type SortingState, + TableController, +} from '@tanstack/lit-table' + +import { makeData, Person } from './makeData' + +const sortStatusFn: SortingFn = (rowA, rowB, _columnId) => { + const statusA = rowA.original.status + const statusB = rowB.original.status + const statusOrder = ['single', 'complicated', 'relationship'] + return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB) +} + +const columns: ColumnDef[] = [ + { + accessorKey: 'firstName', + cell: info => info.getValue(), + //this column will sort in ascending order by default since it is a string column + }, + { + accessorFn: row => row.lastName, + id: 'lastName', + cell: info => info.getValue(), + header: () => html`Last Name`, + sortUndefined: 'last', //force undefined values to the end + sortDescFirst: false, //first sort order will be ascending (nullable values can mess up auto detection of sort order) + }, + { + accessorKey: 'age', + header: () => 'Age', + //this column will sort in descending order by default since it is a number column + }, + { + accessorKey: 'visits', + header: () => html`Visits`, + sortUndefined: 'last', //force undefined values to the end + }, + { + accessorKey: 'status', + header: 'Status', + sortingFn: sortStatusFn, //use our custom sorting function for this enum column + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + // enableSorting: false, //disable sorting for this column + }, + { + accessorKey: 'rank', + header: 'Rank', + invertSorting: true, //invert the sorting order (golf score-like where smaller is better) + }, + { + accessorKey: 'createdAt', + header: 'Created At', + // sortingFn: 'datetime' //make sure table knows this is a datetime column (usually can detect if no null values) + }, +] + +const data: Person[] = makeData(1000) + +@customElement('lit-table-example') +class LitTableExample extends LitElement { + @state() + private _sorting: SortingState = [] + + @state() + private _multiplier: number = 1 + + @state() + private _data: Person[] = new Array() + + private tableController = new TableController(this) + + constructor() { + super() + this._data = [...data] + } + + protected willUpdate( + _changedProperties: PropertyValueMap | Map + ): void { + super.willUpdate(_changedProperties) + if (_changedProperties.has('_multiplier')) { + const newData: Person[] = data.map(d => { + const p: Person = { + ...d, + visits: d.visits ? d.visits * this._multiplier : undefined, + } + return p + }) + this._data.length = 0 + this._data = newData + } + } + protected render() { + const table = this.tableController.table({ + columns, + data: this._data, + state: { + sorting: this._sorting, + }, + onSortingChange: updaterOrValue => { + if (typeof updaterOrValue === 'function') { + this._sorting = updaterOrValue(this._sorting) + } else { + this._sorting = updaterOrValue + } + }, + getSortedRowModel: getSortedRowModel(), + getCoreRowModel: getCoreRowModel(), + }) + + return html` + + + + ${repeat( + table.getHeaderGroups(), + headerGroup => headerGroup.id, + headerGroup => html` + + ${headerGroup.headers.map( + header => html` + + ` + )} + + ` + )} + + + ${table + .getRowModel() + .rows.slice(0, 10) + .map( + row => html` + + ${row + .getVisibleCells() + .map( + cell => html` + + ` + )} + + ` + )} + +
+ ${header.isPlaceholder + ? null + : html`
+ ${flexRender( + header.column.columnDef.header, + header.getContext() + )} + ${{ asc: ' 🔼', desc: ' 🔽' }[ + header.column.getIsSorted() as string + ] ?? null} +
`} +
+ ${flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
${JSON.stringify(this._sorting, null, 2)}
+ + ` + } +} diff --git a/examples/lit/sorting-dynamic-data/src/makeData.ts b/examples/lit/sorting-dynamic-data/src/makeData.ts new file mode 100644 index 0000000000..d6c0639b22 --- /dev/null +++ b/examples/lit/sorting-dynamic-data/src/makeData.ts @@ -0,0 +1,52 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string | undefined + age: number + visits: number | undefined + progress: number + status: 'relationship' | 'complicated' | 'single' + rank: number + createdAt: Date + subRows?: Person[] +} + +const range = (len: number) => { + const arr: number[] = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: Math.random() < 0.1 ? undefined : faker.person.lastName(), + age: faker.number.int(40), + visits: Math.random() < 0.1 ? undefined : faker.number.int(1000), + progress: faker.number.int(100), + createdAt: faker.date.anytime(), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0]!, + rank: faker.number.int(100), + } +} + +export function makeData(...lens: number[]) { + const makeDataLevel = (depth = 0): Person[] => { + const len = lens[depth]! + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/lit/sorting-dynamic-data/tsconfig.json b/examples/lit/sorting-dynamic-data/tsconfig.json new file mode 100644 index 0000000000..56517d3a72 --- /dev/null +++ b/examples/lit/sorting-dynamic-data/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "useDefineForClassFields": false, + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/lit/sorting-dynamic-data/vite.config.js b/examples/lit/sorting-dynamic-data/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/lit/sorting-dynamic-data/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import rollupReplace from '@rollup/plugin-replace' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + rollupReplace({ + preventAssignment: true, + values: { + __DEV__: JSON.stringify(true), + 'process.env.NODE_ENV': JSON.stringify('development'), + }, + }), + ], +}) diff --git a/packages/lit-table/src/index.ts b/packages/lit-table/src/index.ts index 1237d6553b..358bd3f7fe 100755 --- a/packages/lit-table/src/index.ts +++ b/packages/lit-table/src/index.ts @@ -55,6 +55,8 @@ export class TableController this.tableInstance.setOptions(prev => ({ ...prev, state: { ...this._tableState, ...options.state }, + data: options.data, + columns: options.columns, onStateChange: (updater: any) => { this._tableState = updater(this._tableState) this.host.requestUpdate()