diff --git a/README.md b/README.md index e82fa87596..689cb0a7df 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/docs/config.json b/docs/config.json index 1bdb61a68e..3f1901deac 100644 --- a/docs/config.json +++ b/docs/config.json @@ -14,6 +14,12 @@ { "label": "Devtools", "to": "devtools" } ], "frameworks": [ + { + "label": "alpine", + "children": [ + { "label": "Quick Start", "to": "framework/alpine/quick-start" } + ] + }, { "label": "angular", "children": [ @@ -89,6 +95,23 @@ { "label": "Type Helpers", "to": "guide/helpers" } ], "frameworks": [ + { + "label": "alpine", + "children": [ + { + "label": "Table State", + "to": "framework/alpine/guide/table-state" + }, + { + "label": "Composable Tables (createTableHook)", + "to": "framework/alpine/guide/composable-tables" + }, + { + "label": "Custom Plugins", + "to": "framework/alpine/guide/custom-features" + } + ] + }, { "label": "angular", "children": [ @@ -159,6 +182,26 @@ "label": "Feature Guides", "children": [], "frameworks": [ + { + "label": "alpine", + "children": [ + { "label": "Column Ordering", "to": "framework/alpine/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/alpine/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/alpine/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/alpine/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/alpine/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/alpine/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/alpine/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/alpine/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/alpine/guide/column-faceting" }, + { "label": "Grouping", "to": "framework/alpine/guide/grouping" }, + { "label": "Expanding", "to": "framework/alpine/guide/expanding" }, + { "label": "Pagination", "to": "framework/alpine/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/alpine/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/alpine/guide/row-selection" }, + { "label": "Sorting", "to": "framework/alpine/guide/sorting" } + ] + }, { "label": "angular", "children": [ @@ -314,6 +357,12 @@ { "label": "Core API Reference", "to": "reference/index" } ], "frameworks": [ + { + "label": "alpine", + "children": [ + { "label": "Alpine API Reference", "to": "framework/alpine/reference/index" } + ] + }, { "label": "angular", "children": [ @@ -383,6 +432,18 @@ { "label": "DebugOptions", "to": "reference/index/type-aliases/DebugOptions" } ], "frameworks": [ + { + "label": "alpine", + "children": [ + { "label": "createTable", "to": "framework/alpine/reference/functions/createTable" }, + { "label": "createTableHook", "to": "framework/alpine/reference/functions/createTableHook" }, + { "label": "AlpineTable", "to": "framework/alpine/reference/type-aliases/AlpineTable" }, + { "label": "AppAlpineTable", "to": "framework/alpine/reference/type-aliases/AppAlpineTable" }, + { "label": "CreateTableHookOptions", "to": "framework/alpine/reference/type-aliases/CreateTableHookOptions" }, + { "label": "FlexRender", "to": "framework/alpine/reference/functions/FlexRender-1" }, + { "label": "flexRender", "to": "framework/alpine/reference/functions/flexRender" } + ] + }, { "label": "react", "children": [ @@ -500,6 +561,12 @@ { "label": "DeepValue", "to": "reference/index/type-aliases/DeepValue" } ], "frameworks": [ + { + "label": "alpine", + "children": [ + { "label": "AppColumnHelper", "to": "framework/alpine/reference/type-aliases/AppColumnHelper" } + ] + }, { "label": "react", "children": [ @@ -785,6 +852,16 @@ "label": "Basic Examples", "children": [], "frameworks": [ + { + "label": "alpine", + "children": [ + { "to": "framework/alpine/examples/basic-create-table", "label": "Basic (createTable)" }, + { "to": "framework/alpine/examples/basic-app-table", "label": "Basic (createAppTable)" }, + { "to": "framework/alpine/examples/basic-external-state", "label": "Basic (External State)" }, + { "to": "framework/alpine/examples/basic-external-atoms", "label": "Basic (External Atoms)" }, + { "to": "framework/alpine/examples/column-groups", "label": "Header Groups" } + ] + }, { "label": "angular", "children": [ @@ -872,6 +949,29 @@ "label": "Feature Examples", "children": [], "frameworks": [ + { + "label": "alpine", + "children": [ + { "to": "framework/alpine/examples/filters", "label": "Column Filters" }, + { "to": "framework/alpine/examples/filters-faceted", "label": "Column Filters (Faceted)" }, + { "to": "framework/alpine/examples/column-ordering", "label": "Column Ordering" }, + { "to": "framework/alpine/examples/column-pinning", "label": "Column Pinning" }, + { "to": "framework/alpine/examples/column-pinning-split", "label": "Column Pinning (Split)" }, + { "to": "framework/alpine/examples/column-pinning-sticky", "label": "Sticky Column Pinning" }, + { "to": "framework/alpine/examples/column-sizing", "label": "Column Sizing" }, + { "to": "framework/alpine/examples/column-resizing", "label": "Column Resizing" }, + { "to": "framework/alpine/examples/column-resizing-performant", "label": "Performant Column Resizing" }, + { "to": "framework/alpine/examples/column-visibility", "label": "Column Visibility" }, + { "to": "framework/alpine/examples/expanding", "label": "Expanding" }, + { "to": "framework/alpine/examples/sub-components", "label": "Expanding Sub Components" }, + { "to": "framework/alpine/examples/grouping", "label": "Grouping" }, + { "to": "framework/alpine/examples/pagination", "label": "Pagination" }, + { "to": "framework/alpine/examples/row-pinning", "label": "Row Pinning" }, + { "to": "framework/alpine/examples/row-selection", "label": "Row Selection" }, + { "to": "framework/alpine/examples/sorting", "label": "Sorting" }, + { "to": "framework/alpine/examples/sorting-dynamic-data", "label": "Sorting (Dynamic Data)" } + ] + }, { "label": "angular", "children": [ @@ -1056,6 +1156,12 @@ "label": "Specialized Examples", "children": [], "frameworks": [ + { + "label": "alpine", + "children": [ + { "label": "Custom Plugin", "to": "framework/alpine/examples/custom-plugin" } + ] + }, { "label": "angular", "children": [ diff --git a/docs/devtools.md b/docs/devtools.md index 3066f30afb..af2a63efa8 100644 --- a/docs/devtools.md +++ b/docs/devtools.md @@ -51,7 +51,7 @@ npm install @tanstack/angular-devtools @tanstack/angular-table-devtools@beta -Lit, Svelte, and vanilla do not currently ship dedicated table devtools adapters. +Lit, Svelte, Alpine, and vanilla do not currently ship dedicated table devtools adapters. ## The Required `key` Table Option diff --git a/docs/framework/alpine/guide/column-faceting.md b/docs/framework/alpine/guide/column-faceting.md new file mode 100644 index 0000000000..fd9cdb70a0 --- /dev/null +++ b/docs/framework/alpine/guide/column-faceting.md @@ -0,0 +1,247 @@ +--- +title: Faceting (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Faceted Filters](../examples/filters-faceted) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Faceting Setup + +Here's how you set up your table to use faceting features. Adding the faceting feature enables the related APIs. Additionally, if using client-side faceting, you also need to set up `filteredRowModel` and `facetedRowModel` after their associated features because row model slots are type-checked. + +```ts +import { + columnFacetingFeature, + columnFilteringFeature, + createFacetedMinMaxValues, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createTable, + filterFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFacetingFeature, + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + facetedRowModel: createFacetedRowModel(), // if using client-side faceting + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + filterFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Faceting (Alpine) Guide + +Faceting is a feature that generates lists of values from your table's data, either for a single column (column faceting) or across the entire table (global faceting). For example, a list of unique values can be used as search suggestions in an autocomplete filter component, or a tuple of minimum and maximum values from a column of numbers can drive a range slider filter component. The same row models power both the per-column and table-wide APIs. + +### Column Faceting Row Models + +In order to use any of the column faceting features, add the `columnFacetingFeature` and the appropriate faceted row model factories to your features. Faceting exists to power filter UIs, so in practice you will also register the `columnFilteringFeature` and a `filteredRowModel`. Without a filtered row model, the faceted row models fall back to the pre-filtered rows and the facet values will not react to other columns' filters. + +```ts +import { + columnFacetingFeature, + columnFilteringFeature, + createFacetedMinMaxValues, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createTable, + filterFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFacetingFeature, + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // facet values react to other columns' filters + facetedRowModel: createFacetedRowModel(), // required for faceting (other faceted row models depend on this) + facetedMinMaxValues: createFacetedMinMaxValues(), // if you need min/max values + facetedUniqueValues: createFacetedUniqueValues(), // if you need a list of unique values + filterFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +First, you must include the `facetedRowModel`. This row model will generate a list of values for a given column. If you need a list of unique values, include the `facetedUniqueValues` row model. If you need a tuple of minimum and maximum values, include the `facetedMinMaxValues` row model. + +### Use Faceted Row Models + +Once you have included the appropriate row models in your table options, you will be able to use the faceting column instance APIs to access the lists of values generated by the faceted row models. `column.getFacetedUniqueValues()` returns a `Map` of value to count, so read its size with `.size` and its values with `.keys()`. + +```ts +// list of unique values for autocomplete filter +const autoCompleteSuggestions = Array.from( + column.getFacetedUniqueValues().keys(), +) + .sort() + .slice(0, 5000) +``` + +```ts +// tuple of min and max values for range filter +const [min, max] = column.getFacetedMinMaxValues() ?? [0, 1] +``` + +These reads are reactive inside Alpine bindings, so expose them as methods on your `Alpine.data` object and call them from the template. For example, a text filter can offer a faceted `` of unique values, while a numeric column can seed the `min`/`max` of a range input. + +```ts +Alpine.data('table', () => { + // ...createTable as above + return { + table, + FlexRender, + isNumberColumn(column) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + return typeof firstValue === 'number' + }, + uniqueCount(column) { + return column.getFacetedUniqueValues().size + }, + uniqueValues(column) { + return Array.from(column.getFacetedUniqueValues().keys()) + .sort() + .slice(0, 5000) + }, + facetMin(column) { + return column.getFacetedMinMaxValues()?.[0] ?? '' + }, + facetMax(column) { + return column.getFacetedMinMaxValues()?.[1] ?? '' + }, + } +}) +``` + +```html + + + + + +``` + +### Global Faceting + +The same `columnFacetingFeature` and faceted row models also power table-wide (global) faceting. Where column faceting derives values from a single column, global faceting derives values across all columns, which is useful for populating a global filter's autocomplete suggestions or a global range slider. If your table also uses global filtering, register the `globalFilteringFeature` so global facet values react to the active global filter. + +Use the global faceting table instance APIs to read the values: + +```ts +const globalFacetedRows = table.getGlobalFacetedRowModel().flatRows +``` + +```ts +// list of unique values for autocomplete filter +const autoCompleteSuggestions = Array.from( + table.getGlobalFacetedUniqueValues().keys(), +) + .sort() + .slice(0, 5000) +``` + +```ts +// tuple of min and max values for range filter +const [min, max] = table.getGlobalFacetedMinMaxValues() ?? [0, 1] +``` + +### Custom (Server-Side) Faceting + +Instead of using the built-in client-side faceting features, you can implement your own faceting logic on the server-side and pass the faceted values to the client-side. Supply custom `facetedUniqueValues` and `facetedMinMaxValues` factories in `tableFeatures`. Each factory receives the table and a column ID and returns a thunk that resolves the faceted values. The column instance APIs (`column.getFacetedUniqueValues()` and `column.getFacetedMinMaxValues()`) will then return your server-provided values. + +```ts +const features = tableFeatures({ + columnFacetingFeature, + facetedUniqueValues: (_table, columnId) => () => { + const uniqueValueMap = new Map() + //... populate from your server data for this columnId + return uniqueValueMap + }, + facetedMinMaxValues: (_table, columnId) => () => { + //... read from your server data for this columnId + return [min, max] + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... +}) +``` + +The same factories also serve global faceting. Global faceting requests values with the internal `__global__` column ID, so you can branch on it inside the same `facetedUniqueValues` and `facetedMinMaxValues` factories to return table-wide facet values: + +```ts +facetedUniqueValues: (_table, columnId) => () => { + if (columnId !== '__global__') return new Map() // per-column facets + return new Map(globalFacets.uniqueValues) // global facets +}, +``` + +Alternatively, you don't have to put any of your faceting logic through the TanStack Table APIs at all. Just fetch your lists and pass them to your filter components directly. diff --git a/docs/framework/alpine/guide/column-filtering.md b/docs/framework/alpine/guide/column-filtering.md new file mode 100644 index 0000000000..b3400dfac0 --- /dev/null +++ b/docs/framework/alpine/guide/column-filtering.md @@ -0,0 +1,517 @@ +--- +title: Column Filtering (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Filtering Setup + +Here's how you set up your table to use column filtering features. Adding the column filtering feature enables the related APIs. Additionally, if using client-side filtering, you also need to set up `filteredRowModel` after its associated feature because row model slots are type-checked. + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createTable, + filterFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + filterFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Filtering (Alpine) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on column filtering, which is a filter that is applied to a single column's accessor value. + +TanStack table supports both client-side and manual server-side filtering. This guide will go over how to implement and customize both, and help you decide which one is best for your use-case. + +### Client-Side vs Server-Side Filtering + +If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. + +However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. + +> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. + +Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: + +1. Can your server query all of the data in a reasonable amount of time (and cost)? +2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) +3. Is the client's browser using too much memory if all of the data is loaded at once? + +If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. + +### Manual Server-Side Filtering + +If you have decided that you need to implement server-side filtering instead of using the built-in client-side filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `tableFeatures`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +const features = tableFeatures({ columnFilteringFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + manualFiltering: true, +}) +``` + +> **Note:** When using manual filtering, many of the options that are discussed in the rest of this guide will have no effect. When `manualFiltering` is set to `true`, the table instance will not apply any filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the `data` that you pass to it as-is. + +### Client-Side Filtering + +If you are using the built-in client-side filtering features, add the `columnFilteringFeature` and the `filteredRowModel` factory to your features. Import `createFilteredRowModel` and `filterFns` from TanStack Table: + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createTable, + filterFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Column Filter State + +Whether or not you use client-side or server-side filtering, you can take advantage of the built-in column filter state management that TanStack Table provides. There are many table and column APIs to mutate and interact with the filter state and retrieving the column filter state. + +The column filtering state is defined as an array of objects with the following shape: + +```ts +interface ColumnFilter { + id: string + value: unknown +} +type ColumnFiltersState = ColumnFilter[] +``` + +Since the column filter state is an array of objects, you can have multiple column filters applied at once. + +#### Accessing Column Filter State + +The table's state atoms are reactive in Alpine. `table.atoms.columnFilters.get()` is a reactive read when used inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. `table.store.get()` returns a current full-state snapshot, useful for debugging. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... +}) + +table.atoms.columnFilters.get() // reactive read inside Alpine bindings, plain read elsewhere +``` + +However, if you need access to the column filter state outside of the table, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state in other parts of your application, you can own the column filter state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The filter values can be read, written, or subscribed to elsewhere (such as in a query key for server-side filtering) without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' + +const columnFiltersAtom = createAtom([]) // can set initial column filter state here + +// subscribe to the atom wherever you need the value (e.g. for a query key) +columnFiltersAtom.subscribe(() => { + // react to filter changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... + atoms: { + columnFilters: columnFiltersAtom, // table filter APIs now update columnFiltersAtom + }, +}) +``` + +Alternatively, the v8-style `state.columnFilters` plus `onColumnFiltersChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ columnFilters: [] as ColumnFiltersState }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... + state: { + get columnFilters() { + return local.columnFilters // connect the reactive slice back down to the table + }, + }, + onColumnFiltersChange: (updater) => { + local.columnFilters = + typeof updater === 'function' ? updater(local.columnFilters) : updater + }, +}) +``` + +#### Initial Column Filter State + +If you do not need to control the column filter state in your own state management or scope, but you still want to set an initial column filter state, you can use the `initialState` table option instead of `state`. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 12 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equals` - Strict equality `===` +- `weakEquals` - Weak equality `==` +- `arrIncludes` - The row's array (or string) value includes at least one of the filter values +- `arrIncludesAll` - The row's array value includes every filter value +- `arrIncludesSome` - The row's array value includes at least one of the filter values +- `arrHas` - The row's scalar value equals at least one of the filter values +- `inNumberRange` - Inclusive `[min, max]` number range (endpoints normalized and swapped if reversed) +- `between` - Exclusive min/max range (blank endpoints are open-ended) +- `betweenInclusive` - Inclusive min/max range (blank endpoints are open-ended) + +You can also define your own custom filter functions, either inline as the `filterFn` column option, or by name in the filter function registry that you pass to `createFilteredRowModel`. + +#### Custom Filter Functions + +> **Note:** These filter functions only run during client-side filtering. + +Whether you register a custom filter function in the registry passed to `createFilteredRowModel` or pass it directly as a `filterFn` column option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = ( + row, // Row + columnId: string, + filterValue: any, + addMeta?: (meta: FilterMeta) => void, +): boolean => ... +``` + +Every filter function receives: + +- The row to filter +- The columnId to use to retrieve the row's value +- The filter value + +and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + filterFn: 'includesString', // use built-in filter function + }, + { + header: () => 'Age', + accessorKey: 'age', + filterFn: 'inNumberRange', + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + filterFn: 'myCustomFilterFn', // reference a custom filter function registered with createFilteredRowModel + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + }, +] +//... +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { + ...filterFns, + myCustomFilterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + startsWith: startsWithFilterFn, // defined elsewhere + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +> **TypeScript Note:** For `filterFn: 'myCustomFilterFn'` string references to typecheck, register the function in the `filterFns` slot on `tableFeatures` (as shown above). The slot is the registry; no `declare module` augmentation is needed. Alternatively, skip the registry entirely by passing the function directly to the `filterFn` column option. + +##### Customize Filter Function Behavior + +You can attach a few other properties to filter functions to customize their behavior: + +- `filterFn.resolveFilterValue` - This optional "hanging" method on any given `filterFn` allows the filter function to transform/sanitize/format the filter value before it is passed to the filter function. + +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. + +```ts +const startsWithFilterFn = < + TFeatures extends TableFeatures, + TData extends RowData, +>( + row: Row, + columnId: string, + filterValue: string, // resolveFilterValue below transforms the raw value to a string +) => + row + .getValue(columnId) + .toString() + .toLowerCase() + .trim() + .startsWith(filterValue) // toString, toLowerCase, and trim the filter value in `resolveFilterValue` + +// remove the filter value from filter state if it is falsy (empty string in this case) +startsWithFilterFn.autoRemove = (val: any) => !val + +// transform/sanitize/format the filter value before it is passed to the filter function +startsWithFilterFn.resolveFilterValue = (val: any) => + val.toString().toLowerCase().trim() +``` + +### Wiring up the filter UI + +TanStack Table will not add filter inputs to your table. Add them yourself on real elements (Alpine does not initialize directives inside content set with `x-html`). Read the current value with `column.getFilterValue()` and write it with `column.setFilterValue()`. Use `column.getCanFilter()` to decide whether to render an input. + +```html + + + +``` + +For a numeric range filter, store a `[min, max]` tuple and update each bound separately. Reading and writing each bound is small enough to keep inline, or you can expose helpers on your `Alpine.data` object. + +```ts +Alpine.data('table', () => { + // ...createTable as above + return { + table, + FlexRender, + rangeValue(column, index) { + return column.getFilterValue()?.[index] ?? '' + }, + setRangeMin(column, value) { + column.setFilterValue((old) => [ + value === '' ? undefined : Number(value), + old?.[1], + ]) + }, + setRangeMax(column, value) { + column.setFilterValue((old) => [ + old?.[0], + value === '' ? undefined : Number(value), + ]) + }, + } +}) +``` + +```html +
+ + +
+``` + +### Customize Column Filtering + +There are a lot of table and column options that you can use to further customize the column filtering behavior. + +#### Disable Column Filtering + +By default, column filtering is enabled for all columns. You can disable the column filtering for all columns or for specific columns by using the `enableColumnFilters` table option or the `enableColumnFilter` column option. You can also turn off both column and global filtering by setting the `enableFilters` table option to `false`. + +Disabling column filtering for a column will cause the `column.getCanFilter` API to return `false` for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableColumnFilters: false, // disable column filtering for all columns +}) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```ts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + filterFromLeafRows: true, // filter and search through sub-rows +}) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```ts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +}) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `input` or `change` handlers. + +- `column.getCanFilter` - Useful for disabling/enabling filter inputs. +- `column.getIsFiltered` - Useful for displaying a visual indicator that a column is currently being filtered. +- `column.getFilterIndex` - Useful for displaying in what order the current filter is being applied. + +- `column.getAutoFilterFn` - Used internally to find the default filter function for a column if none is specified. +- `column.getFilterFn` - Useful for displaying which filter mode or function is currently being used. diff --git a/docs/framework/alpine/guide/column-ordering.md b/docs/framework/alpine/guide/column-ordering.md new file mode 100644 index 0000000000..2e0190760f --- /dev/null +++ b/docs/framework/alpine/guide/column-ordering.md @@ -0,0 +1,186 @@ +--- +title: Column Ordering (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Ordering](../examples/column-ordering) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Ordering Setup + +Here's how you set up your table to use column ordering features. Adding the column ordering feature enables the related APIs. + +```ts +import { createTable, tableFeatures, columnOrderingFeature } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Ordering (Alpine) Guide + +By default, columns are ordered in the order they are defined in the `columns` array. However, you can manually specify the column order using the `columnOrder` state. Other features like column pinning and grouping can also affect the column order. + +### What Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into left, center (unpinned), and right pinned columns. +2. Manual **Column Ordering** - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +> **Note:** `columnOrder` state will only affect unpinned columns if used in conjunction with column pinning. + +### Column Order State + +If you don't provide a `columnOrder` state, TanStack Table will just use the order of the columns in the `columns` array. However, you can provide an array of string column ids to the `columnOrder` state to specify the order of the columns. + +#### Default Column Order + +If all you need to do is specify the initial column order, you can just specify the `columnOrder` state in the `initialState` table option. + +```ts +const features = tableFeatures({ columnOrderingFeature }) + +const table = createTable({ + features, + //... + initialState: { + columnOrder: ['columnId1', 'columnId2', 'columnId3'], + }, + //... +}) +``` + +> **Note:** If you are using the `state` table option to also specify the `columnOrder` state, the `initialState` will have no effect. Only specify particular states in either `initialState` or `state`, not both. + +#### Managing Column Order State + +If you need to dynamically change the column order, or set the column order after the table has been initialized, you can manage the `columnOrder` state just like any other table state. + +In v9, the recommended way to own a state slice is with an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the column order without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import { createTable, tableFeatures, columnOrderingFeature } from '@tanstack/alpine-table' +import type { ColumnOrderState } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const columnOrderAtom = createAtom([ + 'columnId1', + 'columnId2', + 'columnId3', +]) + +// subscribe wherever it is needed +columnOrderAtom.subscribe(() => { + // react to column order changes +}) + +const table = createTable({ + features, + //... + atoms: { + columnOrder: columnOrderAtom, + }, + //... +}) +``` + +Alternatively, the v8-style `state.columnOrder` plus `onColumnOrderChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnOrderingFeature }) + +const local = Alpine.reactive({ + columnOrder: ['columnId1', 'columnId2', 'columnId3'] as ColumnOrderState, +}) +//... +const table = createTable({ + features, + //... + state: { + get columnOrder() { + return local.columnOrder // connect the reactive slice back down to the table + }, + //... + }, + onColumnOrderChange: (updater) => { + local.columnOrder = + typeof updater === 'function' ? updater(local.columnOrder) : updater + }, + //... +}) +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, hook the drop event of your drag-and-drop solution up to `table.setColumnOrder`. For example, with native browser drag events on the header cells. Keep the drag state in `Alpine.reactive` so the markup can react to it: + +```ts +const local = Alpine.reactive({ movingColumnId: null as string | null }) + +// move the dragged column in front of the column it was dropped on +function handleDrop(targetColumnId: string) { + const fromId = local.movingColumnId + if (!fromId || fromId === targetColumnId) return + table.setColumnOrder((prevColumnOrder) => { + const newColumnOrder = [...prevColumnOrder] + newColumnOrder.splice( + newColumnOrder.indexOf(targetColumnId), + 0, + newColumnOrder.splice(newColumnOrder.indexOf(fromId), 1)[0]!, + ) + return newColumnOrder + }) + local.movingColumnId = null +} +``` + +`table.setColumnOrder` works the same whether the table manages the `columnOrder` state internally, you control it with `state` + `onColumnOrderChange`, or you own it with an external atom. The official [Column Ordering example](../examples/column-ordering) calls it with a full array of leaf column ids. + +### Column Ordering APIs + +Use `table.setColumnOrder` to update the column order state directly. Use `table.resetColumnOrder` to reset the order to `initialState.columnOrder`, or pass `true` to clear the order state. + +```ts +table.setColumnOrder(['lastName', 'firstName', 'age']) +table.resetColumnOrder() +table.resetColumnOrder(true) +``` + +Columns expose helpers for reading their current position after column pinning, manual ordering, and grouping have been applied. + +```ts +column.getIndex() +column.getIndex('left') +column.getIndex('center') +column.getIndex('right') + +column.getIsFirstColumn() +column.getIsLastColumn() +``` + +These helpers are useful for styling column boundaries or building drag-and-drop targets that need to know the current rendered order. + +#### Drag and Drop Column Reordering Suggestions (Alpine) + +TanStack Table is not opinionated about which drag-and-drop solution you use. Here are a few suggestions: + +1. Consider native browser drag events (`@dragstart`, `@dragenter`, `@dragend`) with your own `Alpine.reactive` state if you want zero dependencies. This can be very lightweight, but you will need to do extra work for proper touch support on mobile. [Material React Table](https://www.material-react-table.com/docs/examples/column-ordering) implements TanStack Table column ordering this way with no DnD dependencies; the approach translates directly to Alpine since it is just DOM events feeding `table.setColumnOrder`. + +2. If you want a library, look at framework-agnostic options such as Atlassian's [Pragmatic drag and drop](https://atlassian.design/components/pragmatic-drag-and-drop/about). Check maintenance status, bundle size, and how well they handle semantic `` markup before committing. + +3. Do NOT reach for React-only DnD libraries (including DnD Kit's `@dnd-kit/*` packages). They depend on React's component model and do not work with Alpine. diff --git a/docs/framework/alpine/guide/column-pinning.md b/docs/framework/alpine/guide/column-pinning.md new file mode 100644 index 0000000000..59ed1b0457 --- /dev/null +++ b/docs/framework/alpine/guide/column-pinning.md @@ -0,0 +1,232 @@ +--- +title: Column Pinning (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Pinning Setup + +Here's how you set up your table to use column pinning features. Adding the column pinning feature enables the related APIs. + +```ts +import { createTable, tableFeatures, columnPinningFeature } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnPinningFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Pinning (Alpine) Guide + +TanStack Table offers state and APIs helpful for implementing column pinning features in your table UI. You can implement column pinning in multiple ways. You can either split pinned columns into their own separate tables, or you can keep all columns in the same table, but use the pinning state to order the columns correctly and use sticky CSS to pin the columns to the left or right. + +### How Column Pinning Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. **Column Pinning** - If pinning, columns are split into left, center (unpinned), and right pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +The only way to change the order of the pinned columns is in the `columnPinning.left` and `columnPinning.right` state itself. `columnOrder` state will only affect the order of the unpinned ("center") columns. + +### Column Pinning State + +Managing the `columnPinning` state is optional, and usually not necessary unless you are adding persistent state features. TanStack Table will already keep track of the column pinning state for you. Manage the `columnPinning` state just like any other table state if you need to. + +In v9, the recommended way to own a state slice is with an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import { createTable, tableFeatures, columnPinningFeature } from '@tanstack/alpine-table' +import type { ColumnPinningState } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnPinningFeature }) + +const columnPinningAtom = createAtom({ + left: [], + right: [], +}) + +// subscribe wherever it is needed +columnPinningAtom.subscribe(() => { + // react to pinning changes +}) + +const table = createTable({ + features, + //... + atoms: { + columnPinning: columnPinningAtom, + }, + //... +}) +``` + +Alternatively, the v8-style `state.columnPinning` plus `onColumnPinningChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + columnPinning: { left: [], right: [] } as ColumnPinningState, +}) + +const table = createTable({ + features, + //... + state: { + get columnPinning() { + return local.columnPinning // connect the reactive slice back down to the table + }, + //... + }, + onColumnPinningChange: (updater) => { + local.columnPinning = + typeof updater === 'function' ? updater(local.columnPinning) : updater + }, + //... +}) +``` + +### Pin Columns by Default + +A very common use case is to pin some columns by default. You can do this by either initializing the `columnPinning` state with the pinned columnIds, or by using the `initialState` table option + +```ts +const table = createTable({ + features, + //... + initialState: { + columnPinning: { + left: ['expand-column'], + right: ['actions-column'], + }, + //... + }, + //... +}) +``` + +### Useful Column Pinning APIs + +> Note: These APIs are available when using `columnPinningFeature`. + +There are a handful of useful Column API methods to help you implement column pinning features: + +- `column.getCanPin`: Use to determine if a column can be pinned. +- `column.pin`: Use to pin a column to the left or right. Or use to unpin a column. +- `column.getIsPinned`: Use to determine where a column is pinned. +- `column.getPinnedIndex`: Use to read the column's index within its pinned column group. +- `column.getStart`: Use to provide the correct `left` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `right` CSS value for a pinned column. +- `column.getIsLastColumn`: Use to determine if a column is the last column in its pinned group. Useful for adding a box-shadow. +- `column.getIsFirstColumn`: Use to determine if a column is the first column in its pinned group. Useful for adding a box-shadow. + +Use `table.setColumnPinning` to update the pinning state directly. Use `table.resetColumnPinning` to reset to `initialState.columnPinning`, or pass `true` to clear both pinned column arrays. + +```ts +table.setColumnPinning({ + left: ['firstName'], + right: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```ts +table.getLeftLeafColumns() +table.getCenterLeafColumns() +table.getRightLeafColumns() + +table.getLeftVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getRightVisibleLeafColumns() + +table.getLeftHeaderGroups() +table.getCenterHeaderGroups() +table.getRightHeaderGroups() + +table.getLeftFooterGroups() +table.getCenterFooterGroups() +table.getRightFooterGroups() + +table.getLeftFlatHeaders() +table.getCenterFlatHeaders() +table.getRightFlatHeaders() + +table.getLeftLeafHeaders() +table.getCenterLeafHeaders() +table.getRightLeafHeaders() +``` + +You can also request pinned leaf columns by region with `table.getPinnedLeafColumns(position)` and visible pinned leaf columns with `table.getPinnedVisibleLeafColumns(position)`. + +```ts +table.getPinnedLeafColumns('left') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('right') + +table.getPinnedVisibleLeafColumns('left') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('right') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'left'` or `'right'` to check one pinned side. + +### Wiring up the pinning UI + +Because Alpine does not initialize directives inside content set with `x-html`, render the header content with `x-html="FlexRender({ header })"` and attach the pin handlers to real ` +``` + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getLeftHeaderGroups`, `table.getCenterHeaderGroups`, `table.getRightHeaderGroups`, `row.getLeftVisibleCells`, `row.getCenterVisibleCells`, and `row.getRightVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/alpine/guide/column-resizing.md b/docs/framework/alpine/guide/column-resizing.md new file mode 100644 index 0000000000..0b98c60240 --- /dev/null +++ b/docs/framework/alpine/guide/column-resizing.md @@ -0,0 +1,387 @@ +--- +title: Column Resizing (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Resizing Setup + +Here's how you set up your table to use column resizing features. Column resizing depends on column sizing, so add `columnSizingFeature` before `columnResizingFeature`. Adding the column resizing feature enables the related APIs. + +```ts +import { createTable, tableFeatures, columnSizingFeature, columnResizingFeature } from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Resizing (Alpine) Guide + +TanStack Table provides built-in column resizing state and APIs that allow you to easily implement column resizing in your table UI with a variety of options for UX and performance. + +Column resizing builds on column sizing. If you only need to define starting, minimum, or maximum widths, see the [Column Sizing Guide](./column-sizing). + +### Enable Column Resizing + +To use column resizing, add `columnSizingFeature` and then `columnResizingFeature` to your features. The `column.getCanResize()` API will return `true` by default for all columns, but you can either disable column resizing for all columns with the `enableColumnResizing` table option, or disable column resizing on a per-column basis with the `enableResizing` column option. + +```ts +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, + createTable, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns = [ + { + accessorKey: 'id', + enableResizing: false, // disable resizing for just this column + size: 200, // starting column size + }, + //... +] + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Column Resize Mode + +By default, the column resize mode is set to `"onEnd"`. This means that the `column.getSize()` API will not return the new column size until the user has finished resizing (dragging) the column. Usually a small UI indicator will be displayed while the user is resizing the column. + +The `"onEnd"` default exists because immediate resize updates can be expensive in large or complex tables: every drag movement updates the `columnSizing` state, and anything that reads column widths recomputes. Alpine's per-binding reactivity helps here, since only the bindings that actually read the sizing state re-run, but if every header and cell reads `column.getSize()` directly, a complex table can still stutter during an `"onChange"` drag. The `"onEnd"` mode sidesteps this by deferring the size update until the drag finishes. + +> Advanced column resizing performance tips will be discussed [down below](#advanced-column-resizing-performance). + +If you want to change the column resize mode to `"onChange"` for immediate column resizing renders, you can do so with the `columnResizeMode` table option. + +```ts +const table = createTable({ + //... + columnResizeMode: 'onChange', // change column resize mode to "onChange" +}) +``` + +### Column Resize Direction + +By default, TanStack Table assumes that the table markup is laid out in a left-to-right direction. For right-to-left layouts, you may need to change the column resize direction to `"rtl"`. + +```ts +const table = createTable({ + //... + columnResizeDirection: 'rtl', // change column resize direction to "rtl" for certain locales +}) +``` + +### Connect Column Resizing APIs to UI + +There are a few really handy APIs that you can use to hook up your column resizing drag interactions to your UI. + +#### Column Size APIs + +To apply the size of a column to the column head cells, data cells, or footer cells, you can use the following APIs: + +```ts +header.getSize() +column.getSize() +cell.column.getSize() +``` + +How you apply these size styles to your markup is up to you, but it is pretty common to use either CSS variables or inline styles to apply the column sizes. Because table reads are reactive inside Alpine bindings, a `:style` that reads `header.getSize()` updates automatically as the size changes: + +```html + +``` + +Though, as discussed in the [advanced column resizing performance section](#advanced-column-resizing-performance), you may want to consider using CSS variables to apply column sizes to your markup. + +#### Column Resize APIs + +TanStack Table provides a pre-built event handler to make your drag interactions easy to implement. These event handlers are just convenience functions that call other internal APIs to update the column sizing state and re-render the table. Use `header.getResizeHandler()` to connect to your column resize drag interactions, for both mouse and touch events. The handler is returned by `getResizeHandler()` and called with the event, so the pattern in markup is `header.getResizeHandler()($event)`. + +```html +
+``` + +#### Column Resize Indicator with Column Resizing State + +TanStack Table keeps track of a `columnResizing` state object that you can use to render a column resize indicator UI. Read it with `table.atoms.columnResizing.get()`. The `:style` binding that uses `header.column.getIsResizing()` and the resizing state stays reactive, so the indicator follows the drag. + +When using the `"onEnd"` resize mode, the size only updates when the drag finishes, so you translate the resize handle by the live `deltaOffset` while dragging. A method on your `Alpine.data` object is a convenient place to compute that transform: + +```ts +Alpine.data('table', () => { + const local = Alpine.reactive({ + data: makeData(10), + columnResizeMode: 'onEnd' as ColumnResizeMode, + columnResizeDirection: 'ltr' as ColumnResizeDirection, + }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + get columnResizeMode() { + return local.columnResizeMode + }, + get columnResizeDirection() { + return local.columnResizeDirection + }, + }) + + return { + table, + FlexRender, + local, + // Translate the resizer while dragging when using the "onEnd" resize mode. + resizerTransform(header: any) { + if (local.columnResizeMode === 'onEnd' && header.column.getIsResizing()) { + const delta = table.atoms.columnResizing.get().deltaOffset ?? 0 + const dir = local.columnResizeDirection === 'rtl' ? -1 : 1 + return `transform: translateX(${dir * delta}px)` + } + return '' + }, + } +}) +``` + +```html +
+``` + +This is the same pattern the [Column Resizing example](../examples/column-resizing) uses. + +The `columnResizing` state stores transient drag information: + +```ts +type columnResizingState = { + columnSizingStart: Array<[string, number]> + deltaOffset: null | number + deltaPercentage: null | number + isResizingColumn: false | string + startOffset: null | number + startSize: null | number +} +``` + +You rarely need to manage this transient drag state yourself, but if you do, the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can observe the resize state without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import type { columnResizingState } from '@tanstack/alpine-table' + +const columnResizingAtom = createAtom({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +// subscribe wherever it is needed +columnResizingAtom.subscribe(() => { + // react to resize state changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + columnResizing: columnResizingAtom, + }, +}) +``` + +Alternatively, the v8-style `state.columnResizing` plus `onColumnResizingChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + columnResizing: { + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, + } as columnResizingState, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get columnResizing() { + return local.columnResizing // connect the reactive slice back down to the table + }, + }, + onColumnResizingChange: (updater) => { + local.columnResizing = + typeof updater === 'function' ? updater(local.columnResizing) : updater + }, +}) +``` + +### Column Resizing APIs + +Use `header.getResizeHandler()` to connect mouse or touch events to the resizing logic. Use `column.getCanResize()` to decide whether to render a resize handle, and `column.getIsResizing()` to render active resizing UI. + +```ts +header.getResizeHandler() +column.getCanResize() +column.getIsResizing() +``` + +The table instance exposes APIs for the transient resize state. Note that the current v9 API spelling is `table.setcolumnResizing` with a lowercase `c` in `column`; use that exact name. + +```ts +table.setcolumnResizing((old) => ({ + ...old, + deltaOffset: 12, +})) + +table.resetHeaderSizeInfo() +table.resetHeaderSizeInfo(true) +``` + +### Advanced Column Resizing Performance + +Alpine's per-binding reactivity means you usually do not have to fight whole-component re-renders the way React users do. But in a large or complex table where every header and every data cell reads `column.getSize()` directly, an `"onChange"` resize drag still triggers a lot of recomputation per frame. + +We have created a [performant column resizing example](../examples/column-resizing-performant) that demonstrates how to keep column resizing smooth with a complex table that has artificially slow cell renders. It is recommended that you just look at that example to see how it is done, but these are the basic things to keep in mind: + +1. Don't read `column.getSize()` in every header and every data cell. Instead, calculate all column widths once in a single method that maps header and column ids to CSS variable values. Touch `table.atoms.columnResizing.get()` inside that method so Alpine tracks it as a dependency and recomputes the variables while a column is being resized. +2. Use CSS variables (e.g. `width: calc(var(--col-firstName-size) * 1px)`) to communicate column widths to your table cells. During a drag, only the binding that produces the variables re-runs and the browser applies the new widths; the cell bindings themselves do not recompute their width. + +The example computes the variables in a method on the `Alpine.data` object and binds them once on the table container: + +```ts +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(200) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + defaultColumn: { minSize: 60, maxSize: 800 }, + columnResizeMode: 'onChange', + }) + + return { + table, + FlexRender, + columnSizeVars(): string { + // touch the resizing state so Alpine tracks it as a dependency + void table.atoms.columnResizing.get().columnSizingStart + const headers = table.getFlatHeaders() + const styles: Array = [] + for (const header of headers) { + styles.push(`--header-${header.id}-size:${header.getSize()}`) + styles.push(`--col-${header.column.id}-size:${header.column.getSize()}`) + } + styles.push(`width:${table.getTotalSize()}px`) + return styles.join(';') + }, + } +}) +``` + +```html +
+
+ +
+
+ +
+
+``` + +If you follow these steps, you should see significant performance improvements while resizing columns. diff --git a/docs/framework/alpine/guide/column-sizing.md b/docs/framework/alpine/guide/column-sizing.md new file mode 100644 index 0000000000..0cad76e107 --- /dev/null +++ b/docs/framework/alpine/guide/column-sizing.md @@ -0,0 +1,220 @@ +--- +title: Column Sizing (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Sizing](../examples/column-sizing) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Sizing Setup + +Here's how you set up your table to use column sizing features. Adding the column sizing feature enables the related APIs. + +```ts +import { createTable, tableFeatures, columnSizingFeature } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnSizingFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Sizing (Alpine) Guide + +The column sizing feature allows you to optionally specify the width of each column including min and max widths. + +If you want users to dynamically change column widths by dragging column headers, see the [Column Resizing Guide](./column-resizing). + +### Column Widths + +Columns by default are given the following measurement options: + +```ts +export const defaultColumnSizing = { + size: 150, + minSize: 20, + maxSize: Number.MAX_SAFE_INTEGER, +} +``` + +These defaults can be overridden by both `tableOptions.defaultColumn` and individual column defs, in that order. + +```ts +const features = tableFeatures({ columnSizingFeature }) + +const columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +const table = createTable({ + features, + defaultColumn: { + size: 200, // starting column size + minSize: 50, // enforced during column resizing + maxSize: 500, // enforced during column resizing + }, + //... +}) +``` + +The column "sizes" are stored in the table state as numbers, and are usually interpreted as pixel unit values, but you can hook up these column sizing values to your css styles however you see fit. + +As a headless utility, table logic for column sizing is really only a collection of states that you can apply to your own layouts how you see fit (our example above implements 2 styles of this logic). You can apply these width measurements in a variety of ways: + +- semantic `table` elements or any elements being displayed in a table css mode +- `div/span` elements or any elements being displayed in a non-table css mode + - Block level elements with strict widths + - Absolutely positioned elements with strict widths + - Flexbox positioned elements with loose widths + - Grid positioned elements with loose widths +- Really any layout mechanism that can interpolate cell widths into a table structure. + +Each of these approaches has its own tradeoffs and limitations which are usually opinions held by a UI/component library or design system, luckily not you 😉. + +### Applying Column Sizes + +To apply the calculated size to your markup, read `header.getSize()` or `column.getSize()` inside an Alpine binding. Because table reads are reactive in Alpine bindings, the widths update automatically when the sizing state changes. A common approach is an inline `:style` that interpolates the size into a pixel width. + +```html +
+
+ +
+ +
+ + + + + + +
+``` + +### Column Sizing APIs + +Use the column and header APIs to read the calculated size and offsets for rendering. These values come from the `columnSizing` state and the column definition defaults. + +```ts +column.getSize() +header.getSize() + +column.getStart() // left offset in the current column flow +column.getStart('left') +column.getStart('center') +column.getStart('right') + +column.getAfter() // right offset in the current column flow +column.getAfter('left') +column.getAfter('center') +column.getAfter('right') + +column.resetSize() +``` + +The table instance also exposes total size helpers. These are useful when building scroll containers, split pinned-column tables, or CSS variables for column widths. + +```ts +table.getTotalSize() +table.getLeftTotalSize() +table.getCenterTotalSize() +table.getRightTotalSize() +``` + +If you need to update sizing state directly, use `table.setColumnSizing`. Use `table.resetColumnSizing` to reset to `initialState.columnSizing`, or pass `true` to reset to the feature default. + +```ts +table.setColumnSizing({ + firstName: 180, + age: 80, +}) + +table.resetColumnSizing() +table.resetColumnSizing(true) +``` + +### Managing Column Sizing State + +If you need to own the `columnSizing` state yourself (for example, to persist user-set column widths), the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the sizing state without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import type { ColumnSizingState } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnSizingFeature }) + +const columnSizingAtom = createAtom({}) + +// subscribe wherever it is needed +columnSizingAtom.subscribe(() => { + // react to sizing changes (e.g. persist widths) +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + columnSizing: columnSizingAtom, + }, +}) +``` + +Alternatively, the v8-style `state.columnSizing` plus `onColumnSizingChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const features = tableFeatures({ columnSizingFeature }) + +const local = Alpine.reactive({ columnSizing: {} as ColumnSizingState }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get columnSizing() { + return local.columnSizing // connect the reactive slice back down to the table + }, + }, + onColumnSizingChange: (updater) => { + local.columnSizing = + typeof updater === 'function' ? updater(local.columnSizing) : updater + }, +}) +``` diff --git a/docs/framework/alpine/guide/column-visibility.md b/docs/framework/alpine/guide/column-visibility.md new file mode 100644 index 0000000000..95d0566269 --- /dev/null +++ b/docs/framework/alpine/guide/column-visibility.md @@ -0,0 +1,220 @@ +--- +title: Column Visibility (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Column Visibility](../examples/column-visibility) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Column Visibility Setup + +Here's how you set up your table to use column visibility features. Adding the column visibility feature enables the related APIs. + +```ts +import { + columnVisibilityFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Column Visibility (Alpine) Guide + +The column visibility feature allows table columns to be hidden or shown dynamically. In v9, add `columnVisibilityFeature` to your `features` to enable this. There is a dedicated `columnVisibility` state and APIs for managing column visibility dynamically. + +### Column Visibility State + +The `columnVisibility` state is a map of column IDs to boolean values. A column will be hidden if its ID is present in the map and the value is `false`. If the column ID is not present in the map, or the value is `true`, the column will be shown. + +If you need to own the `columnVisibility` state yourself (for example, to persist user preferences), the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the visibility state without going through the component that owns the table. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. + +```ts +import { createAtom } from '@tanstack/store' +import { + columnVisibilityFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import type { ColumnVisibilityState } from '@tanstack/alpine-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const columnVisibilityAtom = createAtom({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +// subscribe to the atom wherever you need the value +columnVisibilityAtom.subscribe(() => { + // react to visibility changes +}) + +const table = createTable({ + features, + //... + atoms: { + columnVisibility: columnVisibilityAtom, + }, +}) +``` + +Alternatively, the v8-style `state.columnVisibility` plus `onColumnVisibilityChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + } as ColumnVisibilityState, +}) + +const table = createTable({ + features, + //... + state: { + get columnVisibility() { + return local.columnVisibility // connect the reactive slice back down to the table + }, + //... + }, + onColumnVisibilityChange: (updater) => { + local.columnVisibility = + typeof updater === 'function' + ? updater(local.columnVisibility) + : updater + }, +}) +``` + +Alternatively, if you don't need to manage the column visibility state outside of the table, you can still set the initial default column visibility state using the `initialState` option. + +> **Note**: If `columnVisibility` is provided to both `initialState` and a controlled option (`atoms` or `state`), the controlled value will take precedence and `initialState` will be ignored. Only provide `columnVisibility` in one place. + +```ts +const features = tableFeatures({ columnVisibilityFeature }) + +const table = createTable({ + features, + //... + initialState: { + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, + //... + }, +}) +``` + +### Disable Hiding Columns + +By default, all columns can be hidden or shown. If you want to prevent certain columns from being hidden, you set the `enableHiding` column option to `false` for those columns. + +```ts +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessorKey: 'name', // can be hidden + }, +] +``` + +### Column Visibility Toggle APIs + +There are several column API methods that are useful for rendering column visibility toggles in the UI. + +- `column.getCanHide` - Useful for disabling the visibility toggle for a column that has `enableHiding` set to `false`. +- `column.getIsVisible` - Useful for setting the initial state of the visibility toggle. +- `column.toggleVisibility` - Useful for toggling the visibility of a column. +- `column.getToggleVisibilityHandler` - Shortcut for hooking up the `column.toggleVisibility` method to a UI event handler. + +Render a checkbox per column on real elements (not inside `x-html`). Bind `:checked` to `column.getIsVisible()`, `:disabled` to `!column.getCanHide()`, and call the handler returned by `getToggleVisibilityHandler` from `@change`. + +```html + +``` + +A "Toggle All" checkbox can use the table-level helpers `table.getIsAllColumnsVisible()` and `table.getToggleAllColumnsVisibilityHandler()`. + +```html + +``` + +### Column Visibility Aware Table APIs + +When you render your header, body, and footer cells, there are a lot of API options available. You may see APIs like `table.getAllLeafColumns` and `row.getAllCells`, but if you use these APIs, they will not take column visibility into account. Instead, you need to use the "visible" variants of these APIs, such as `table.getVisibleLeafColumns` and `row.getVisibleCells`. + +Render cell and header content with `x-html="FlexRender(...)"`, and use the visible-aware row models when iterating with `x-for`: + +```html + + + + + + + +
+``` + +If you are using the Header Group APIs, they will already take column visibility into account. diff --git a/docs/framework/alpine/guide/composable-tables.md b/docs/framework/alpine/guide/composable-tables.md new file mode 100644 index 0000000000..0cb7db7b36 --- /dev/null +++ b/docs/framework/alpine/guide/composable-tables.md @@ -0,0 +1,148 @@ +--- +title: Composable Tables (createTableHook) Guide +--- + +`createTableHook` creates an app-specific table factory. Use it to define shared features, row models, and default table options once, then create each Alpine table with the columns and data that are unique to that table. + +> [!NOTE] +> Unlike the React, Solid, Lit, and Svelte adapters, the Alpine `createTableHook` does not register reusable cell/header/table components. Alpine renders cell and header content as HTML strings through `x-html`, and Alpine has no component primitive to bind, so the hook is focused on sharing features and default options. Reusable interactive markup is expressed with [`Alpine.bind`](https://alpinejs.dev/globals/alpine-bind) bundles in your templates instead. + +## Examples + +- [Basic App Table](../examples/basic-app-table) - Minimal `createTableHook` setup. + +## Start With Shared Features and Options + +Create one app table hook and put the feature set, row models, and shared defaults there. This example makes sorting available to every table created by `createAppTable`. + +```ts +import { + createSortedRowModel, + createTableHook, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { createAppTable, createAppColumnHelper } = createTableHook({ + features, + debugTable: true, + enableSortingRemoval: false, +}) +``` + +Options passed to `createTableHook` become defaults for every table created by `createAppTable`. The `features` option is also bound to the returned column helper, so column definitions know that sorting APIs are available. + +## Create App Columns + +Create one column helper per row type. The helper is already bound to your app's feature set, so each table does not need to thread `typeof features` through its column definitions. Renderers return HTML strings, which are rendered with `x-html` via `table.FlexRender`. + +```ts +type Person = { + firstName: string + lastName: string + age: number + visits: number +} + +const columnHelper = createAppColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => 'Last Name', + cell: (info) => `${info.getValue()}`, + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), +]) +``` + +## Create A Table + +Create each table with `createAppTable` inside an `Alpine.data` component. The call site provides table-specific inputs such as `columns` and reactive `data`; shared features and defaults come from the hook. + +```ts +import Alpine from 'alpinejs' + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: [] as Array }) + + const table = createAppTable({ + columns, + get data() { + return local.data + }, + }) + + return { table } +}) + +window.Alpine = Alpine +Alpine.start() +``` + +## Render With The Normal Table APIs + +You render the table with the same table instance APIs used by a standalone `createTable` table. `table.FlexRender` is attached to the instance, so you do not need to import the top-level helper. Attach the sort click handler to a real element (Alpine does not initialize directives inside `x-html`). + +```html +
+ + + + + + + +
+
+``` + +## Override Shared Defaults Per Table + +Options passed to `createAppTable` override defaults from `createTableHook`. Use this for the few tables that need different behavior without creating a separate app hook. + +```ts +const table = createAppTable({ + columns, + get data() { + return local.data + }, + enableSortingRemoval: true, // override the hook default for this table only +}) +``` + +## When To Use This Pattern + +Use `createTableHook` when multiple tables should share features, row models, default options, or conventions. Use the standalone `createTable` API for a one-off table. diff --git a/docs/framework/alpine/guide/custom-features.md b/docs/framework/alpine/guide/custom-features.md new file mode 100644 index 0000000000..41dc5d9d08 --- /dev/null +++ b/docs/framework/alpine/guide/custom-features.md @@ -0,0 +1,350 @@ +--- +title: Custom Features (Alpine) Guide +--- + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +## Custom Features (Alpine) Guide + +In this guide, we'll cover how to extend TanStack Table with custom features, and along the way, we'll learn more about how the TanStack Table v9 codebase is structured and how it works. + +### TanStack Table Strives to be Lean + +TanStack Table has a core set of features that are built into the library such as sorting, filtering, pagination, etc. We've received a lot of requests and sometimes even some well thought out PRs to add even more features to the library. While we are always open to improving the library, we also want to make sure that TanStack Table remains a lean library that does not include too much bloat and code that is unlikely to be used in most use cases. Not every PR can, or should, be accepted into the core library, even if it does solve a real problem. This can be frustrating to developers where TanStack Table solves 90% of their use case, but they need a little bit more control. + +TanStack Table has always been built in a way that allows it to be highly extensible (at least since v7). The `table` instance that is returned from whichever framework adapter that you are using (`createTable` for Alpine, `useTable` for React, etc) is a plain JavaScript object that can have extra properties or APIs added to it. It has always been possible to use composition to add custom logic, state, and APIs to the table instance. Libraries like [Material React Table](https://github.com/KevinVandy/material-react-table/blob/v2/packages/material-react-table/src/hooks/useMRT_TableInstance.ts) have simply created custom wrappers around their adapter's table function to extend the table instance with custom functionality. + +In v9, TanStack Table uses the `features` option (via `tableFeatures()`) to declare which features your table uses. This enables tree-shaking: you only bundle the code for the features you need. You can add custom features to the table instance in exactly the same way as the built-in features. + +> In v9, features are opt-in. Use `tableFeatures({ ... })` to declare which features your table uses, including custom features. + +### How TanStack Table Features Work + +TanStack Table's source code is arguably somewhat simple (at least we think so). All code for each feature is split up into its own object/file with instantiation methods to create initial state, default table and column options, and API methods that can be added to the `table`, `header`, `column`, `row`, and `cell` instances. + +All of the functionality of a feature object can be described with the `TableFeature` type that is exported from TanStack Table. This type is a TypeScript interface that describes the shape of a feature object needed to create a feature. + +```ts +export interface TableFeature { + assignCellPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignColumnPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignHeaderPrototype?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + prototype: Record, + table: Table_Internal, + ) => void + assignRowPrototype?: ( + prototype: Record, + table: Table_Internal, + ) => void + constructTableAPIs?: ( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods do: + +#### Default Options and Initial State + +
+ +##### getDefaultTableOptions + +The `getDefaultTableOptions` method in a table feature is responsible for setting the default table options for that feature. For example, in the [Column Resizing](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/column-resizing/columnResizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +##### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature, the `getDefaultColumnDef` method sets the default `sortUndefined` column option with a default value of `1`. + +
+ +##### getInitialState + +The `getInitialState` method in a table feature is responsible for setting the default state for that feature. For example, in the [Pagination](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/row-pagination/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +
+ +##### constructTableAPIs + +The `constructTableAPIs` method in a table feature is responsible for adding methods to the `table` instance. For example, in the [Row Selection](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +##### assignHeaderPrototype + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. + +
+ +##### assignColumnPrototype + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. + +
+ +##### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +##### assignCellPrototype + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, the [Column Grouping](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts) feature adds cell instance API methods such as `getIsGrouped` and `getIsAggregated`. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +The feature object itself is framework-agnostic, so these steps apply to any adapter. You can follow along with the Alpine [Custom Plugin](../examples/custom-plugin) example. Here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/alpine-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +import { + assignTableAPIs, + functionalUpdate, + makeStateUpdater, +} from '@tanstack/alpine-table' +import type { TableFeature, Updater } from '@tanstack/alpine-table' + +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //.. +}) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs, options, and state in your application. Here the `density` state is owned externally in `Alpine.reactive` and connected with the new `onDensityChange` option: + +```ts +const features = tableFeatures({ densityPlugin }) + +const local = Alpine.reactive({ + data: makeData(1_000), + density: 'md', +}) as { data: Array; density: DensityState } + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + //... + state: { + // passing the density state to the table, TS is still happy :) + get density() { + return local.density + }, + }, + onDensityChange: (updater) => { + // raise density state changes to our own state management + local.density = + typeof updater === 'function' ? updater(local.density) : updater + }, +}) +``` + +Expose a `densityPadding()` helper on your `Alpine.data` object and use it from the template to drive the cell padding. The new `table.toggleDensity()` API can be wired to a real button. + +```ts +Alpine.data('table', () => { + // ...table setup from above... + + return { + table, + FlexRender, + densityPadding() { + return local.density === 'sm' + ? '4px' + : local.density === 'md' + ? '8px' + : '16px' + }, + } +}) +``` + +```html + + + +``` + +#### Do We Have to Do It This Way? + +This is just a new way to integrate custom code along-side the built-in features in TanStack Table. In our example up above, we could have just as easily stored the `density` state in `Alpine.reactive`, defined our own `toggleDensity` handler wherever, and just used it in our code separately from the table instance. Building table features along-side TanStack Table instead of deeply integrating them into the table instance is still a perfectly valid way to build custom features. Depending on your use case, this may or may not be the cleanest way to extend TanStack Table with custom features. diff --git a/docs/framework/alpine/guide/expanding.md b/docs/framework/alpine/guide/expanding.md new file mode 100644 index 0000000000..b283a2c750 --- /dev/null +++ b/docs/framework/alpine/guide/expanding.md @@ -0,0 +1,351 @@ +--- +title: Expanding (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Expanding](../examples/expanding) +- [Sub Components](../examples/sub-components) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Expanding Setup + +Here's how you set up your table to use expanding features. Adding the expanding feature enables the related APIs. Additionally, if using client-side expanding, you also need to set up `expandedRowModel` after its associated feature because row model slots are type-checked. + +```ts +import { + createExpandedRowModel, + createTable, + rowExpandingFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), // if using client-side expanding +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Expanding Feature (Alpine) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` and the `expandedRowModel` factory to your features: + +```ts +import { + createExpandedRowModel, + createTable, + rowExpandingFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), +}) + +const table = createTable({ + features, + // other options... +}) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are essentially child rows that inherit the same column structure as their parent rows. If your data object already includes these expanded rows data, you can utilize the `getSubRows` function to specify these child rows. However, if your data object does not contain the expanded rows data, they can be treated as custom expanded data, which is discussed in next section. + +For example, if you have a data object like this: + +```ts +type Person = { + id: number + name: string + age: number + children?: Person[] | undefined +} + +const data: Person[] = [ + { + id: 1, + name: 'John', + age: 30, + children: [ + { id: 2, name: 'Jane', age: 5 }, + { id: 5, name: 'Jim', age: 10 }, + ], + }, + { + id: 3, + name: 'Doe', + age: 40, + children: [{ id: 4, name: 'Alice', age: 10 }], + }, +] +``` + +Then you can use the getSubRows function to return the children array in each row as expanded rows. The table instance will now understand where to look for the sub rows on each row. + +```ts +const table = createTable({ + features, + getSubRows: (row) => row.children, // return the children array as sub-rows + // other options... +}) +``` + +> **Note:** You can have a complicated `getSubRows` function, but keep in mind that it will run for every row and every sub-row. This can be expensive if the function is not optimized. Async functions are not supported. + +### Custom Expanding UI + +In some cases, you may wish to show extra details or information, which may or may not be part of your table data object, such as expanded data for rows. This kind of expanding row UI has gone by many names over the years including "expandable rows", "detail panels", "sub-components", etc. + +By default, the `row.getCanExpand()` row instance API will return false unless it finds `subRows` on a row. This can be overridden by implementing your own `getRowCanExpand` function in the table instance options. + +Because Alpine does not initialize directives inside content set with `x-html`, render the detail panel content with `x-html`, while the expanded sub-row markup itself stays in your template. Use `x-if="row.getIsExpanded()"` to conditionally render the detail row. + +```ts +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(10, 5) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + getRowCanExpand: () => true, + }) + + return { + table, + FlexRender, + renderSubComponent(row) { + return `
${JSON.stringify(
+        row.original,
+        null,
+        2,
+      )}
` + }, + } +}) +``` + +```html + +``` + +### Expanded rows state + +If you need access to the expanded state of the rows in other parts of your application, you can own the `expanded` state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The atom can be read, written, or subscribed to anywhere in your app without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' +import type { ExpandedState } from '@tanstack/alpine-table' + +const expandedAtom = createAtom({}) + +// subscribe to the atom wherever you need the value +expandedAtom.subscribe(() => { + // react to expanded changes +}) + +const table = createTable({ + features, + // other options... + atoms: { + expanded: expandedAtom, // expanding APIs now update expandedAtom + }, +}) +``` + +Alternatively, the v8-style `state.expanded` plus `onExpandedChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ expanded: {} as ExpandedState }) + +const table = createTable({ + features, + // other options... + state: { + get expanded() { + return local.expanded // connect the reactive slice back down to the table + }, + }, + onExpandedChange: (updater) => { + local.expanded = + typeof updater === 'function' ? updater(local.expanded) : updater + }, +}) +``` + +You can read the current expanded value with `table.atoms.expanded.get()`. Inside an Alpine binding this is a reactive read; in event handlers it simply returns the current value. + +The ExpandedState type is defined as follows: + +```ts +type ExpandedState = true | Record +``` + +If the ExpandedState is true, it means all rows are expanded. If it's a record, only the rows with IDs present as keys in the record and have their value set to true are expanded. For example, if the expanded state is { row1: true, row2: false }, it means the row with ID row1 is expanded and the row with ID row2 is not expanded. This state is used by the table to determine which rows are expanded and should display their subRows, if any. + +### UI toggling handler for expanded rows + +TanStack table will not add a toggling handler UI for expanded data to your table. You should manually add it within each row's UI to allow users to expand and collapse the row. Because Alpine does not initialize directives inside content set with `x-html`, the expander button cannot live inside an `x-html` cell. Instead, special-case the expander column by its column id directly in your markup and attach the handler returned by `getToggleExpandedHandler` to a real button. + +```html + + + + + + +``` + +### Expanding APIs + +Rows expose helpers for reading and toggling their expanded state: + +```ts +row.getCanExpand() +row.getIsExpanded() +row.getIsAllParentsExpanded() +row.getToggleExpandedHandler() +row.toggleExpanded() +``` + +The table instance exposes helpers for reading and toggling aggregate expanded state: + +```ts +table.getCanSomeRowsExpand() +table.getIsAllRowsExpanded() +table.getIsSomeRowsExpanded() +table.getExpandedDepth() +table.getToggleAllRowsExpandedHandler() +table.toggleAllRowsExpanded() +table.resetExpanded() +``` + +Use `table.setExpanded` to update the expanded state directly. `table.resetExpanded()` resets to `initialState.expanded`, while `table.resetExpanded(true)` clears the expanded state. + +### Filtering Expanded Rows + +By default, the filtering process starts from the parent rows and moves downwards. This means if a parent row is excluded by the filter, all its child rows will also be excluded. However, you can change this behavior by using the `filterFromLeafRows` option. When this option is enabled, the filtering process starts from the leaf (child) rows and moves upwards. This ensures that a parent row will be included in the filtered results as long as at least one of its child or grandchild rows meets the filter criteria. Additionally, you can control how deep into the child hierarchy the filter process goes by using the `maxLeafRowFilterDepth` option. This option allows you to specify the maximum depth of child rows that the filter should consider. + +```ts +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns, +}) + +//... +const table = createTable({ + features, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +}) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their parents page. This also means more rows will be rendered than the set page size) you can use the `paginateExpandedRows` option. + +```ts +const table = createTable({ + features, + // other options... + paginateExpandedRows: false, +}) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Row Pinning Guide](./row-pinning) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Auto Reset Expanded State + +If you are also using the grouping feature, the `expanded` state is automatically reset whenever the grouped row model recomputes, such as when the `data` or the grouping state changes. This default is automatically disabled when `manualExpanding` is `true`, but it can be overridden by explicitly assigning a boolean value to the `autoResetExpanded` table option. There is also a global `autoResetAll` table option that disables (or enables) every auto-reset behavior at once. + +A common reason to set `autoResetExpanded: false` is editing data while viewing the table (for example, inline cell editing). Every edit updates `data`, which recomputes the row models and would otherwise collapse the user's expanded rows. If you also use the pagination feature, pair it with `autoResetPageIndex: false` so the current page is kept as well. + +```ts +const table = createTable({ + features, + // other options... + autoResetExpanded: false, // keep expanded state when data changes + // autoResetAll: false, // or turn off all auto resets at once +}) +``` + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +const features = tableFeatures({ rowExpandingFeature }) + +const table = createTable({ + features, + // other options... + manualExpanding: true, +}) +``` diff --git a/docs/framework/alpine/guide/fuzzy-filtering.md b/docs/framework/alpine/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..ef63d72a10 --- /dev/null +++ b/docs/framework/alpine/guide/fuzzy-filtering.md @@ -0,0 +1,231 @@ +--- +title: Fuzzy Filtering (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Filters](../examples/filters) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Fuzzy Filtering Setup + +Here's how you set up your table to use fuzzy filtering features. Adding the fuzzy filtering feature enables the related APIs. Additionally, if using client-side fuzzy filtering and sorting, you also need to set up `filteredRowModel` and `sortedRowModel` after their associated features because row model slots are type-checked. + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createSortedRowModel, + createTable, + filterFns, + globalFilteringFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + sortedRowModel: createSortedRowModel(), // if using client-side sorting + filterFns, + sortFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Fuzzy Filtering (Alpine) Guide + +Fuzzy filtering is a technique that allows you to filter data based on approximate matches. This can be useful when you want to search for data that is similar to a given value, rather than an exact match. + +You can implement a client side fuzzy filtering by defining a custom filter function. This function should take in the row, columnId, and filter value, and return a boolean indicating whether the row should be included in the filtered data. + +Fuzzy filtering is mostly used with global filtering, but you can also apply it to individual columns. We will discuss how to implement fuzzy filtering for both cases. + +> **Note:** You will need to install the `@tanstack/match-sorter-utils` library to use fuzzy filtering. +> TanStack Match Sorter Utils is a fork of [match-sorter](https://github.com/kentcdodds/match-sorter) by Kent C. Dodds. It was forked in order to work better with TanStack Table's row by row filtering approach. + +```bash +npm install @tanstack/match-sorter-utils +``` + +Using the match-sorter libraries is optional, but the TanStack Match Sorter Utils library provides a great way to both fuzzy filter and sort by the rank information it returns, so that rows can be sorted by their closest matches to the search query. + +### Defining a Custom Fuzzy Filter Function + +Here's an example of a custom fuzzy filter function: + +```ts +import { rankItem } from '@tanstack/match-sorter-utils' +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import type { FilterFn, RowData, TableFeatures } from '@tanstack/alpine-table' + +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} +type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } + +const fuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta?.({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the `rankItem` function from the `@tanstack/match-sorter-utils` library to rank the item. We then store the ranking information in the filter meta of the row (the `addMeta` callback is optional, so call it with optional chaining), and return whether the item passed the ranking criteria. + +To reference this filter function by the string name `'fuzzy'` (and to type the stored filter meta), register it in the `filterFns` slot on `tableFeatures` and declare a `filterMeta` slot for the meta type: + +```ts +import { metaHelper } from '@tanstack/alpine-table' +import type { FilterFn, RowData, TableFeatures } from '@tanstack/alpine-table' + +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} +type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } + +const fuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + const itemRank = rankItem(row.getValue(columnId), value) + addMeta?.({ itemRank }) + return itemRank.passed +} + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: { ...filterFns, fuzzy: fuzzyFilter }, + sortFns, + filterMeta: metaHelper(), +}) +``` + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, register the fuzzy filter function in the `filterFns` slot on `tableFeatures` and reference it in the `globalFilterFn` option of the table: + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createSortedRowModel, + createTable, + filterFns, + globalFilteringFeature, + metaHelper, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), // needed if you want sorting with fuzzy rank + filterFns: { ...filterFns, fuzzy: fuzzyFilter }, + sortFns, + filterMeta: metaHelper(), +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + globalFilterFn: 'fuzzy', +}) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, pass your fuzzy filter function to `createFilteredRowModel` (merging it with the built-in `filterFns`). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```ts +const column = [ + { + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +] +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```ts +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFns } from '@tanstack/alpine-table' +import type { SortFn } from '@tanstack/alpine-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId].itemRank!, + rowB.columnFiltersMeta[columnId].itemRank!, + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then pass this sorting function directly to the `sortFn` option of the column definition: + +```ts +{ + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + filterFn: 'fuzzy', // using our custom fuzzy filter function (registered above) + sortFn: fuzzySort, // pass our custom fuzzy sort function directly +} +``` + +> **Note:** Unlike `filterFn: 'fuzzy'` above, `fuzzySort` is passed as a function rather than a string. A string reference like `sortFn: 'fuzzySort'` would only work if you also registered the function in the `sortFns` slot on `tableFeatures` (e.g. `sortFns: { ...sortFns, fuzzySort }`). Passing the function directly skips that step. diff --git a/docs/framework/alpine/guide/global-filtering.md b/docs/framework/alpine/guide/global-filtering.md new file mode 100644 index 0000000000..b34e7219a4 --- /dev/null +++ b/docs/framework/alpine/guide/global-filtering.md @@ -0,0 +1,280 @@ +--- +title: Global Filtering (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Faceted Filters](../examples/filters-faceted) +- [Column Filters](../examples/filters) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Global Filtering Setup + +Here's how you set up your table to use global filtering features. Global filtering depends on column filtering, so add `columnFilteringFeature` before `globalFilteringFeature`. Adding the global filtering feature enables the related APIs. Additionally, if using client-side filtering, you also need to set up `filteredRowModel` after its associated feature because row model slots are type-checked. + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createTable, + filterFns, + globalFilteringFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + filterFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Global Filtering (Alpine) Guide + +Filtering comes in 2 flavors: Column Filtering and Global Filtering. + +This guide will focus on global filtering, which is a filter that is applied across all columns. + +### Client-Side vs Server-Side Filtering + +If you have a large dataset, you may not want to load all of that data into the client's browser in order to filter it. In this case, you will most likely want to implement server-side filtering, sorting, pagination, etc. + +However, as also discussed in the [Pagination Guide](./pagination#should-you-use-client-side-pagination), a lot of developers underestimate how many rows can be loaded client-side without a performance hit. The TanStack table examples are often tested to handle up to 100,000 rows or more with decent performance for client-side filtering, sorting, pagination, and grouping. This doesn't necessarily mean that your app will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side filtering, sorting, pagination, and grouping that TanStack table provides. + +> TanStack Table can handle thousands of client-side rows with good performance. Don't rule out client-side filtering, pagination, sorting, etc. without some thought first. + +Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: + +1. Can your server query all of the data in a reasonable amount of time (and cost)? +2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) +3. Is the client's browser using too much memory if all of the data is loaded at once? + +If you're not sure, you can always start with client-side filtering and pagination and then switch to server-side strategies in the future as your data grows. + +### Manual Server-Side Global Filtering + +If you have decided that you need to implement server-side global filtering instead of using the built-in client-side global filtering, here's how you do that. + +No `filteredRowModel` is needed for manual server-side global filtering. Instead, the `data` that you pass to the table should already be filtered. However, if you have added a `filteredRowModel` to `tableFeatures`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```ts +import { + columnFilteringFeature, + createTable, + globalFilteringFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + manualFiltering: true, +}) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` (along with its required `columnFilteringFeature` prerequisite) and the `filteredRowModel` factory to your features: + +```ts +import { + columnFilteringFeature, + createFilteredRowModel, + createTable, + filterFns, + globalFilteringFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns, +}) + +const table = createTable({ + features, + // other options... +}) +``` + +### Global Filter Function + +The `globalFilterFn` option allows you to specify the filter function that will be used for global filtering. The filter function can be a string that references a built-in filter function, a string that references a custom filter function registered in the `filterFns` slot on `tableFeatures`, or a custom filter function passed directly. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + globalFilterFn: 'includesString', // built-in filter function +}) +``` + +By default there are 12 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equals` - Strict equality `===` +- `weakEquals` - Weak equality `==` +- `arrIncludes` - The row's array (or string) value includes at least one of the filter values +- `arrIncludesAll` - The row's array value includes every filter value +- `arrIncludesSome` - The row's array value includes at least one of the filter values +- `arrHas` - The row's scalar value equals at least one of the filter values +- `inNumberRange` - Inclusive `[min, max]` number range (endpoints normalized and swapped if reversed) +- `between` - Exclusive min/max range (blank endpoints are open-ended) +- `betweenInclusive` - Inclusive min/max range (blank endpoints are open-ended) + +You can also define your own custom global filter function and pass it directly to the `globalFilterFn` table option, as shown [below](#custom-global-filter-function). + +### Global Filter State + +The `globalFilter` state slice holds the current global filter value, usually a search string (the slice is typed as `any` so custom global filter functions can accept other value shapes). The table's state atoms are reactive in Alpine. `table.atoms.globalFilter.get()` is a reactive read when used inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. + +If you need access to the global filter state outside of the table, you can own the slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The filter value can be read, written, or subscribed to elsewhere (such as in a query key for server-side filtering) without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' + +const globalFilterAtom = createAtom('') + +// subscribe to the atom wherever you need the value (e.g. for a query key) +globalFilterAtom.subscribe(() => { + // react to global filter changes +}) + +const table = createTable({ + features, + // other options... + atoms: { + globalFilter: globalFilterAtom, // table.setGlobalFilter now updates globalFilterAtom + }, +}) +``` + +Alternatively, the v8-style `state.globalFilter` plus `onGlobalFilterChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ globalFilter: '' }) + +const table = createTable({ + features, + // other options... + state: { + get globalFilter() { + return local.globalFilter // connect the reactive slice back down to the table + }, + }, + onGlobalFilterChange: (updater) => { + local.globalFilter = + typeof updater === 'function' ? updater(local.globalFilter) : updater + }, +}) +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. Bind the input's `:value` to `table.atoms.globalFilter.get()` (a reactive read inside the binding) and update it from `@input` with `table.setGlobalFilter`. Put interactivity on real elements, not inside `x-html`. + +```html + +``` + +### Custom Global Filter Function + +If you want to use a custom global filter function, you can define the function and pass it to the `globalFilterFn` option. + +> **Note:** It is often a popular idea to use fuzzy filtering functions for global filtering. This is discussed in the [Fuzzy Filtering Guide](./fuzzy-filtering). + +```ts +const customFilterFn = (row, columnId, filterValue) => { + return // true if the row should be included in the filtered rows +} + +const table = createTable({ + features, + // other options... + globalFilterFn: customFilterFn, +}) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table `initialState` option. However, if you are controlling the slice yourself, set the starting value on your external atom or reactive state instead. + +```ts +const table = createTable({ + features, + // other options... + initialState: { + globalFilter: 'search term', // if not controlling globalFilter state, set initial state here + }, +}) +``` + +> NOTE: Do not use both `initialState.globalFilter` and a controlled `globalFilter` (via `atoms` or `state`) at the same time, as the controlled value will override `initialState.globalFilter`. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```ts +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +const table = createTable({ + features, + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +}) +``` + +### Global Filter APIs + +There are several APIs that are useful for hooking up your global filter UI: + +- `table.setGlobalFilter` - Set the global filter value. Useful for connecting a search input's `input` handler. +- `table.resetGlobalFilter` - Reset the global filter value to its initial state, or clear it with `table.resetGlobalFilter(true)`. +- `table.getGlobalFilterFn` - Returns the filter function currently used for global filtering. +- `table.getGlobalAutoFilterFn` - Returns the default global filter function (currently `includesString`). +- `column.getCanGlobalFilter` - Returns whether a column participates in global filtering. Useful for debugging which columns are searched. diff --git a/docs/framework/alpine/guide/grouping.md b/docs/framework/alpine/guide/grouping.md new file mode 100644 index 0000000000..d75c87af6f --- /dev/null +++ b/docs/framework/alpine/guide/grouping.md @@ -0,0 +1,353 @@ +--- +title: Grouping (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Grouping](../examples/grouping) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Grouping Setup + +Here's how you set up your table to use grouping features. Adding the grouping feature enables the related APIs. Additionally, if using client-side grouping, you also need to set up `groupedRowModel` after its associated feature because row model slots are type-checked. + +```ts +import { + aggregationFns, + columnGroupingFeature, + createGroupedRowModel, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), // if using client-side grouping + aggregationFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Grouping (Alpine) Guide + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +Grouping can also affect column order. There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into left, center (unpinned), and right pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +To use the grouping feature, add the `columnGroupingFeature` and the `groupedRowModel` factory to your features. The grouped row model is responsible for grouping the rows based on the grouping state. + +```ts +import { + aggregationFns, + columnGroupingFeature, + createGroupedRowModel, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), + aggregationFns, +}) + +const table = createTable({ + features, + // other options... +}) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```ts +const features = tableFeatures({ + columnGroupingFeature, + rowExpandingFeature, + groupedRowModel: createGroupedRowModel(), + expandedRowModel: createExpandedRowModel(), + aggregationFns, +}) + +const table = createTable({ + features, + // other options... +}) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```ts +table.setGrouping(['column1', 'column2']) +``` + +You can also reset the grouping state to its initial state using the resetGrouping function: + +```ts +table.resetGrouping() +``` + +By default, when a column is grouped, it is moved to the start of the table. You can control this behavior using the groupedColumnMode option. If you set it to 'reorder', then the grouped columns will be moved to the start of the table. If you set it to 'remove', then the grouped columns will be removed from the table. If you set it to false, then the grouped columns will not be moved or removed. + +```ts +const table = createTable({ + features, + // other options... + groupedColumnMode: 'reorder', +}) +``` + +### Aggregations + +When rows are grouped, you can aggregate the data in the grouped rows by columns using the `aggregationFn` column option. This is a string that is the name of a built-in aggregation function, or a custom aggregation function registered in the registry passed to `createGroupedRowModel`. + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'sum', +}) +``` + +In the above example, the sum aggregation function will be used to aggregate the data in the grouped rows. +By default, numeric columns will use the sum aggregation function, and non-numeric columns will use the count aggregation function. You can override this behavior by specifying the aggregationFn option in the column definition. + +There are several built-in aggregation functions that you can use: + +- sum - Sums the values in the grouped rows. +- count - Counts the number of rows in the grouped rows. +- min - Finds the minimum value in the grouped rows. +- max - Finds the maximum value in the grouped rows. +- extent - Finds the extent (min and max) of the values in the grouped rows. +- mean - Finds the mean of the values in the grouped rows. +- median - Finds the median of the values in the grouped rows. +- unique - Returns an array of unique values in the grouped rows. +- uniqueCount - Counts the number of unique values in the grouped rows. + +#### Custom Aggregations + +You can define custom aggregation functions in the `aggregationFns` slot on `tableFeatures`. The slot is a record where the keys are the names of the aggregation functions, and the values are the aggregation functions themselves. You can then reference these aggregation functions by name in a column's `aggregationFn` option. + +```ts +const features = tableFeatures({ + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), + aggregationFns: { + ...aggregationFns, + myCustomAggregation: (columnId, leafRows, childRows) => { + // return the aggregated value + }, + }, +}) + +const table = createTable({ + features, + // other options... +}) +``` + +In the above example, myCustomAggregation is a custom aggregation function that takes the column ID, the leaf rows, and the child rows, and returns the aggregated value. You can then use this aggregation function in a column's aggregationFn option: + +```ts +const column = columnHelper.accessor('key', { + aggregationFn: 'myCustomAggregation', +}) +``` + +> **TypeScript Note:** For `aggregationFn: 'myCustomAggregation'` string references to typecheck, register the function in the `aggregationFns` slot on `tableFeatures` (as shown above). The slot is the registry; no `declare module` augmentation is needed. Alternatively, skip the registry entirely by passing the function directly to the `aggregationFn` column option. + +### Manual Grouping + +If you are doing server-side grouping and aggregation, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to manually group the rows before passing them to the table. + +```ts +const features = tableFeatures({ columnGroupingFeature }) + +const table = createTable({ + features, + // other options... + manualGrouping: true, +}) +``` + +> **Note:** There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Controlled Grouping State + +If you need access to the grouping state in other parts of your application, you can own the `grouping` state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The atom can be read, written, or subscribed to anywhere in your app (such as in a query key for server-side grouping) without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' +import type { GroupingState } from '@tanstack/alpine-table' + +const groupingAtom = createAtom([]) + +// subscribe to the atom wherever you need the value +groupingAtom.subscribe(() => { + // react to grouping changes +}) + +const table = createTable({ + features, + // other options... + atoms: { + grouping: groupingAtom, // grouping APIs now update groupingAtom + }, +}) +``` + +Alternatively, the v8-style `state.grouping` plus `onGroupingChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ grouping: [] as GroupingState }) + +const table = createTable({ + features, + // other options... + state: { + get grouping() { + return local.grouping // connect the reactive slice back down to the table + }, + }, + onGroupingChange: (updater) => { + local.grouping = + typeof updater === 'function' ? updater(local.grouping) : updater + }, +}) +``` + +You can read the current grouping value with `table.atoms.grouping.get()`. Inside an Alpine binding this is a reactive read; in event handlers it simply returns the current value. + +### Wiring up the grouping UI + +Because Alpine does not initialize directives inside content set with `x-html`, the in-cell grouping controls (the header group toggle button and the grouped-cell expander) cannot live inside an `x-html` span. Render the header and cell content with `x-html="FlexRender(...)"`, but special-case the interactive parts directly in the markup using the cell helpers (`cell.getIsGrouped()`, `cell.getIsAggregated()`, `cell.getIsPlaceholder()`). + +The group toggle button lives in the header. Call the handler returned by `getToggleGroupingHandler` with the event. + +```html + + + +``` + +In the cell, choose between grouped, aggregated, placeholder, and normal rendering. The grouped cell also carries the expander button (which calls the row's `getToggleExpandedHandler`). It helps to expose a small helper on your `Alpine.data` object for the cell background. + +```ts +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(10_000) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + }) + + return { + table, + FlexRender, + cellBackground(cell) { + if (cell.getIsGrouped()) return '#0aff0082' + if (cell.getIsAggregated()) return '#ffa50078' + if (cell.getIsPlaceholder()) return '#ff000042' + return 'white' + }, + } +}) +``` + +```html + + + + + + + + + + +``` + +### Grouping APIs + +Columns expose grouping APIs for toggling grouping and building grouping UI: + +```ts +column.toggleGrouping() +column.getToggleGroupingHandler() +column.getCanGroup() +column.getIsGrouped() +column.getGroupedIndex() +column.getAutoAggregationFn() +column.getAggregationFn() +``` + +Rows expose grouping helpers for grouped row rendering: + +```ts +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose helpers for choosing between grouped, aggregated, placeholder, and normal cell rendering: + +```ts +cell.getIsGrouped() +cell.getIsAggregated() +cell.getIsPlaceholder() +``` + +The table instance exposes grouped and pre-grouped row models: + +```ts +table.getGroupedRowModel() +table.getPreGroupedRowModel() +``` + +Use `table.setGrouping` and `table.resetGrouping` to update the grouping state directly. diff --git a/docs/framework/alpine/guide/pagination.md b/docs/framework/alpine/guide/pagination.md new file mode 100644 index 0000000000..9a2d4c3a44 --- /dev/null +++ b/docs/framework/alpine/guide/pagination.md @@ -0,0 +1,295 @@ +--- +title: Pagination (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Pagination](../examples/pagination) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Pagination Setup + +Here's how you set up your table to use pagination features. Adding the pagination feature enables the related APIs. Additionally, if using client-side pagination, you also need to set up `paginatedRowModel` after its associated feature because row model slots are type-checked. + +```ts +import { + createPaginatedRowModel, + createTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), // if using client-side pagination +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Pagination (Alpine) Guide + +TanStack Table has great support for both client-side and server-side pagination. This guide will walk you through the different ways to implement pagination in your table. + +### Client-Side Pagination + +Using client-side pagination means that the `data` that you fetch will contain ***ALL*** of the rows for the table, and the table instance will handle pagination logic in the front-end. + +#### Should You Use Client-Side Pagination? + +Client-side pagination is usually the simplest way to implement pagination when using TanStack Table, but it might not be practical for very large datasets. + +However, a lot of people underestimate just how much data can be handled client-side. If your table will only ever have a few thousand rows or less, client-side pagination can still be a viable option. TanStack Table is designed to scale up to 10s of thousands of rows with decent performance for pagination, filtering, sorting, and grouping. The [official pagination example](../examples/pagination) loads 1,000 rows by default and includes a 100,000 row stress-test button that still performs well, albeit with only a handful of columns. + +Every use-case is different and will depend on the complexity of the table, how many columns you have, how large every piece of data is, etc. The main bottlenecks to pay attention to are: + +1. Can your server query all of the data in a reasonable amount of time (and cost)? +2. What is the total size of the fetch? (This might not scale as badly as you think if you don't have many columns.) +3. Is the client's browser using too much memory if all of the data is loaded at once? + +If you're not sure, you can always start with client-side pagination and then switch to server-side pagination in the future as your data grows. + +#### Pagination Row Model + +If you want to take advantage of the built-in client-side pagination in TanStack Table, add the `rowPaginationFeature` and the `paginatedRowModel` factory to your features: + +```ts +import { + createPaginatedRowModel, + createTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. + +```ts +import { + createTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> **Note**: Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +In Alpine, the table's state atoms are reactive. `table.atoms.pagination.get()` is a reactive read when used inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. + +If you need access to the `pagination` state outside of the table (a server-side query key is the most common case), you can own the slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The pagination value can be used in a query key without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' +import { + createTable, + rowPaginationFeature, + tableFeatures, + type PaginationState, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ rowPaginationFeature }) + +const paginationAtom = createAtom({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +// subscribe to the atom wherever you need the value (e.g. for a query key) +paginationAtom.subscribe(() => { + // react to pagination changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + pagination: paginationAtom, // table pagination APIs now update paginationAtom + }, +}) +``` + +Alternatively, the v8-style `state.pagination` plus `onPaginationChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + pagination: { + pageIndex: 0, // initial page index + pageSize: 10, // default page size + } as PaginationState, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get pagination() { + return local.pagination // connect the reactive slice back down to the table + }, + }, + onPaginationChange: (updater) => { + local.pagination = + typeof updater === 'function' ? updater(local.pagination) : updater + }, +}) +``` + +Alternatively, if you have no need for managing the `pagination` state in your own scope, but you need to set different initial values for the `pageIndex` and `pageSize`, you can use the `initialState` option. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> **Note**: Do NOT provide the `pagination` slice in more than one of the `atoms`, `state`, and `initialState` options. Controlled values (`atoms` or `state`) will overwrite `initialState`. Only use one of them. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` whenever the client-side row models recompute, such as when the `data` is updated, filters change, sorting changes, or grouping changes. This behavior is automatically disabled when `manualPagination` is `true`, but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. There is also a global `autoResetAll` table option that disables (or enables) every auto-reset behavior at once. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + autoResetPageIndex: false, // turn off auto reset of pageIndex + // autoResetAll: false, // or turn off all auto resets at once +}) +``` + +A common reason to set `autoResetPageIndex: false` is editing data while viewing the table (for example, inline cell editing). Every edit updates `data`, which recomputes the row models and would otherwise snap the user back to the first page. Setting the option to a static `false` keeps the current page when the row model recomputes. If you also use the expanding feature, pair it with `autoResetExpanded: false` so expanded rows do not collapse on edits. + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `previousPage`: Useful for going to the previous page. (Button click handler) +- `nextPage`: Useful for going to the next page. (Button click handler) +- `firstPage`: Useful for going to the first page. (Button click handler) +- `lastPage`: Useful for going to the last page. (Button click handler) +- `setPageIndex`: Useful for a "go to page" input. +- `resetPageIndex`: Useful for resetting the table state to the original page index. +- `setPageSize`: Useful for a "page size" input/select. +- `resetPageSize`: Useful for resetting the table state to the original page size. +- `setPagination`: Useful for setting all of the pagination state at once. +- `resetPagination`: Useful for resetting the table state to the original pagination state. + +> **Note**: These pagination APIs are available when using `rowPaginationFeature`. + +Pagination controls live on real elements so the click handlers and `:disabled` bindings stay interactive. Read the page index and page size with `table.atoms.pagination.get()`. + +```html + + + + + + Page + + + of + + + + +``` + +#### Pagination Info APIs + +- `getPageCount`: Useful for showing the total number of pages. +- `getRowCount`: Useful for showing the total number of rows. diff --git a/docs/framework/alpine/guide/row-pinning.md b/docs/framework/alpine/guide/row-pinning.md new file mode 100644 index 0000000000..e9edfa26fe --- /dev/null +++ b/docs/framework/alpine/guide/row-pinning.md @@ -0,0 +1,290 @@ +--- +title: Row Pinning (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Row Pinning](../examples/row-pinning) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Row Pinning Setup + +Here's how you set up your table to use row pinning features. Adding the row pinning feature enables the related APIs. + +```ts +import { createTable, tableFeatures, rowPinningFeature } from '@tanstack/alpine-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Row Pinning (Alpine) Guide + +Row pinning lets you keep selected rows in top or bottom row regions while the rest of the rows render in the center region. + +There are 2 table features that can reorder rows, which happen in the following order: + +1. **Row Pinning** - If pinning, rows are split into top, center (unpinned), and bottom pinned rows. +2. [Sorting](./sorting) + +### Enable Row Pinning + +To use row pinning, add `rowPinningFeature` to your features. Row pinning does not require a row model factory. + +```ts +import { + rowPinningFeature, + tableFeatures, + createTable, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Row Pinning State + +The `rowPinning` state stores row IDs in `top` and `bottom` arrays: + +```ts +type RowPinningState = { + top: string[] + bottom: string[] +} +``` + +You can pin rows by default with `initialState.rowPinning`: + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +}) +``` + +If you need to manage row pinning outside of the table instance, the recommended v9 approach is an external atom passed to the table's `atoms` option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without going through the component that owns the table. + +```ts +import { createAtom } from '@tanstack/store' +import type { RowPinningState } from '@tanstack/alpine-table' + +const rowPinningAtom = createAtom({ + top: [], + bottom: [], +}) + +// subscribe to the atom wherever you need the value +rowPinningAtom.subscribe(() => { + // react to pinning changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + rowPinning: rowPinningAtom, + }, +}) +``` + +Alternatively, the v8-style `state.rowPinning` plus `onRowPinningChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ + rowPinning: { top: [], bottom: [] } as RowPinningState, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get rowPinning() { + return local.rowPinning // connect the reactive slice back down to the table + }, + }, + onRowPinningChange: (updater) => { + local.rowPinning = + typeof updater === 'function' ? updater(local.rowPinning) : updater + }, +}) +``` + +Use `table.setRowPinning` to update the state directly, and `table.resetRowPinning` to reset it to `initialState.rowPinning`. Pass `true` to `resetRowPinning` to clear both pinned row arrays. + +```ts +table.setRowPinning({ + top: ['0', '2'], + bottom: ['8'], +}) + +table.resetRowPinning() +table.resetRowPinning(true) +``` + +You can read the current pinning state with `table.atoms.rowPinning.get()`, which is a reactive read when used inside an Alpine binding and a plain read elsewhere. + +### Pin Rows With Row APIs + +Each row exposes APIs for checking whether it can be pinned, reading its pinned position, and changing its pinned position. + +```ts +row.getCanPin() +row.getIsPinned() // 'top', 'bottom', or false +row.getPinnedIndex() + +row.pin('top') +row.pin('bottom') +row.pin(false) +``` + +You can use these APIs to build pinning controls. Because Alpine does not initialize directives inside content set with `x-html`, render any pin buttons on real elements rather than inside a cell renderer. Define a `pin` column that exposes a plain value, then special-case it in your template by column id: + +```ts +const columns = [ + { + id: 'pin', + header: () => 'Pin', + cell: () => '', // buttons are rendered on real elements in the template + }, + //... +] +``` + +```html + + + + +``` + +The `row.pin` API also accepts `includeLeafRows` and `includeParentRows` flags. These can be useful when pinning grouped or expanded rows and deciding whether related parent or leaf rows should move with the row. + +### Row Pinning Table APIs + +Row pinning splits the current row model into 3 row lists: + +```ts +table.getTopRows() +table.getCenterRows() +table.getBottomRows() +``` + +If you render pinned rows in separate table sections, use those APIs directly with `x-for`: + +```html + + + + + +``` + +Use `table.getIsSomeRowsPinned()` to check whether any rows are pinned, or pass a position to check a specific pinned region. + +```ts +table.getIsSomeRowsPinned() +table.getIsSomeRowsPinned('top') +table.getIsSomeRowsPinned('bottom') +``` + +### Disable Row Pinning + +By default, all rows can be pinned. You can disable row pinning for the whole table or decide per row with `enableRowPinning`. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableRowPinning: (row) => row.original.status !== 'archived', +}) +``` + +### Keep Pinned Rows + +By default, `keepPinnedRows` is `true`, so pinned rows stay visible in their pinned region even when they would otherwise be filtered or paginated out of the center rows. + +Set `keepPinnedRows` to `false` if pinned rows should only render when they are present in the current filtered and paginated row model. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + keepPinnedRows: false, +}) +``` diff --git a/docs/framework/alpine/guide/row-selection.md b/docs/framework/alpine/guide/row-selection.md new file mode 100644 index 0000000000..0d08e409b2 --- /dev/null +++ b/docs/framework/alpine/guide/row-selection.md @@ -0,0 +1,251 @@ +--- +title: Row Selection (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Row Selection](../examples/row-selection) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Row Selection Setup + +Here's how you set up your table to use row selection features. Adding the row selection feature enables the related APIs. + +```ts +import { createTable, tableFeatures, rowSelectionFeature } from '@tanstack/alpine-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Row Selection (Alpine) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you. You can access the row selection state or the selected rows from a few APIs. + +- `table.atoms.rowSelection.get()` - returns the row selection state (reactive when read inside an Alpine binding) +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.atoms.rowSelection.get()) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +In Alpine, the table's state atoms are reactive. `table.atoms.rowSelection.get()` is a reactive read when called inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. + +> Note: If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +If you need easy access to the selected row ids in other parts of your application (for example, to make API calls with them), you can own the row selection state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. Atoms preserve fine-grained subscriptions, and the selection value can be read anywhere in your app without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' +import { createTable, tableFeatures, rowSelectionFeature, type RowSelectionState } from '@tanstack/alpine-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const rowSelectionAtom = createAtom({}) + +// subscribe to the atom wherever you need the value +rowSelectionAtom.subscribe(() => { + // react to selection changes +}) + +const table = createTable({ + features, + //... + atoms: { + rowSelection: rowSelectionAtom, // selection APIs now update rowSelectionAtom + }, +}) +``` + +Alternatively, the v8-style `state.rowSelection` plus `onRowSelectionChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ rowSelection: {} as RowSelectionState }) + +const table = createTable({ + features, + //... + state: { + get rowSelection() { + return local.rowSelection // connect the reactive slice back down to the table + }, + }, + onRowSelectionChange: (updater) => { + local.rowSelection = + typeof updater === 'function' ? updater(local.rowSelection) : updater + }, +}) +``` + +### Useful Row Ids + +By default, the row id for each row is simply the `row.index`. If you are using row selection features, you most likely want to use a more useful row identifier, since the row selection state is keyed by row id. You can use the `getRowId` table option to specify a function that returns a unique row id for each row. + +```ts +const table = createTable({ + features, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +Now as rows are selected, the row selection state will look something like this: + +```json +{ + "13e79140-62a8-4f9c-b087-5da737903b76": true, + "f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e": false + //... +} +``` + +instead of this: + +```json +{ + "0": true, + "1": false + //... +} +``` + +### Enable Row Selection Conditionally + +Row selection is enabled by default for all rows. To either enable row selection conditionally for certain rows or disable row selection for all rows, you can use the `enableRowSelection` table option which accepts either a boolean or a function for more granular control. + +```ts +const table = createTable({ + //... + enableRowSelection: (row) => row.original.age > 18, //only enable row selection for adults +}) +``` + +To enforce whether a row is selectable or not in your UI, you can use the `row.getCanSelect()` API for your checkboxes or other selection UI. + +### Single Row Selection + +By default, the table allows multiple rows to be selected at once. If, however, you only want to allow a single row to be selected at once, you can set the `enableMultiRowSelection` table option to `false` to disable multi-row selection, or pass in a function to disable multi-row selection conditionally for a row's sub-rows. + +This is useful for making tables that have radio buttons instead of checkboxes. + +```ts +const table = createTable({ + //... + enableMultiRowSelection: false, //only allow a single row to be selected at once + // enableMultiRowSelection: row => row.original.age > 18, //only allow a single row to be selected at once for adults +}) +``` + +### Sub-Row Selection + +By default, selecting a parent row will select all of its sub-rows. If you want to disable auto sub-row selection, you can set the `enableSubRowSelection` table option to `false` to disable sub-row selection, or pass in a function to disable sub-row selection conditionally for a row's sub-rows. + +```ts +const table = createTable({ + //... + enableSubRowSelection: false, //disable sub-row selection + // enableSubRowSelection: row => row.original.age > 18, //disable sub-row selection for adults +}) +``` + +### Render Row Selection UI + +TanStack Table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These functions automatically call other internal APIs to update the row selection state and re-render the table. + +Use the `row.getToggleSelectedHandler()` API to connect to your checkbox inputs to toggle the selection of a row. + +Use the `table.getToggleAllRowsSelectedHandler()` or `table.getToggleAllPageRowsSelectedHandler` APIs to connect to your "select all" checkbox input to toggle the selection of all rows. + +If you need more granular control over these function handlers, you can always just use the `row.toggleSelected()` or `table.toggleAllRowsSelected()` APIs directly. Or you can even just call the `table.setRowSelection()` API to directly set the row selection state just as you would with any other state updater. These handler functions are just a convenience. + +Because checkboxes are interactive and Alpine cannot process directives inside content set with `x-html`, they cannot live in a cell or header renderer. Define a `select` column that exposes a plain value, then render the real `` elements in your template, special-cased by column id. Bind the indeterminate state with `x-effect`, since it is a DOM property that cannot be set with a normal attribute binding. + +```ts +const columns = [ + { + id: 'select', + header: () => '', // checkboxes are rendered on real elements in the template + cell: () => '', + }, + //... more column definitions... +] +``` + +```html + + + + +``` + +```html + + + + +``` + +#### Connect Row Selection APIs to UI + +If you want a simpler row selection UI, you can just hook up click events to the row itself. The `row.getToggleSelectedHandler()` API is also useful for this use case. Attach it to a real element such as the ``. + +```html + + + +``` diff --git a/docs/framework/alpine/guide/sorting.md b/docs/framework/alpine/guide/sorting.md new file mode 100644 index 0000000000..e15aefebdb --- /dev/null +++ b/docs/framework/alpine/guide/sorting.md @@ -0,0 +1,548 @@ +--- +title: Sorting (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Alpine examples: + +- [Sorting](../examples/sorting) + +Read your reactive inputs such as `data` through a getter (for example backing them with `Alpine.reactive`) when creating the table, so the table sees updates. + +### Sorting Setup + +Here's how you set up your table to use sorting features. Adding the sorting feature enables the related APIs. Additionally, if using client-side sorting, you also need to set up `sortedRowModel` after its associated feature because row model slots are type-checked. + +```ts +import { + createSortedRowModel, + createTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), // if using client-side sorting + sortFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +## Sorting (Alpine) Guide + +TanStack Table provides solutions for just about any sorting use-case you might have. This guide will walk you through the various options that you can use to customize the built-in client-side sorting functionality, as well as how to opt out of client-side sorting in favor of manual server-side sorting. + +### Sorting State + +The sorting state is defined as an array of objects with the following shape: + +```ts +type ColumnSort = { + id: string + desc: boolean +} +type SortingState = ColumnSort[] +``` + +Since the sorting state is an array, it is possible to sort by multiple columns at once. Read more about the multi-sorting customizations down [below](#multi-sorting). + +#### Accessing Sorting State + +The table's state atoms are reactive in Alpine. `table.atoms.sorting.get()` is a reactive read when used inside an Alpine binding (`x-text`, `x-html`, `:value`, `x-if`, `x-for`, `x-effect`, or a getter/method on your `Alpine.data` object); in event handlers and other untracked code, the same call simply returns the current value. `table.store.get()` returns a current full-state snapshot, useful for debugging. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) + +table.atoms.sorting.get() // reactive read inside Alpine bindings, plain read elsewhere +``` + +However, if you need access to the sorting state outside of the table, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state in other parts of your application, you can own the sorting state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. The atom can be read, written, or subscribed to elsewhere (such as in a query key for server-side sorting) without making the table depend on component-local state. + +```ts +import { createAtom } from '@tanstack/store' + +const sortingAtom = createAtom([]) // can set initial sorting state here + +// subscribe to the atom wherever you need the value (e.g. for a query key) +sortingAtom.subscribe(() => { + // react to sorting changes +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + sorting: sortingAtom, // table sorting APIs now update sortingAtom + }, +}) +``` + +Alternatively, the v8-style `state.sorting` plus `onSortingChange` pattern is still supported by owning the slice in `Alpine.reactive`. It can be convenient for simple integrations or when migrating v8 code. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const local = Alpine.reactive({ sorting: [] as SortingState }) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + state: { + get sorting() { + return local.sorting // connect the reactive slice back down to the table + }, + }, + onSortingChange: (updater) => { + local.sorting = + typeof updater === 'function' ? updater(local.sorting) : updater + }, +}) +``` + +#### Initial Sorting State + +If you do not need to control the sorting state in your own state management or scope, but you still want to set an initial sorting state, you can use the `initialState` table option instead of `state`. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + initialState: { + sorting: [ + { + id: 'name', + desc: true, // sort by name in descending order by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.sorting` and `state.sorting` at the same time, as the controlled `state.sorting` value will override the `initialState.sorting`. + +### Client-Side vs Server-Side Sorting + +Whether or not you should use client-side or server-side sorting depends entirely on whether you are also using client-side or server-side pagination or filtering. Be consistent, because using client-side sorting with server-side pagination or filtering will only sort the data that is currently loaded, and not the entire dataset. + +### Manual Server-Side Sorting + +If you plan to just use your own server-side sorting in your back-end logic, you do not need to provide a sorted row model. But if you have provided a sorting row model, but you want to disable it, you can use the `manualSorting` table option. + +```ts +import { createAtom } from '@tanstack/store' + +const features = tableFeatures({ rowSortingFeature }) // feature needed for sorting state/APIs + +const sortingAtom = createAtom([]) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + manualSorting: true, // use pre-sorted row model instead of sorted row model + atoms: { + sorting: sortingAtom, + }, +}) +``` + +Hoisting the sorting state into your own scope (with an external atom or the `state.sorting` plus `onSortingChange` pattern) is covered in the [Controlled Sorting State](#controlled-sorting-state) section above. + +> **NOTE**: When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` and the `sortedRowModel` factory to your features. Import `createSortedRowModel` and `sortFns` from TanStack Table: + +```ts +import { + createSortedRowModel, + createTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions, either inline as the `sortFn` column option, or by name in the sorting function registry that you pass to `createSortedRowModel`. + +#### Custom Sorting Functions + +Whether you register a custom sorting function in the registry passed to `createSortedRowModel` or pass it directly as a `sortFn` column option, it should have the following signature: + +```ts +// optionally use the SortFn to infer the parameter types +const myCustomSortFn: SortFn = ( + rowA: Row, + rowB: Row, + columnId: string, +) => { + return // -1, 0, or 1 - access any row data using rowA.original and rowB.original +} +``` + +> Note: The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take care of that logic. `sortFn` functions only need to provide a consistent comparison. + +Every sorting function receives 2 rows and a column ID and are expected to compare the two rows using the column ID to return `-1`, `0`, or `1` in ascending order. Here's a cheat sheet: + +| Return | Ascending Order | +| ------ | --------------- | +| `-1` | `a < b` | +| `0` | `a === b` | +| `1` | `a > b` | + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortFn: 'alphanumeric', // use built-in sorting function by name + }, + { + header: () => 'Age', + accessorKey: 'age', + sortFn: 'myCustomSortFn', // reference a custom sorting function registered with createSortedRowModel + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + }, +] +//... +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { + ...sortFns, + myCustomSortFn: (rowA, rowB, columnId) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0, + }, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + +> **TypeScript Note:** For `sortFn: 'myCustomSortFn'` string references to typecheck, register the function in the `sortFns` slot on `tableFeatures` (as shown above). The slot is the registry; no `declare module` augmentation is needed. Alternatively, skip the registry entirely by passing the function directly to the `sortFn` column option. + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```ts +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```ts +const columns = [ + { + header: () => 'Name', + accessorKey: 'name', + sortDescFirst: true, // sort by name in descending order first (default is ascending for string columns) + }, + { + header: () => 'Age', + accessorKey: 'age', + sortDescFirst: false, // sort by age in ascending order first (default is descending for number columns) + }, + //... +] +//... +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + sortDescFirst: true, // sort by all columns in descending order first (default is ascending for string columns and descending for number columns) +}) +``` + +> **NOTE**: You may want to explicitly set the `sortDescFirst` column option on any columns that have nullable values. The table may not be able to properly determine if a column is a number or a string if it contains nullable values. + +#### Invert Sorting + +Inverting sorting is not the same as changing the default sorting direction. If `invertSorting` column option is `true` for a column, then the "desc/asc" sorting states will still cycle like normal, but the actual sorting of the rows will be inverted. This is useful for values that have an inverted best/worst scale where lower numbers are better, eg. a ranking (1st, 2nd, 3rd) or golf-like scoring. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied + }, + //... +] +``` + +#### Sort Undefined Values + +Any undefined values will be sorted to the beginning or end of the list based on the `sortUndefined` column option or table option. You can customize this behavior for your specific use-case. + +If not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be considered tied and need to be sorted by the next column filter or original index (whichever applies) +- `-1` - Undefined values will be sorted with higher priority (ascending) (if ascending, undefined will appear on the beginning of the list) +- `1` - Undefined values will be sorted with lower priority (descending) (if ascending, undefined will appear on the end of the list) + +> NOTE: `'first'` and `'last'` options are available in v9. + +```ts +const columns = [ + { + header: () => 'Rank', + accessorKey: 'rank', + sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false + }, +] +``` + +#### Sorting Removal + +By default, the ability to remove sorting while cycling through the sorting states for a column is enabled. You can disable this behavior using the `enableSortingRemoval` table option. This behavior is useful if you want to ensure that at least one column is always sorted. + +The default behavior when using either the `getToggleSortingHandler` or `toggleSorting` APIs is to cycle through the sorting states like this (the first direction depends on the column's data type and the `sortDescFirst` option, as discussed [above](#sorting-direction); a string column is shown here): + +`'none' -> 'asc' -> 'desc' -> 'none' -> 'asc' -> 'desc' -> ...` + +If you disable sorting removal, the `'none'` state is skipped after the first sort: + +`'none' -> 'asc' -> 'desc' -> 'asc' -> 'desc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (sorting can never return to 'none' once applied) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```ts +const columns = [ + { + header: () => 'Created At', + accessorKey: 'createdAt', + enableMultiSort: false, // always sort by just this column if sorting by this column + }, + //... +] +//... +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableMultiSort: false, // disable multi-sorting for the entire table +}) +``` + +##### Customize Multi-Sorting Trigger + +By default, the `Shift` key is used to trigger multi-sorting. You can change this behavior with the `isMultiSortEvent` table option. You can even specify that all sorting events should trigger multi-sorting by returning `true` from the custom function. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + isMultiSortEvent: (e) => true, // normal click triggers multi-sorting + //or + isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting +}) +``` + +##### Multi-Sorting Limit + +By default, there is no limit to the number of columns that can be sorted at once. You can set a limit using the `maxMultiSortColCount` table option. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once +}) +``` + +##### Multi-Sorting Removal + +By default, the ability to remove multi-sorts is enabled. You can disable this behavior using the `enableMultiRemove` table option. + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableMultiRemove: false, // disable the ability to remove multi-sorts +}) +``` + +### Wiring up the sort UI + +Because Alpine does not initialize directives inside content set with `x-html`, render the header content with `x-html="FlexRender({ header })"` but attach the click handler to a real element around it. Call the handler returned by `getToggleSortingHandler` with the event. + +```html + + + +``` + +### Sorting APIs + +There are a lot of sorting related APIs that you can use to hook up to your UI or other logic. Here is a list of all of the sorting APIs and some of their use-cases. + +- `table.setSorting` - Set the sorting state directly. +- `table.resetSorting` - Reset the sorting state to the initial state or clear it. + +- `column.getCanSort` - Useful for enabling/disabling the sorting UI for a column. +- `column.getIsSorted` - Useful for showing a visual sorting indicator for a column. + +- `column.getToggleSortingHandler` - Useful for hooking up the sorting UI for a column. Add to a sort arrow (icon button), menu item, or simply the entire column header cell. This handler will call `column.toggleSorting` with the correct parameters. +- `column.toggleSorting` - Useful for hooking up the sorting UI for a column. If using instead of `column.getToggleSortingHandler`, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`) +- `column.clearSorting` - Useful for a "clear sorting" button or menu item for a specific column. + +- `column.getNextSortingOrder` - Useful for showing which direction the column will sort by next. (asc/desc/clear in a tooltip/menu item/aria-label or something) +- `column.getFirstSortDir` - Useful for showing which direction the column will sort by first. (asc/desc in a tooltip/menu item/aria-label or something) +- `column.getAutoSortDir` - Determines whether the first sorting direction will be ascending or descending for a column. +- `column.getAutoSortFn` - Used internally to find the default sorting function for a column if none is specified. +- `column.getSortFn` - Returns the exact sorting function being used for a column. + +- `column.getCanMultiSort` - Useful for enabling/disabling the multi-sorting UI for a column. +- `column.getSortIndex` - Useful for showing a badge or indicator of the column's sort order in a multi-sort scenario. i.e. whether or not it is the first, second, third, etc. column to be sorted. diff --git a/docs/framework/alpine/guide/table-state.md b/docs/framework/alpine/guide/table-state.md new file mode 100644 index 0000000000..d8a6834794 --- /dev/null +++ b/docs/framework/alpine/guide/table-state.md @@ -0,0 +1,330 @@ +--- +title: Table State (Alpine) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these examples: + +- [Basic createTable](../examples/basic-create-table) +- [Basic External Atoms](../examples/basic-external-atoms) +- [Basic External State](../examples/basic-external-state) + +## Table State (Alpine) Guide + +> **If you boil TanStack Table down to one sentence: TanStack Table is a large state-management coordinator for table states.** + +Understanding this guide is fundamental to understanding how TanStack Table works and how to interact with it for the best results. + +### Do you need to Manage External State? + +You usually do NOT need to manage table state yourself. If you pass nothing to `initialState`, `atoms`, `state`, or any of the `on[State]Change` table options, TanStack Table will manage its own state internally. + +There will be situations where you need to customize how you interact with the internal table state, or even hoist it up to your own scopes. TanStack Table lets you read, subscribe to, or own the state slices that matter to your app. This guide explains how table state works in Alpine, how to read it, and when to use external atoms or external state. + +### State in v9 + +TanStack Table v9 overhauled state management around TanStack Store. TanStack Store uses the `alien-signals` implementation and supports performant derived state. + +A table instance has a few state surfaces: + +- `table.baseAtoms` are the internal writable atoms created from the resolved initial state. +- `table.atoms` are readonly derived atoms exposed per registered state slice. +- `table.store` is a readonly flat TanStack Store derived by putting all of the registered `table.atoms` together. + +The Alpine adapter provides `alpineReactivity()` to the table's `coreReactivityFeature`, so the atoms are backed directly by TanStack Store. `createTable` then makes the instance reactive to Alpine: it returns the table wrapped in a proxy and subscribes to `table.store`. Because of this, any reactive Alpine binding that reads a table API re-runs when state changes, whether that read is in `x-text`, `x-html`, `x-for`, `x-if`, a bound attribute (`:value`), `x-effect`, or a getter/method on your `Alpine.data` object. (Event handlers like `@click` are not reactive; they simply read the current value whenever they fire.) You do not pass a state selector, and there is no `table.Subscribe`: reactivity is automatic per binding. When any registered slice changes, Alpine re-evaluates the bindings that read table APIs and patches only the DOM that actually changed. + +### Feature-based State + +State slices are only created for the features that are registered in `features`. This keeps TanStack Table tree-shakeable and gives TypeScript more accurate state inference. + +```ts +const features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) + +table.atoms.pagination.get() +table.atoms.sorting.get() + +// table.atoms.rowSelection // TypeScript error unless rowSelectionFeature is registered +``` + +If `features` does not include a feature, its state should not be available in `table.atoms`, `table.store.get()`, `initialState`, `state`, or `atoms`. + +### Accessing Table State + +There are two different questions when reading table state: + +- Do you only need the current value? +- Or should the markup update when that value changes? + +Use direct atom reads for slice values. Use `table.store.get()` for the current flat state snapshot. Because the adapter makes table reads reactive, both update your markup automatically when read inside an Alpine binding. + +#### Reading State + +The simplest and most performant way to read a current state value is to read the matching atom: + +```ts +const pagination = table.atoms.pagination.get() +const sorting = table.atoms.sorting.get() +``` + +You can also read the current flat store snapshot: + +```ts +const tableState = table.store.get() +const pagination = table.store.get().pagination +``` + +Prefer `table.atoms..get()` for narrow reads. Use `table.store.get()` for full-state debug output or when a binding intentionally depends on the whole table state. + +#### Reading State Reactively in Markup + +Because the table instance is reactive, you read state directly in your Alpine expressions. There is nothing extra to subscribe to: each binding tracks the table reads inside it and re-runs when they change. + +```html + + Page + + of + + + + +

+```
+
+For derived values, expose a getter or method on your `Alpine.data` object. These run inside Alpine's reactivity, so reading a table API from them stays reactive:
+
+```ts
+Alpine.data('table', () => {
+  const local = Alpine.reactive({ data: makeData(1_000) })
+
+  const table = createTable({
+    features,
+    columns,
+    get data() {
+      return local.data
+    },
+  })
+
+  return {
+    table,
+    FlexRender,
+    // derived value used from the template
+    get pageCount() {
+      return table.getPageCount()
+    },
+    sortIndicator(isSorted: false | 'asc' | 'desc') {
+      return { asc: ' 🔼', desc: ' 🔽' }[isSorted as string] ?? ''
+    },
+  }
+})
+```
+
+### Setting Table State
+
+You should almost never need to set table state directly. TanStack Table features expose dedicated APIs for interacting with their state, and those APIs are the safest way to make changes.
+
+```ts
+table.nextPage()
+table.previousPage()
+table.setPageIndex(0)
+table.setPageSize(25)
+```
+
+Use APIs like `table.setSorting(...)`, `table.setColumnFilters(...)`, `column.toggleVisibility()`, or `row.toggleSelected()` instead of manually editing the underlying state object.
+
+If you only care about setting starting values, use `initialState`. If you want to reset a state slice back to its initial value, use that feature's reset API.
+
+If you really do need to write a state slice directly, the low-level write surface for internally owned state is the matching base atom:
+
+```ts
+table.baseAtoms.pagination.set((old) => ({
+  ...old,
+  pageIndex: 0,
+}))
+```
+
+Direct base atom writes should be rare. If a slice is owned by an external atom passed through `atoms`, write to that external atom instead; `table.atoms.pagination` will read from the external atom, not the internal base atom.
+
+### Custom Initial State
+
+If you only need to customize the starting value for some table state, use `initialState`. You still do not need to manage that state yourself.
+
+`initialState` only applies to registered state slices. It is used to create the table's initial state and is also used by reset APIs such as `table.resetSorting()` or `table.resetPagination()`. Changing the `initialState` object later does not reset table state.
+
+```ts
+const table = createTable({
+  features,
+  columns,
+  get data() {
+    return local.data
+  },
+  initialState: {
+    sorting: [
+      {
+        id: 'age',
+        desc: true,
+      },
+    ],
+    pagination: {
+      pageIndex: 0,
+      pageSize: 25,
+    },
+  },
+})
+```
+
+> **Note:** Do not provide the same state slice in multiple ownership places unless you intentionally want one to win. For a slice like `pagination`, prefer exactly one of `initialState.pagination`, `atoms.pagination`, or `state.pagination` as the source of truth. The precedence is `atoms[key]` > `state[key]` > internal `baseAtoms[key]`: external atoms take precedence over external `state`, and external `state` syncs into the table's internal base atom.
+
+#### Resetting to Initial State
+
+Feature reset APIs reset to `table.initialState` by default. Many reset APIs also accept `true` to reset to that feature's blank/default state instead:
+
+```ts
+table.resetSorting()
+table.resetPagination()
+table.resetPagination(true)
+```
+
+Slice reset APIs like `resetPagination()` update through that feature's state updater and can update an externally owned atom. The core `table.reset()` API resets the internal base atoms, so do not use it as the primary way to reset state that is owned by external atoms.
+
+### Controlled State
+
+If you need easy access to table state in other parts of your application, you can control individual state slices. In Alpine, you have two options: own the slice in an external TanStack Store atom (good for sharing across modules or subscribing outside the table), or own it in `Alpine.reactive` state and connect it with `state` plus `on[State]Change`.
+
+#### External Atoms
+
+Use external atoms when the app should own one or more table state slices as TanStack Store atoms. `@tanstack/store` is already a dependency of `@tanstack/alpine-table`, so `createAtom` is available. Create stable writable atoms, pass them to the `atoms` option, and read, write, or subscribe to them from anywhere.
+
+```ts
+import { createAtom } from '@tanstack/store'
+import {
+  createTable,
+  rowPaginationFeature,
+  tableFeatures,
+  type PaginationState,
+} from '@tanstack/alpine-table'
+
+const features = tableFeatures({
+  rowPaginationFeature,
+})
+
+// Create stable external atoms at module scope (or in a shared store module)
+const paginationAtom = createAtom({
+  pageIndex: 0,
+  pageSize: 10,
+})
+
+Alpine.data('table', () => {
+  const local = Alpine.reactive({ data: makeData(1_000) })
+
+  const table = createTable({
+    features,
+    columns,
+    get data() {
+      return local.data
+    },
+    atoms: {
+      pagination: paginationAtom,
+    },
+  })
+
+  return { table, FlexRender }
+})
+```
+
+Reads and writes for `pagination` are now routed through `paginationAtom` instead of the internal base atom. Atom changes flow through the derived `table.store`, which the adapter subscribes to, so the template re-renders. You can also subscribe to the atom directly from anywhere with `paginationAtom.subscribe(...)`. When using the `atoms` option for a slice, you do not need to add the matching `on[State]Change` option.
+
+#### External State
+
+Use `state` plus `on[State]Change` when an `Alpine.reactive` object should own a table state slice. Read the controlled slices through getters inside `state`: that is what lets the adapter re-apply options when they change.
+
+```ts
+const local = Alpine.reactive({
+  data: makeData(1_000),
+  sorting: [] as SortingState,
+  pagination: { pageIndex: 0, pageSize: 10 },
+})
+
+const table = createTable({
+  features,
+  columns,
+  get data() {
+    return local.data
+  },
+  // connect our external state back down to the table via getters
+  state: {
+    get sorting() {
+      return local.sorting
+    },
+    get pagination() {
+      return local.pagination
+    },
+  },
+  onSortingChange: (updater) => {
+    // raise sorting state changes to our own state management
+    local.sorting =
+      typeof updater === 'function' ? updater(local.sorting) : updater
+  },
+  onPaginationChange: (updater) => {
+    // raise pagination state changes to our own state management
+    local.pagination =
+      typeof updater === 'function' ? updater(local.pagination) : updater
+  },
+})
+```
+
+Use the per-slice `on[State]Change` callbacks to keep controlled table state slices atomic and separated.
+
+##### On State Change Callbacks
+
+The `on[State]Change` callbacks are useful when you are controlling a matching slice through the `state` option. They work like setters: an updater can be a raw value or a function that receives the previous value and returns the next value.
+
+If you provide an `on[State]Change` callback, also provide the corresponding value in `state`. For example, `onSortingChange` should be paired with `state.sorting`.
+
+```ts
+onPaginationChange: (updater) => {
+  local.pagination =
+    updater instanceof Function ? updater(local.pagination) : updater
+
+  // side effects or validation can happen here
+}
+```
+
+### State Types
+
+Most complex states in TanStack Table have their own TypeScript types that you can import and use.
+
+```ts
+import {
+  createTable,
+  type PaginationState,
+  type RowSelectionState,
+  type SortingState,
+  type TableState,
+} from '@tanstack/alpine-table'
+
+const local = Alpine.reactive({
+  sorting: [{ id: 'age', desc: true }] as SortingState,
+})
+```
+
+`TableState` is inferred from the features registered on that table:
+
+```ts
+type MyTableState = TableState
+```
diff --git a/docs/framework/alpine/quick-start.md b/docs/framework/alpine/quick-start.md
new file mode 100644
index 0000000000..1cc3d7b456
--- /dev/null
+++ b/docs/framework/alpine/quick-start.md
@@ -0,0 +1,210 @@
+---
+title: Quick Start
+---
+
+TanStack Table is a headless table library. It manages your table's state and logic (sorting, filtering, pagination, selection, and more) while you keep 100% control over the markup and styles. This page gets you from install to a rendering Alpine table, then shows how to layer on your first feature.
+
+## Installation
+
+TanStack Table v9 is currently published under the `beta` tag:
+
+```bash
+npm install @tanstack/alpine-table@beta alpinejs
+```
+
+The `@tanstack/alpine-table` package works with Alpine 3.
+
+## How the Alpine adapter works
+
+The adapter is built around two ideas:
+
+- **`createTable` returns a reactive table instance.** State lives in [TanStack Store](https://tanstack.com/store/latest) atoms that the adapter bridges into Alpine's reactivity. Any Alpine binding that reads a table API (`table.getRowModel()`, `table.atoms.sorting.get()`, and so on) re-runs automatically when the underlying state changes. There is no state selector to pass.
+- **You render with `x-html` and `table.FlexRender`.** A column's `cell`/`header`/`footer` renderer returns a string of HTML. `table.FlexRender({ cell })` produces that string and you place it with `x-html`.
+
+> [!IMPORTANT]
+> Alpine does not initialize directives (`@click`, `x-model`, etc.) inside content set with `x-html`. So render cell/header **content** with `x-html="table.FlexRender(...)"`, but put any **interactivity** (click-to-sort, filter inputs, checkboxes) on real elements in your markup, next to the `x-html` span. You will see this pattern in the sorting example below.
+
+## Your First Table
+
+You define the table in a JavaScript module with [`Alpine.data`](https://alpinejs.dev/globals/alpine-data), then render it from your HTML.
+
+```ts
+// main.ts
+import Alpine from 'alpinejs'
+import { FlexRender, createTable, tableFeatures } from '@tanstack/alpine-table'
+import type { ColumnDef } from '@tanstack/alpine-table'
+
+// 1. Define the shape of your data
+type Person = {
+  firstName: string
+  lastName: string
+  age: number
+}
+
+const defaultData: Array = [
+  { firstName: 'tanner', lastName: 'linsley', age: 24 },
+  { firstName: 'tandy', lastName: 'miller', age: 40 },
+  { firstName: 'joe', lastName: 'dirte', age: 45 },
+]
+
+// 2. New in v9: declare which features this table uses (none yet)
+const features = tableFeatures({})
+
+// 3. Define your columns. Renderers return HTML strings (rendered via x-html).
+const columns: Array> = [
+  {
+    accessorKey: 'firstName', // accessorKey shorthand
+    header: 'First Name',
+    cell: (info) => info.getValue(),
+  },
+  {
+    accessorFn: (row) => row.lastName, // accessorFn alternative with a custom id
+    id: 'lastName',
+    header: () => 'Last Name',
+    cell: (info) => `${info.getValue()}`,
+  },
+  {
+    accessorKey: 'age',
+    header: () => 'Age',
+  },
+]
+
+// 4. Register an Alpine component
+Alpine.data('table', () => {
+  // Store data in Alpine-reactive state so updates flow into the table
+  const local = Alpine.reactive({ data: defaultData })
+
+  // 5. Create the table instance, reading data through a getter so it stays reactive
+  const table = createTable({
+    features,
+    columns,
+    get data() {
+      return local.data
+    },
+  })
+
+  // Expose the instance (and FlexRender) to the template
+  return { table, FlexRender }
+})
+
+window.Alpine = Alpine
+Alpine.start()
+```
+
+```html
+
+
+ + + + + + + +
+
+ +``` + +A few things to note: + +- `tableFeatures({})` declares which optional features the table uses. Registering only what you need keeps bundles small and gives TypeScript accurate types for the table instance. +- The core row model is always included automatically. Feature row models (sorting, filtering, pagination) are registered as slots directly on the `tableFeatures({...})` call when you need them. +- The `get data()` getter keeps the table reactive: when `local.data` is reassigned, the table sees the new data. Passing `data: local.data` would capture a one-time snapshot. +- `FlexRender` is also attached to the instance as `table.FlexRender`, so you can write `x-html="table.FlexRender({ cell })"` instead of exposing the top-level helper. + +See the full [Basic createTable example](./examples/basic-create-table) for a runnable version with more columns and a footer. + +## Add a Feature: Sorting + +Features are opt-in in v9. To make columns sortable, register `rowSortingFeature` and the `sortedRowModel` factory in `tableFeatures`, then wire a header click handler. Because the click handler cannot live inside `x-html`, wrap the rendered header in a real element and attach `@click` there. + +```ts +// main.ts (additions) +import { + FlexRender, + createSortedRowModel, + createTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowSortingFeature, // enables sorting APIs and state + sortedRowModel: createSortedRowModel(), // client-side sorting + sortFns, +}) + +// columns and the Alpine.data registration are otherwise unchanged +``` + +```html + + + + +``` + +Clicking a header now toggles between ascending, descending, and unsorted. Every other feature follows this same pattern: register the feature (and its row model factory as a slot on `tableFeatures` if it has one), then use the APIs it adds to the table, columns, and rows, attaching any interactive controls to real elements in your markup. See the [Sorting example](./examples/sorting) for custom sort functions, multi-sorting, and per-column options. + +## Where to Go Next + +**Table state.** In v9, table state is backed by TanStack Store atoms, which the adapter makes reactive in Alpine. You usually do not need to manage it yourself: set `initialState` for starting values and call feature APIs like `table.setSorting(...)` or `table.nextPage()`. When you need to read a state slice in your markup, use `table.atoms..get()` (for example `table.atoms.pagination.get().pageIndex`) or `table.store.get()` for the whole state. There is no state selector, because the table instance is already reactive. The [Table State Guide](./guide/table-state.md) is the foundational guide for everything else. + +**Feature examples.** Each feature has a runnable example, such as [Column Filters](./examples/filters), [Pagination](./examples/pagination), [Row Selection](./examples/row-selection), and [Column Visibility](./examples/column-visibility). + +**Composable tables.** When multiple tables in your app share features and row models, define them once with `createTableHook`: + +```ts +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { createAppTable, createAppColumnHelper } = createTableHook({ features }) +``` + +Then call `createAppTable({ columns, data })` from your component instead of `createTable`, and define columns with `createAppColumnHelper`. See the [Basic createAppTable example](./examples/basic-app-table) for the full pattern. + +**Examples.** Browse the runnable [Alpine examples](./examples/basic-create-table), from basic tables to feature demos, to see intended usage end to end. diff --git a/docs/framework/alpine/reference/functions/FlexRender-1.md b/docs/framework/alpine/reference/functions/FlexRender-1.md new file mode 100644 index 0000000000..1124ef2e62 --- /dev/null +++ b/docs/framework/alpine/reference/functions/FlexRender-1.md @@ -0,0 +1,47 @@ +--- +id: FlexRender +title: FlexRender +--- + +# Function: FlexRender() + +```ts +function FlexRender(props): any; +``` + +Defined in: [flexRender.ts:76](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/flexRender.ts#L76) + +Simplified wrapper of `flexRender`. Use this utility function to render headers, cells, or footers with custom markup. +Only one prop (`cell`, `header`, or `footer`) may be passed. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `unknown` = `unknown` + +## Parameters + +### props + +[`FlexRenderProps`](../type-aliases/FlexRenderProps.md)\<`TFeatures`, `TData`, `TValue`\> + +## Returns + +`any` + +## Example + +```html + + + +``` diff --git a/docs/framework/alpine/reference/functions/createTable.md b/docs/framework/alpine/reference/functions/createTable.md new file mode 100644 index 0000000000..23fa961a70 --- /dev/null +++ b/docs/framework/alpine/reference/functions/createTable.md @@ -0,0 +1,32 @@ +--- +id: createTable +title: createTable +--- + +# Function: createTable() + +```ts +function createTable(tableOptions): AlpineTable; +``` + +Defined in: [createTable.ts:27](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTable.ts#L27) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +## Parameters + +### tableOptions + +`TableOptions`\<`TFeatures`, `TData`\> + +## Returns + +[`AlpineTable`](../type-aliases/AlpineTable.md)\<`TFeatures`, `TData`\> diff --git a/docs/framework/alpine/reference/functions/createTableHook.md b/docs/framework/alpine/reference/functions/createTableHook.md new file mode 100644 index 0000000000..aa7203f52d --- /dev/null +++ b/docs/framework/alpine/reference/functions/createTableHook.md @@ -0,0 +1,72 @@ +--- +id: createTableHook +title: createTableHook +--- + +# Function: createTableHook() + +```ts +function createTableHook(__namedParameters): object; +``` + +Defined in: [createTableHook.ts:21](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTableHook.ts#L21) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +## Parameters + +### \_\_namedParameters + +[`CreateTableHookOptions`](../type-aliases/CreateTableHookOptions.md)\<`TFeatures`\> + +## Returns + +`object` + +### appFeatures + +```ts +appFeatures: TFeatures; +``` + +### createAppColumnHelper() + +```ts +createAppColumnHelper: () => ColumnHelper; +``` + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +#### Returns + +`ColumnHelper`\<`TFeatures`, `TData`\> + +### createAppTable() + +```ts +createAppTable: (tableOptions) => AppAlpineTable; +``` + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +#### Parameters + +##### tableOptions + +`Omit`\<`TableOptions`\<`TFeatures`, `TData`\>, `"features"`\> + +#### Returns + +[`AppAlpineTable`](../type-aliases/AppAlpineTable.md)\<`TFeatures`, `TData`\> diff --git a/docs/framework/alpine/reference/functions/flexRender.md b/docs/framework/alpine/reference/functions/flexRender.md new file mode 100644 index 0000000000..0c7178925b --- /dev/null +++ b/docs/framework/alpine/reference/functions/flexRender.md @@ -0,0 +1,45 @@ +--- +id: flexRender +title: flexRender +--- + +# Function: flexRender() + +```ts +function flexRender(render, props): any; +``` + +Defined in: [flexRender.ts:22](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/flexRender.ts#L22) + +Renders an Alpine table value with the provided context props. + +Use this lower-level helper for custom header, cell, or footer renderers when +you already have the render function and context. `FlexRender` is the +convenience wrapper for table cell/header/footer objects. Renderers typically +return a string of markup that you render into the DOM with `x-html`. + +## Type Parameters + +### TProps + +`TProps` *extends* `object` + +## Parameters + +### render + +`any` + +### props + +`TProps` + +## Returns + +`any` + +## Example + +```ts +flexRender(cell.column.columnDef.cell, cell.getContext()) +``` diff --git a/docs/framework/alpine/reference/index.md b/docs/framework/alpine/reference/index.md new file mode 100644 index 0000000000..c5f4fea221 --- /dev/null +++ b/docs/framework/alpine/reference/index.md @@ -0,0 +1,21 @@ +--- +id: "@tanstack/alpine-table" +title: "@tanstack/alpine-table" +--- + +# @tanstack/alpine-table + +## Type Aliases + +- [AlpineTable](type-aliases/AlpineTable.md) +- [AppAlpineTable](type-aliases/AppAlpineTable.md) +- [AppColumnHelper](type-aliases/AppColumnHelper.md) +- [CreateTableHookOptions](type-aliases/CreateTableHookOptions.md) +- [FlexRenderProps](type-aliases/FlexRenderProps.md) + +## Functions + +- [createTable](functions/createTable.md) +- [createTableHook](functions/createTableHook.md) +- [flexRender](functions/flexRender.md) +- [FlexRender](functions/FlexRender-1.md) diff --git a/docs/framework/alpine/reference/type-aliases/AlpineTable.md b/docs/framework/alpine/reference/type-aliases/AlpineTable.md new file mode 100644 index 0000000000..a92c167fb8 --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/AlpineTable.md @@ -0,0 +1,40 @@ +--- +id: AlpineTable +title: AlpineTable +--- + +# Type Alias: AlpineTable\ + +```ts +type AlpineTable = Table & object; +``` + +Defined in: [createTable.ts:12](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTable.ts#L12) + +## Type Declaration + +### flexRender + +```ts +flexRender: typeof flexRender; +``` + +A lower-level helper to render the content of a cell, header, or footer from a render function and its context. + +### FlexRender + +```ts +FlexRender: typeof FlexRender; +``` + +A convenience helper to render a cell, header, or footer object. Call from `x-html`, e.g. `FlexRender({ header })`. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` diff --git a/docs/framework/alpine/reference/type-aliases/AppAlpineTable.md b/docs/framework/alpine/reference/type-aliases/AppAlpineTable.md new file mode 100644 index 0000000000..29d2810046 --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/AppAlpineTable.md @@ -0,0 +1,22 @@ +--- +id: AppAlpineTable +title: AppAlpineTable +--- + +# Type Alias: AppAlpineTable\ + +```ts +type AppAlpineTable = AlpineTable; +``` + +Defined in: [createTableHook.ts:11](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTableHook.ts#L11) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` diff --git a/docs/framework/alpine/reference/type-aliases/AppColumnHelper.md b/docs/framework/alpine/reference/type-aliases/AppColumnHelper.md new file mode 100644 index 0000000000..32eb01c8f0 --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/AppColumnHelper.md @@ -0,0 +1,22 @@ +--- +id: AppColumnHelper +title: AppColumnHelper +--- + +# Type Alias: AppColumnHelper\ + +```ts +type AppColumnHelper = ReturnType; +``` + +Defined in: [createTableHook.ts:16](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTableHook.ts#L16) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` diff --git a/docs/framework/alpine/reference/type-aliases/CreateTableHookOptions.md b/docs/framework/alpine/reference/type-aliases/CreateTableHookOptions.md new file mode 100644 index 0000000000..1110702572 --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/CreateTableHookOptions.md @@ -0,0 +1,18 @@ +--- +id: CreateTableHookOptions +title: CreateTableHookOptions +--- + +# Type Alias: CreateTableHookOptions\ + +```ts +type CreateTableHookOptions = Omit, "columns" | "data" | "state">; +``` + +Defined in: [createTableHook.ts:6](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/createTableHook.ts#L6) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` diff --git a/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md b/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md new file mode 100644 index 0000000000..8331f6ea7c --- /dev/null +++ b/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md @@ -0,0 +1,59 @@ +--- +id: FlexRenderProps +title: FlexRenderProps +--- + +# Type Alias: FlexRenderProps\ + +```ts +type FlexRenderProps = + | { + cell: Cell; + footer?: never; + header?: never; +} + | { + cell?: never; + footer?: never; + header: Header; +} + | { + cell?: never; + footer: Header; + header?: never; +}; +``` + +Defined in: [flexRender.ts:49](https://github.com/TanStack/table/blob/main/packages/alpine-table/src/flexRender.ts#L49) + +Simplified wrapper of `flexRender`. Use this utility function to render headers, cells, or footers with custom markup. +Only one prop (`cell`, `header`, or `footer`) may be passed. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` = `CellData` + +## Example + +```html + + + +``` + +This replaces calling `flexRender` directly like this: +```ts +flexRender(cell.column.columnDef.cell, cell.getContext()) +flexRender(header.column.columnDef.header, header.getContext()) +flexRender(footer.column.columnDef.footer, footer.getContext()) +``` diff --git a/docs/guide/table-and-column-meta.md b/docs/guide/table-and-column-meta.md index 8bceab16af..cbe946f06b 100644 --- a/docs/guide/table-and-column-meta.md +++ b/docs/guide/table-and-column-meta.md @@ -145,6 +145,26 @@ const table = this.tableController.table({ table.options.meta?.updateData(rowIndex, columnId, newValue) ``` +# Alpine + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +}) + +// ...later, anywhere the table is available (e.g. inside an Alpine.data method) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + # Vanilla ```ts @@ -285,6 +305,18 @@ const features = tableFeatures({ }) ``` +# Alpine + +```ts +import { metaHelper, rowSortingFeature, tableFeatures } from '@tanstack/alpine-table' + +const features = tableFeatures({ + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + # Vanilla ```ts @@ -549,6 +581,26 @@ declare module '@tanstack/lit-table' { } ``` +# Alpine + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/alpine-table' + +declare module '@tanstack/alpine-table' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + # Vanilla ```ts diff --git a/docs/guide/tables.md b/docs/guide/tables.md index 9337847654..8f79c3394b 100644 --- a/docs/guide/tables.md +++ b/docs/guide/tables.md @@ -138,6 +138,23 @@ const table = this.tableController.table({ }) ``` +# Alpine + +```ts +import { createTable, tableFeatures } from '@tanstack/alpine-table' + +const features = tableFeatures({}) // Core features only; add columnFilteringFeature, rowSortingFeature, etc. as needed + +// inside Alpine.data('table', () => { ... }): +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + # Vanilla ```ts @@ -227,6 +244,18 @@ const table = this.tableController.table({ }) ``` +# Alpine + +```ts +const table = createTable({ + features, + columns, + get data() { + return local.data + }, +}) +``` + # Vanilla ```ts @@ -284,6 +313,10 @@ Direct reads like `table.atoms.rowSelection.get()` and `table.store.state.rowSel [Table State Guide](../framework/lit/guide/table-state) +# Alpine + +[Table State Guide](../framework/alpine/guide/table-state) + # Vanilla [Table State Guide](../framework/vanilla/guide/table-state) diff --git a/docs/installation.md b/docs/installation.md index 7b06e6e542..294439c328 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -15,6 +15,7 @@ solid: @tanstack/solid-table@beta svelte: @tanstack/svelte-table@beta angular: @tanstack/angular-table@beta lit: @tanstack/lit-table@beta +alpine: @tanstack/alpine-table@beta @@ -51,6 +52,10 @@ The `@tanstack/angular-table` package works with Angular 19 or newer. The Angula The `@tanstack/lit-table` package works with Lit 3 (3.1.3 or newer) and also requires `@lit/context` as a peer dependency. +# Alpine + +The `@tanstack/alpine-table` package works with Alpine 3. + Don't see your favorite framework (or favorite version of your framework) listed? You can always just use the `@tanstack/table-core` package and build your own adapter in your own codebase. Usually, only a thin wrapper is needed to manage state and rendering for your specific framework. Browse the [source code](https://github.com/TanStack/table/tree/beta/packages) of all of the other adapters to see how they work. diff --git a/docs/overview.md b/docs/overview.md index 5acc0ba632..ddcbfdebb7 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -2,7 +2,7 @@ title: Overview --- -TanStack Table is a **headless UI** library for building powerful tables and datagrids. Its core is **framework agnostic**, which means its API is the same regardless of the framework you are using. Official adapters are provided for React, Preact, Vue, Solid, Svelte, Angular, and Lit, and you can use the core directly in vanilla TypeScript or JavaScript via `@tanstack/table-core`. +TanStack Table is a **headless UI** library for building powerful tables and datagrids. Its core is **framework agnostic**, which means its API is the same regardless of the framework you are using. Official adapters are provided for React, Preact, Vue, Solid, Svelte, Angular, Lit, and Alpine, and you can use the core directly in vanilla TypeScript or JavaScript via `@tanstack/table-core`. > **These docs are for TanStack Table v9, which is currently in beta.** The docs for v8, the latest stable release, live at [tanstack.com/table/v8](https://tanstack.com/table/v8). When installing v9 packages, use the `beta` npm tag (see the [Installation](./installation) page). @@ -38,6 +38,10 @@ If you are upgrading from v8, start with the migration guide for your framework: - [Migrating to V9](./framework/lit/guide/migrating) +# Alpine + +- Alpine support is new in v9, so there is nothing to migrate. Start with the [Quick Start](./framework/alpine/quick-start). + # Vanilla - The vanilla `@tanstack/table-core` entry point changed in v9. See the [Vanilla JS](./vanilla) page for the new setup. @@ -68,7 +72,7 @@ If you use TypeScript, you will get top-notch type safety and editor autocomplet Since TanStack Table is headless and runs on a vanilla TypeScript core, it is agnostic in a couple of ways: -1. TanStack Table is **framework agnostic**, which means you can use it with any JavaScript framework (or library) that you want. TanStack Table provides ready-to-use adapters for React, Preact, Vue, Solid, Svelte, Angular, and Lit out of the box, and the framework-agnostic core works in vanilla TypeScript or JavaScript and even in JS-to-native platforms like React Native. You can also create your own adapter if you need to. +1. TanStack Table is **framework agnostic**, which means you can use it with any JavaScript framework (or library) that you want. TanStack Table provides ready-to-use adapters for React, Preact, Vue, Solid, Svelte, Angular, Lit, and Alpine out of the box, and the framework-agnostic core works in vanilla TypeScript or JavaScript and even in JS-to-native platforms like React Native. You can also create your own adapter if you need to. 2. TanStack Table is **CSS / component library agnostic**, which means that you can use TanStack Table with whatever CSS strategy or component library you want. TanStack Table itself does not render any table markup or styles. You bring your own! Want to use Tailwind or ShadCN? No problem! Want to use Material UI or Bootstrap? No problem! Have your own custom design system? TanStack Table was made for you! ## Core Objects and Types @@ -216,6 +220,23 @@ TanStack Table will help you build just about any type of table you can imagine. - [Row Selection](./framework/lit/guide/row-selection) - Select/deselect rows (checkboxes) - [Row Sorting](./framework/lit/guide/sorting) - Sort rows by column values +# Alpine + +- [Faceting](./framework/alpine/guide/column-faceting) - List unique values or min/max values for a column or for the entire table +- [Column Filtering](./framework/alpine/guide/column-filtering) - Filter rows based on search values for a column +- [Column Grouping](./framework/alpine/guide/grouping) - Group columns together, run aggregations, and more +- [Column Ordering](./framework/alpine/guide/column-ordering) - Dynamically change the order of columns +- [Column Pinning](./framework/alpine/guide/column-pinning) - Pin (Freeze) columns to the left or right of the table +- [Column Resizing](./framework/alpine/guide/column-resizing) - Let users resize columns with drag handles +- [Column Sizing](./framework/alpine/guide/column-sizing) - Dynamically change the size of columns +- [Column Visibility](./framework/alpine/guide/column-visibility) - Hide/show columns +- [Global Filtering](./framework/alpine/guide/global-filtering) - Filter rows based on search values for the entire table +- [Row Expanding](./framework/alpine/guide/expanding) - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/alpine/guide/pagination) - Paginate rows +- [Row Pinning](./framework/alpine/guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table +- [Row Selection](./framework/alpine/guide/row-selection) - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/alpine/guide/sorting) - Sort rows by column values + These are just some of the capabilities that you can build with TanStack Table. There are many more features that are possible with TanStack Table that you can add along-side the built-in features. diff --git a/examples/alpine/basic-app-table/index.html b/examples/alpine/basic-app-table/index.html new file mode 100644 index 0000000000..49e2d13e3d --- /dev/null +++ b/examples/alpine/basic-app-table/index.html @@ -0,0 +1,58 @@ + + + + + + TanStack Alpine Table - Basic (createAppTable) + + + +
+ + + + + + + + + + +
+
+
+ + + diff --git a/examples/alpine/basic-app-table/package.json b/examples/alpine/basic-app-table/package.json new file mode 100644 index 0000000000..8ee3b23f29 --- /dev/null +++ b/examples/alpine/basic-app-table/package.json @@ -0,0 +1,22 @@ +{ + "name": "tanstack-alpine-table-example-basic-app-table", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/basic-app-table/src/index.css b/examples/alpine/basic-app-table/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/basic-app-table/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/basic-app-table/src/main.ts b/examples/alpine/basic-app-table/src/main.ts new file mode 100644 index 0000000000..7055a9638d --- /dev/null +++ b/examples/alpine/basic-app-table/src/main.ts @@ -0,0 +1,118 @@ +import Alpine from 'alpinejs' +import { createTableHook, tableFeatures } from '@tanstack/alpine-table' +import './index.css' + +// This example uses the new `createTableHook` method to create a re-usable table hook factory instead of independently using the standalone `createTable` function and `createColumnHelper` method. You can choose to use either way. + +// 1. Define what the shape of your data will be for each row +type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +// 2. Create some dummy data with a stable reference (this could be an API response stored in `Alpine.reactive` or similar) +const defaultData: Array = [ + { + firstName: 'tanner', + lastName: 'linsley', + age: 24, + visits: 100, + status: 'In Relationship', + progress: 50, + }, + { + firstName: 'tandy', + lastName: 'miller', + age: 40, + visits: 40, + status: 'Single', + progress: 80, + }, + { + firstName: 'joe', + lastName: 'dirte', + age: 45, + visits: 20, + status: 'Complicated', + progress: 10, + }, + { + firstName: 'kevin', + lastName: 'vandy', + age: 28, + visits: 100, + status: 'Single', + progress: 70, + }, +] + +// 3. New in V9! Tell the table which features and row models we want to use. In this case, this will be a basic table with no additional features. These are now shared across every table created from this hook. +const { createAppTable, createAppColumnHelper } = createTableHook({ + features: tableFeatures({}), + debugTable: true, +}) + +// 4. Create a helper object to help define our columns (pre-bound to the hook's features) +const columnHelper = createAppColumnHelper() + +// 5. Define the columns for your table with a stable reference (defined statically outside of the Alpine component). Renderers return HTML strings that are rendered with `x-html` via `table.FlexRender`. +const columns = columnHelper.columns([ + // accessorKey method (most common for simple use-cases) + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (info) => info.column.id, + }), + // accessorFn used (alternative) along with a custom id + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => `${info.getValue()}`, + header: () => 'Last Name', + footer: (info) => info.column.id, + }), + // accessorFn used to transform the data + columnHelper.accessor((row) => Number(row.age), { + id: 'age', + header: () => 'Age', + cell: (info) => info.renderValue(), + footer: (info) => info.column.id, + }), + columnHelper.accessor('visits', { + header: () => 'Visits', + footer: (info) => info.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (info) => info.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (info) => info.column.id, + }), +]) + +// 6. Register the Alpine component. +Alpine.data('table', () => { + // Store data with a stable reference + const local = Alpine.reactive({ data: [...defaultData] }) + + // 7. Create the table instance with the required columns and data. Features + // and row models were already defined in the `createTableHook` call above. + const table = createAppTable({ + columns, + get data() { + return local.data + }, + // add additional table options here or in the createTableHook call above + }) + + // `table.FlexRender` is attached by the hook, so the template can render + // headers/cells/footers with `x-html="table.FlexRender({ ... })"`. + return { table } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/basic-app-table/src/vite-env.d.ts b/examples/alpine/basic-app-table/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/basic-app-table/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/basic-app-table/tsconfig.json b/examples/alpine/basic-app-table/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/basic-app-table/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/basic-app-table/vite.config.js b/examples/alpine/basic-app-table/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/basic-app-table/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/examples/alpine/basic-create-table/index.html b/examples/alpine/basic-create-table/index.html new file mode 100644 index 0000000000..f0d57a7ff9 --- /dev/null +++ b/examples/alpine/basic-create-table/index.html @@ -0,0 +1,58 @@ + + + + + + TanStack Alpine Table - Basic Create Table + + + +
+
+ + +
+ + + + + + + + + + +
+
+
+ + + diff --git a/examples/alpine/basic-create-table/package.json b/examples/alpine/basic-create-table/package.json new file mode 100644 index 0000000000..4fc3cdef62 --- /dev/null +++ b/examples/alpine/basic-create-table/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-basic-create-table", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/basic-create-table/src/index.css b/examples/alpine/basic-create-table/src/index.css new file mode 100644 index 0000000000..0f729c8493 --- /dev/null +++ b/examples/alpine/basic-create-table/src/index.css @@ -0,0 +1,50 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.button-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} diff --git a/examples/alpine/basic-create-table/src/main.ts b/examples/alpine/basic-create-table/src/main.ts new file mode 100644 index 0000000000..f736bffd42 --- /dev/null +++ b/examples/alpine/basic-create-table/src/main.ts @@ -0,0 +1,76 @@ +import Alpine from 'alpinejs' +import { FlexRender, createTable, tableFeatures } from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +// This example uses the standalone `createTable` function to create a table without the `createTableHook` util. + +// 1. New in V9! Tell the table which features and row models we want to use. In this case, this will be a basic table with no additional features +const features = tableFeatures({}) // util method to create sharable TFeatures object/type + +// 4. Define the columns for your table. This uses the new `ColumnDef` type to define columns. +// Alternatively, check out the createTableHook/createAppColumnHelper util for an even more type-safe way to define columns. +const columns: Array> = [ + { + accessorKey: 'firstName', // accessorKey method (most common for simple use-cases) + header: 'First Name', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => row.lastName, // accessorFn used (alternative) along with a custom id + id: 'lastName', + header: () => 'Last Name', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => Number(row.age), // accessorFn used to transform the data + id: 'age', + header: () => 'Age', + cell: (info) => info.renderValue(), + }, + { + accessorKey: 'visits', + header: () => 'Visits', + }, + { + accessorKey: 'status', + header: 'Status', + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + }, +] + +// 5. Register the Alpine component. Store data in Alpine-reactive state so the +// buttons can swap it out and the table re-renders. +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(20) }) + + // 6. Create the table instance with required features, columns, and data + const table = createTable({ + debugTable: true, // optionally, enable console logging debug messages + features, // new required option in V9. Tell the table which features you are importing and using (better tree-shaking) + columns, + get data() { + return local.data + }, + // add additional table options here + }) + + return { + table, + FlexRender, // exposed so the template can call it inside `x-html` + refreshData() { + local.data = makeData(20) + }, + stressTest() { + local.data = makeData(1_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/basic-create-table/src/makeData.ts b/examples/alpine/basic-create-table/src/makeData.ts new file mode 100644 index 0000000000..fd239ee360 --- /dev/null +++ b/examples/alpine/basic-create-table/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/basic-create-table/src/vite-env.d.ts b/examples/alpine/basic-create-table/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/basic-create-table/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/basic-create-table/tsconfig.json b/examples/alpine/basic-create-table/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/basic-create-table/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/basic-create-table/vite.config.js b/examples/alpine/basic-create-table/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/basic-create-table/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/examples/alpine/basic-external-atoms/index.html b/examples/alpine/basic-external-atoms/index.html new file mode 100644 index 0000000000..a12c1fbb1e --- /dev/null +++ b/examples/alpine/basic-external-atoms/index.html @@ -0,0 +1,116 @@ + + + + + + TanStack Alpine Table - Basic External Atoms + + +
+
+ + +
+ + + + + + + +
+
+
+ + + + + +
Page
+ + + of + + +
+ + | Go to page: + + + +
+
+

+    
+ + + diff --git a/examples/alpine/basic-external-atoms/package.json b/examples/alpine/basic-external-atoms/package.json new file mode 100644 index 0000000000..1a6b63ecac --- /dev/null +++ b/examples/alpine/basic-external-atoms/package.json @@ -0,0 +1,24 @@ +{ + "name": "tanstack-alpine-table-example-basic-external-atoms", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "@tanstack/store": "^0.11.0", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/basic-external-atoms/src/index.css b/examples/alpine/basic-external-atoms/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/basic-external-atoms/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/basic-external-atoms/src/main.ts b/examples/alpine/basic-external-atoms/src/main.ts new file mode 100644 index 0000000000..27f6fb34d1 --- /dev/null +++ b/examples/alpine/basic-external-atoms/src/main.ts @@ -0,0 +1,110 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + createColumnHelper, + createPaginatedRowModel, + createSortedRowModel, + createTable, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' +import { createAtom } from '@tanstack/store' +import { makeData } from './makeData' +import './index.css' +import type { PaginationState, SortingState } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +// This example demonstrates managing individual slices of table state via +// external TanStack Store atoms. Each atom is a stand-alone, subscribable +// reactive cell — you can read, write, or subscribe to it from anywhere, +// which makes it convenient for sharing state across components or modules. + +const features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortFns, +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + }), +]) + +// Create stable external atoms for the individual state slices you want to +// own. These live at module scope here, but could just as easily be created +// in a shared store module and imported by multiple components. +const sortingAtom = createAtom([]) +const paginationAtom = createAtom({ + pageIndex: 0, + pageSize: 10, +}) + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(1_000) }) + + // The table creates internal base atoms for every slice, and (because we + // pass `atoms` below) reads/writes for `sorting` and `pagination` are routed + // through the external atoms we created instead. Atom changes flow through + // the derived `table.store`, which the Alpine adapter subscribes to so the + // template re-renders. + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + atoms: { + sorting: sortingAtom, + pagination: paginationAtom, + }, + debugTable: true, + }) + + return { + table, + FlexRender, + // pick the indicator for a column's current sort direction + sortIndicator(isSorted: false | 'asc' | 'desc') { + return { asc: ' 🔼', desc: ' 🔽' }[isSorted as string] ?? '' + }, + goToPage(value: string) { + table.setPageIndex(value ? Number(value) - 1 : 0) + }, + setPageSize(value: string) { + table.setPageSize(Number(value)) + }, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(1_000_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/basic-external-atoms/src/makeData.ts b/examples/alpine/basic-external-atoms/src/makeData.ts new file mode 100644 index 0000000000..d274a17f45 --- /dev/null +++ b/examples/alpine/basic-external-atoms/src/makeData.ts @@ -0,0 +1,47 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + return makeDataLevel() +} diff --git a/examples/alpine/basic-external-atoms/src/vite-env.d.ts b/examples/alpine/basic-external-atoms/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/basic-external-atoms/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/basic-external-atoms/tsconfig.json b/examples/alpine/basic-external-atoms/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/basic-external-atoms/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/basic-external-atoms/vite.config.js b/examples/alpine/basic-external-atoms/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/basic-external-atoms/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/examples/alpine/basic-external-state/index.html b/examples/alpine/basic-external-state/index.html new file mode 100644 index 0000000000..fba3cb9be7 --- /dev/null +++ b/examples/alpine/basic-external-state/index.html @@ -0,0 +1,114 @@ + + + + + + TanStack Alpine Table - Basic External State + + +
+
+ + +
+ + + + + + + +
+
+
+ + + + + +
Page
+ + + of + + +
+ + | Go to page: + + + +
+
+

+    
+ + + diff --git a/examples/alpine/basic-external-state/package.json b/examples/alpine/basic-external-state/package.json new file mode 100644 index 0000000000..4f2de0cf1d --- /dev/null +++ b/examples/alpine/basic-external-state/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-basic-external-state", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/basic-external-state/src/index.css b/examples/alpine/basic-external-state/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/basic-external-state/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/basic-external-state/src/main.ts b/examples/alpine/basic-external-state/src/main.ts new file mode 100644 index 0000000000..601be6c0d6 --- /dev/null +++ b/examples/alpine/basic-external-state/src/main.ts @@ -0,0 +1,121 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + createColumnHelper, + createPaginatedRowModel, + createSortedRowModel, + createTable, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { PaginationState, SortingState } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +// This example demonstrates managing table state externally (here, in an +// Alpine-reactive object) instead of letting the table manage its own state +// internally. The controlled slices are passed in via `state` and raised back +// up through the `onXxxChange` handlers. + +const features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortFns, +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + }), +]) + +Alpine.data('table', () => { + // Own the controlled state slices in Alpine-reactive state. Reading them via + // getters inside the table options is what makes the adapter re-apply options + // when they change. + const local = Alpine.reactive({ + data: makeData(1_000), + sorting: [] as SortingState, + pagination: { pageIndex: 0, pageSize: 10 }, + }) + + const table = createTable({ + debugTable: true, + features, + columns, + get data() { + return local.data + }, + // connect our external state back down to the table via getters + state: { + get sorting() { + return local.sorting + }, + get pagination() { + return local.pagination + }, + }, + onSortingChange: (updater) => { + // raise sorting state changes to our own state management + local.sorting = + typeof updater === 'function' ? updater(local.sorting) : updater + }, + onPaginationChange: (updater) => { + // raise pagination state changes to our own state management + local.pagination = + typeof updater === 'function' ? updater(local.pagination) : updater + }, + }) + + return { + table, + FlexRender, + // pick the indicator for a column's current sort direction + sortIndicator(isSorted: false | 'asc' | 'desc') { + return { asc: ' 🔼', desc: ' 🔽' }[isSorted as string] ?? '' + }, + // expose the controlled state to the template for pagination display + get pagination() { + return local.pagination + }, + goToPage(value: string) { + table.setPageIndex(value ? Number(value) - 1 : 0) + }, + setPageSize(value: string) { + table.setPageSize(Number(value)) + }, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(1_000_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/basic-external-state/src/makeData.ts b/examples/alpine/basic-external-state/src/makeData.ts new file mode 100644 index 0000000000..d274a17f45 --- /dev/null +++ b/examples/alpine/basic-external-state/src/makeData.ts @@ -0,0 +1,47 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + return makeDataLevel() +} diff --git a/examples/alpine/basic-external-state/src/vite-env.d.ts b/examples/alpine/basic-external-state/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/basic-external-state/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/basic-external-state/tsconfig.json b/examples/alpine/basic-external-state/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/basic-external-state/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/basic-external-state/vite.config.js b/examples/alpine/basic-external-state/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/basic-external-state/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/examples/alpine/column-groups/index.html b/examples/alpine/column-groups/index.html new file mode 100644 index 0000000000..b27ca67c80 --- /dev/null +++ b/examples/alpine/column-groups/index.html @@ -0,0 +1,60 @@ + + + + + + TanStack Alpine Table - Column Groups + + +
+
+ + +
+ + + + + + + + + + +
+
+ + + diff --git a/examples/alpine/column-groups/package.json b/examples/alpine/column-groups/package.json new file mode 100644 index 0000000000..14f714188b --- /dev/null +++ b/examples/alpine/column-groups/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-column-groups", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/column-groups/src/index.css b/examples/alpine/column-groups/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/column-groups/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/column-groups/src/main.ts b/examples/alpine/column-groups/src/main.ts new file mode 100644 index 0000000000..1ec57f1b2f --- /dev/null +++ b/examples/alpine/column-groups/src/main.ts @@ -0,0 +1,87 @@ +import Alpine from 'alpinejs' +import { FlexRender, createTable, tableFeatures } from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({}) + +const defaultColumns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(20) }) + + const table = createTable({ + features, + columns: defaultColumns, + get data() { + return local.data + }, + debugTable: true, + }) + + return { + table, + FlexRender, + refreshData() { + local.data = makeData(20) + }, + stressTest() { + local.data = makeData(1_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/column-groups/src/makeData.ts b/examples/alpine/column-groups/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/column-groups/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/column-groups/src/vite-env.d.ts b/examples/alpine/column-groups/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/column-groups/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/column-groups/tsconfig.json b/examples/alpine/column-groups/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/column-groups/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/column-groups/vite.config.js b/examples/alpine/column-groups/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/column-groups/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/examples/alpine/column-ordering/index.html b/examples/alpine/column-ordering/index.html new file mode 100644 index 0000000000..fffe19ed52 --- /dev/null +++ b/examples/alpine/column-ordering/index.html @@ -0,0 +1,92 @@ + + + + + + TanStack Alpine Table - Column Ordering + + +
+
+
+ +
+ +
+
+
+ + +
+
+ + + + + + + + + + +
+
+

+    
+ + + diff --git a/examples/alpine/column-ordering/package.json b/examples/alpine/column-ordering/package.json new file mode 100644 index 0000000000..7e15d7cc4f --- /dev/null +++ b/examples/alpine/column-ordering/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-column-ordering", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/column-ordering/src/index.css b/examples/alpine/column-ordering/src/index.css new file mode 100644 index 0000000000..7de250342f --- /dev/null +++ b/examples/alpine/column-ordering/src/index.css @@ -0,0 +1,162 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} + +/* Column ordering / pinning demo helpers. */ +.demo-button-sm { + padding: 0.25rem; +} +.pin-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; +} +.pin-button { + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0 0.5rem; +} +.outlined-table { + border: 2px solid #000; +} +.table-row-group { + display: flex; +} +.nowrap { + white-space: nowrap; +} +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} diff --git a/examples/alpine/column-ordering/src/main.ts b/examples/alpine/column-ordering/src/main.ts new file mode 100644 index 0000000000..19e469baa2 --- /dev/null +++ b/examples/alpine/column-ordering/src/main.ts @@ -0,0 +1,106 @@ +import Alpine from 'alpinejs' +import { faker } from '@faker-js/faker' +import { + FlexRender, + columnOrderingFeature, + columnVisibilityFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnOrderingFeature, + columnVisibilityFeature, +}) + +const defaultColumns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(20) }) + + const table = createTable({ + features, + columns: defaultColumns, + get data() { + return local.data + }, + debugTable: true, + }) + + return { + table, + FlexRender, + refreshData() { + local.data = makeData(20) + }, + stressTest() { + local.data = makeData(1_000) + }, + reset() { + table.setColumnOrder([]) + table.setColumnVisibility({}) + }, + randomizeColumns() { + table.setColumnOrder( + faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), + ) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/column-ordering/src/makeData.ts b/examples/alpine/column-ordering/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/column-ordering/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/column-ordering/src/vite-env.d.ts b/examples/alpine/column-ordering/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/column-ordering/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/column-ordering/tsconfig.json b/examples/alpine/column-ordering/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/column-ordering/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/column-ordering/vite.config.js b/examples/alpine/column-ordering/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/column-ordering/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/examples/alpine/column-pinning-split/index.html b/examples/alpine/column-pinning-split/index.html new file mode 100644 index 0000000000..83b36c6c2d --- /dev/null +++ b/examples/alpine/column-pinning-split/index.html @@ -0,0 +1,256 @@ + + + + + + TanStack Alpine Table - Column Pinning (Split) + + +
+
+
+ +
+ +
+
+
+ + + +
+
+

+ This example takes advantage of the "splitting" APIs. (APIs that have + "left", "center", and "right" modifiers) +

+
+ + + + + + + + +
+ + + + + + + + +
+ + + + + + + + +
+
+
+

+    
+ + + diff --git a/examples/alpine/column-pinning-split/package.json b/examples/alpine/column-pinning-split/package.json new file mode 100644 index 0000000000..4ae4570801 --- /dev/null +++ b/examples/alpine/column-pinning-split/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-column-pinning-split", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/column-pinning-split/src/index.css b/examples/alpine/column-pinning-split/src/index.css new file mode 100644 index 0000000000..501a649179 --- /dev/null +++ b/examples/alpine/column-pinning-split/src/index.css @@ -0,0 +1,163 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} + +/* Column pinning split demo helpers. */ +.demo-button-sm { + padding: 0.25rem; +} +.pin-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; +} +.pin-button { + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0 0.5rem; +} +.outlined-table { + border: 2px solid #000; +} +.split-tables { + display: flex; + gap: 1rem; +} +.nowrap { + white-space: nowrap; +} +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} diff --git a/examples/alpine/column-pinning-split/src/main.ts b/examples/alpine/column-pinning-split/src/main.ts new file mode 100644 index 0000000000..6adb277f48 --- /dev/null +++ b/examples/alpine/column-pinning-split/src/main.ts @@ -0,0 +1,106 @@ +import Alpine from 'alpinejs' +import { faker } from '@faker-js/faker' +import { + FlexRender, + columnOrderingFeature, + columnPinningFeature, + columnVisibilityFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnOrderingFeature, + columnPinningFeature, + columnVisibilityFeature, +}) + +const defaultColumns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(1_000) }) + + const table = createTable({ + features, + columns: defaultColumns, + get data() { + return local.data + }, + debugTable: true, + debugHeaders: true, + debugColumns: true, + }) + + return { + table, + FlexRender, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(1_000_000) + }, + randomizeColumns() { + table.setColumnOrder( + faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), + ) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/column-pinning-split/src/makeData.ts b/examples/alpine/column-pinning-split/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/column-pinning-split/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/column-pinning-split/src/vite-env.d.ts b/examples/alpine/column-pinning-split/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/column-pinning-split/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/column-pinning-split/tsconfig.json b/examples/alpine/column-pinning-split/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/column-pinning-split/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/column-pinning-split/vite.config.js b/examples/alpine/column-pinning-split/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/column-pinning-split/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/examples/alpine/column-pinning-sticky/index.html b/examples/alpine/column-pinning-sticky/index.html new file mode 100644 index 0000000000..c2b293ac21 --- /dev/null +++ b/examples/alpine/column-pinning-sticky/index.html @@ -0,0 +1,128 @@ + + + + + + TanStack Alpine Table - Column Pinning (Sticky) + + +
+
+
+ +
+ +
+
+
+ + + +
+
+
+ + + + + + + +
+
+
+

+    
+ + + diff --git a/examples/alpine/column-pinning-sticky/package.json b/examples/alpine/column-pinning-sticky/package.json new file mode 100644 index 0000000000..7cc8c6e7f9 --- /dev/null +++ b/examples/alpine/column-pinning-sticky/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-column-pinning-sticky", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/column-pinning-sticky/src/index.css b/examples/alpine/column-pinning-sticky/src/index.css new file mode 100644 index 0000000000..cf1f25e40b --- /dev/null +++ b/examples/alpine/column-pinning-sticky/src/index.css @@ -0,0 +1,192 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} + +/* Column pinning sticky demo helpers. */ +.table-container { + border: 1px solid lightgray; + overflow-x: scroll; + width: 100%; + max-width: 960px; + position: relative; +} + +.table-container th { + background-color: lightgray; + border-bottom: 1px solid lightgray; + font-weight: bold; + height: 30px; + padding: 2px 4px; + position: relative; + text-align: center; +} + +.table-container td { + background-color: white; + padding: 2px 4px; +} + +.resizer { + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + height: 100%; + position: absolute; + right: 0; + top: 0; + touch-action: none; + user-select: none; + width: 5px; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +.demo-button-sm { + padding: 0.25rem; +} +.pin-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; +} +.pin-button { + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0 0.5rem; +} +.nowrap { + white-space: nowrap; +} diff --git a/examples/alpine/column-pinning-sticky/src/main.ts b/examples/alpine/column-pinning-sticky/src/main.ts new file mode 100644 index 0000000000..8ed443057a --- /dev/null +++ b/examples/alpine/column-pinning-sticky/src/main.ts @@ -0,0 +1,133 @@ +import Alpine from 'alpinejs' +import { faker } from '@faker-js/faker' +import { + FlexRender, + columnOrderingFeature, + columnPinningFeature, + columnResizingFeature, + columnSizingFeature, + columnVisibilityFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { Column, ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnOrderingFeature, + columnPinningFeature, + columnResizingFeature, + columnSizingFeature, + columnVisibilityFeature, +}) + +const getCommonPinningStyles = ( + column: Column, +): string => { + const isPinned = column.getIsPinned() + const isLastLeftPinnedColumn = + isPinned === 'left' && column.getIsLastColumn('left') + const isFirstRightPinnedColumn = + isPinned === 'right' && column.getIsFirstColumn('right') + + const boxShadow = isLastLeftPinnedColumn + ? '-4px 0 4px -4px gray inset' + : isFirstRightPinnedColumn + ? '4px 0 4px -4px gray inset' + : undefined + + const styles: Array = [] + if (boxShadow) styles.push(`box-shadow: ${boxShadow}`) + if (isPinned === 'left') styles.push(`left: ${column.getStart('left')}px`) + if (isPinned === 'right') styles.push(`right: ${column.getAfter('right')}px`) + styles.push(`opacity: ${isPinned ? '0.95' : '1'}`) + styles.push(`position: ${isPinned ? 'sticky' : 'relative'}`) + styles.push(`width: ${column.getSize()}px`) + styles.push(`z-index: ${isPinned ? '1' : '0'}`) + return styles.join('; ') +} + +const defaultColumns: Array> = [ + { + accessorKey: 'firstName', + id: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + size: 180, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + size: 180, + }, + { + accessorKey: 'age', + id: 'age', + header: 'Age', + footer: (props) => props.column.id, + size: 180, + }, + { + accessorKey: 'visits', + id: 'visits', + header: 'Visits', + footer: (props) => props.column.id, + size: 180, + }, + { + accessorKey: 'status', + id: 'status', + header: 'Status', + footer: (props) => props.column.id, + size: 180, + }, + { + accessorKey: 'progress', + id: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + size: 180, + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(20) }) + + const table = createTable({ + features, + columns: defaultColumns, + get data() { + return local.data + }, + columnResizeMode: 'onChange', + debugTable: true, + debugHeaders: true, + debugColumns: true, + }) + + return { + table, + FlexRender, + getCommonPinningStyles, + refreshData() { + local.data = makeData(20) + }, + stressTest() { + local.data = makeData(1_000) + }, + randomizeColumns() { + table.setColumnOrder( + faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), + ) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/column-pinning-sticky/src/makeData.ts b/examples/alpine/column-pinning-sticky/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/column-pinning-sticky/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/column-pinning-sticky/src/vite-env.d.ts b/examples/alpine/column-pinning-sticky/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/column-pinning-sticky/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/column-pinning-sticky/tsconfig.json b/examples/alpine/column-pinning-sticky/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/column-pinning-sticky/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/column-pinning-sticky/vite.config.js b/examples/alpine/column-pinning-sticky/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/column-pinning-sticky/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/examples/alpine/column-pinning/index.html b/examples/alpine/column-pinning/index.html new file mode 100644 index 0000000000..e3c7856923 --- /dev/null +++ b/examples/alpine/column-pinning/index.html @@ -0,0 +1,120 @@ + + + + + + TanStack Alpine Table - Column Pinning + + +
+
+
+ +
+ +
+
+
+ + + +
+
+

+ This example uses the non-split APIs. Columns are just reordered within + 1 table instead of being split into 3 different tables. +

+
+ + + + + + + +
+
+
+

+    
+ + + diff --git a/examples/alpine/column-pinning/package.json b/examples/alpine/column-pinning/package.json new file mode 100644 index 0000000000..3c385b2f7d --- /dev/null +++ b/examples/alpine/column-pinning/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-column-pinning", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/column-pinning/src/index.css b/examples/alpine/column-pinning/src/index.css new file mode 100644 index 0000000000..7de250342f --- /dev/null +++ b/examples/alpine/column-pinning/src/index.css @@ -0,0 +1,162 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} + +/* Column ordering / pinning demo helpers. */ +.demo-button-sm { + padding: 0.25rem; +} +.pin-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; +} +.pin-button { + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0 0.5rem; +} +.outlined-table { + border: 2px solid #000; +} +.table-row-group { + display: flex; +} +.nowrap { + white-space: nowrap; +} +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} diff --git a/examples/alpine/column-pinning/src/main.ts b/examples/alpine/column-pinning/src/main.ts new file mode 100644 index 0000000000..6adb277f48 --- /dev/null +++ b/examples/alpine/column-pinning/src/main.ts @@ -0,0 +1,106 @@ +import Alpine from 'alpinejs' +import { faker } from '@faker-js/faker' +import { + FlexRender, + columnOrderingFeature, + columnPinningFeature, + columnVisibilityFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnOrderingFeature, + columnPinningFeature, + columnVisibilityFeature, +}) + +const defaultColumns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(1_000) }) + + const table = createTable({ + features, + columns: defaultColumns, + get data() { + return local.data + }, + debugTable: true, + debugHeaders: true, + debugColumns: true, + }) + + return { + table, + FlexRender, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(1_000_000) + }, + randomizeColumns() { + table.setColumnOrder( + faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), + ) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/column-pinning/src/makeData.ts b/examples/alpine/column-pinning/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/column-pinning/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/column-pinning/src/vite-env.d.ts b/examples/alpine/column-pinning/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/column-pinning/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/column-pinning/tsconfig.json b/examples/alpine/column-pinning/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/column-pinning/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/column-pinning/vite.config.js b/examples/alpine/column-pinning/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/column-pinning/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/examples/alpine/column-resizing-performant/index.html b/examples/alpine/column-resizing-performant/index.html new file mode 100644 index 0000000000..c7681a76b3 --- /dev/null +++ b/examples/alpine/column-resizing-performant/index.html @@ -0,0 +1,70 @@ + + + + + + TanStack Alpine Table - Column Resizing (Performant) + + +
+
+ + +
+
+

+      
+ +
+
+
+ +
+
+ +
+
+
+
+ + + diff --git a/examples/alpine/column-resizing-performant/package.json b/examples/alpine/column-resizing-performant/package.json new file mode 100644 index 0000000000..e90d2751a8 --- /dev/null +++ b/examples/alpine/column-resizing-performant/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-column-resizing-performant", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/column-resizing-performant/src/index.css b/examples/alpine/column-resizing-performant/src/index.css new file mode 100644 index 0000000000..3b32b57fab --- /dev/null +++ b/examples/alpine/column-resizing-performant/src/index.css @@ -0,0 +1,181 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table, +.divTable { + border: 1px solid lightgray; + width: fit-content; +} + +.tr { + display: flex; +} + +tr, +.tr { + width: fit-content; + height: 30px; +} + +th, +.th, +td, +.td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +th, +.th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +td, +.td { + height: 30px; +} + +.resizer { + position: absolute; + top: 0; + right: 0; + height: 100%; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.is-resizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} + +.scroll-container { + overflow-x: auto; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/column-resizing-performant/src/main.ts b/examples/alpine/column-resizing-performant/src/main.ts new file mode 100644 index 0000000000..1dc38f9d9e --- /dev/null +++ b/examples/alpine/column-resizing-performant/src/main.ts @@ -0,0 +1,110 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + columnResizingFeature, + columnSizingFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(200) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + defaultColumn: { minSize: 60, maxSize: 800 }, + columnResizeMode: 'onChange', + debugTable: true, + debugHeaders: true, + debugColumns: true, + }) + + return { + table, + FlexRender, + local, + // Compute CSS variables for column sizes. Reading columnSizingInfo keeps + // this reactive so the variables recompute while a column is being resized. + columnSizeVars(): string { + // touch the resizing state so Alpine tracks it as a dependency + void table.atoms.columnResizing.get().columnSizingStart + const headers = table.getFlatHeaders() + const styles: Array = [] + for (const header of headers) { + styles.push(`--header-${header.id}-size:${header.getSize()}`) + styles.push(`--col-${header.column.id}-size:${header.column.getSize()}`) + } + styles.push(`width:${table.getTotalSize()}px`) + return styles.join(';') + }, + refreshData() { + local.data = makeData(200) + }, + stressTest() { + local.data = makeData(2_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/column-resizing-performant/src/makeData.ts b/examples/alpine/column-resizing-performant/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/column-resizing-performant/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/column-resizing-performant/src/vite-env.d.ts b/examples/alpine/column-resizing-performant/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/column-resizing-performant/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/column-resizing-performant/tsconfig.json b/examples/alpine/column-resizing-performant/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/column-resizing-performant/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/column-resizing-performant/vite.config.js b/examples/alpine/column-resizing-performant/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/column-resizing-performant/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/examples/alpine/column-resizing/index.html b/examples/alpine/column-resizing/index.html new file mode 100644 index 0000000000..3aa764a7b5 --- /dev/null +++ b/examples/alpine/column-resizing/index.html @@ -0,0 +1,179 @@ + + + + + + TanStack Alpine Table - Column Resizing + + +
+
+ + +
+ + +
+
+
<table/>
+
+ + + + + + + +
+
+
+
<div/> (relative)
+
+
+
+ +
+
+ +
+
+
+
+
<div/> (absolute positioning)
+
+
+
+ +
+
+ +
+
+
+
+
+

+    
+ + + diff --git a/examples/alpine/column-resizing/package.json b/examples/alpine/column-resizing/package.json new file mode 100644 index 0000000000..108daf7b54 --- /dev/null +++ b/examples/alpine/column-resizing/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-column-resizing", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/column-resizing/src/index.css b/examples/alpine/column-resizing/src/index.css new file mode 100644 index 0000000000..68a5ab9c4a --- /dev/null +++ b/examples/alpine/column-resizing/src/index.css @@ -0,0 +1,196 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table, +.divTable { + border: 1px solid lightgray; + width: fit-content; +} + +.tr { + display: flex; +} + +tr, +.tr { + width: fit-content; + height: 30px; +} + +th, +.th, +td, +.td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +th, +.th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +td, +.td { + height: 30px; +} + +.resizer { + position: absolute; + top: 0; + height: 100%; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.ltr { + right: 0; +} + +.resizer.rtl { + left: 0; +} + +.resizer.is-resizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.outlined-control { + border-color: #000; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/column-resizing/src/main.ts b/examples/alpine/column-resizing/src/main.ts new file mode 100644 index 0000000000..fe8d953bbe --- /dev/null +++ b/examples/alpine/column-resizing/src/main.ts @@ -0,0 +1,126 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + columnResizingFeature, + columnSizingFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { + ColumnDef, + ColumnResizeDirection, + ColumnResizeMode, +} from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnResizingFeature, + columnSizingFeature, +}) + +const columns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ + data: makeData(10), + columnResizeMode: 'onChange', + columnResizeDirection: 'ltr', + }) as { + data: Array + columnResizeMode: ColumnResizeMode + columnResizeDirection: ColumnResizeDirection + } + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + get columnResizeMode() { + return local.columnResizeMode + }, + get columnResizeDirection() { + return local.columnResizeDirection + }, + debugTable: true, + debugHeaders: true, + debugColumns: true, + }) + + return { + table, + FlexRender, + local, + // Translate the resizer while dragging when using the "onEnd" resize mode. + resizerTransform(header: any) { + if (local.columnResizeMode === 'onEnd' && header.column.getIsResizing()) { + const delta = table.atoms.columnResizing.get().deltaOffset ?? 0 + const dir = local.columnResizeDirection === 'rtl' ? -1 : 1 + return `transform: translateX(${dir * delta}px)` + } + return '' + }, + refreshData() { + local.data = makeData(10) + }, + stressTest() { + local.data = makeData(100) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/column-resizing/src/makeData.ts b/examples/alpine/column-resizing/src/makeData.ts new file mode 100644 index 0000000000..fd239ee360 --- /dev/null +++ b/examples/alpine/column-resizing/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/column-resizing/src/vite-env.d.ts b/examples/alpine/column-resizing/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/column-resizing/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/column-resizing/tsconfig.json b/examples/alpine/column-resizing/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/column-resizing/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/column-resizing/vite.config.js b/examples/alpine/column-resizing/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/column-resizing/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/examples/alpine/column-sizing/index.html b/examples/alpine/column-sizing/index.html new file mode 100644 index 0000000000..2bf130e72c --- /dev/null +++ b/examples/alpine/column-sizing/index.html @@ -0,0 +1,59 @@ + + + + + + TanStack Alpine Table - Column Sizing + + +
+
+ + +
+ + + + + + + +
+
+

+    
+ + + diff --git a/examples/alpine/column-sizing/package.json b/examples/alpine/column-sizing/package.json new file mode 100644 index 0000000000..50228bff66 --- /dev/null +++ b/examples/alpine/column-sizing/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-column-sizing", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/column-sizing/src/index.css b/examples/alpine/column-sizing/src/index.css new file mode 100644 index 0000000000..0143ea4de4 --- /dev/null +++ b/examples/alpine/column-sizing/src/index.css @@ -0,0 +1,168 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; + position: relative; +} + +.resizer { + position: absolute; + top: 0; + height: 100%; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.ltr { + right: 0; +} + +.resizer.rtl { + left: 0; +} + +.resizer.is-resizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/column-sizing/src/main.ts b/examples/alpine/column-sizing/src/main.ts new file mode 100644 index 0000000000..61f13e3ebe --- /dev/null +++ b/examples/alpine/column-sizing/src/main.ts @@ -0,0 +1,85 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + columnResizingFeature, + columnSizingFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns: Array> = [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + }, + { + accessorKey: 'age', + header: () => 'Age', + }, + { + accessorKey: 'visits', + header: () => 'Visits', + }, + { + accessorKey: 'status', + header: 'Status', + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + }, + { + accessorKey: 'rank', + header: 'Rank', + }, + { + accessorKey: 'createdAt', + header: 'Created At', + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(1_000) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + columnResizeMode: 'onChange', + columnResizeDirection: 'ltr', + debugTable: true, + debugHeaders: true, + debugColumns: true, + }) + + return { + table, + FlexRender, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(1_000_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/column-sizing/src/makeData.ts b/examples/alpine/column-sizing/src/makeData.ts new file mode 100644 index 0000000000..fc070cd5d2 --- /dev/null +++ b/examples/alpine/column-sizing/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?: Array +} + +const range = (len: number) => { + const arr: Array = [] + 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: Array) { + const makeDataLevel = (depth = 0): Array => { + 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/alpine/column-sizing/src/vite-env.d.ts b/examples/alpine/column-sizing/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/column-sizing/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/column-sizing/tsconfig.json b/examples/alpine/column-sizing/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/column-sizing/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/column-sizing/vite.config.js b/examples/alpine/column-sizing/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/column-sizing/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/examples/alpine/column-visibility/index.html b/examples/alpine/column-visibility/index.html new file mode 100644 index 0000000000..33fc2cb2ff --- /dev/null +++ b/examples/alpine/column-visibility/index.html @@ -0,0 +1,86 @@ + + + + + + TanStack Alpine Table - Column Visibility + + +
+
+ + +
+
+
+
+ +
+ +
+
+ + + + + + + + + + +
+
+ + + diff --git a/examples/alpine/column-visibility/package.json b/examples/alpine/column-visibility/package.json new file mode 100644 index 0000000000..5426282360 --- /dev/null +++ b/examples/alpine/column-visibility/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-column-visibility", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/column-visibility/src/index.css b/examples/alpine/column-visibility/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/column-visibility/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/column-visibility/src/main.ts b/examples/alpine/column-visibility/src/main.ts new file mode 100644 index 0000000000..25ea4a7a13 --- /dev/null +++ b/examples/alpine/column-visibility/src/main.ts @@ -0,0 +1,94 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + columnVisibilityFeature, + createTable, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnVisibilityFeature, +}) + +const columns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(20) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + debugTable: true, + }) + + return { + table, + FlexRender, + refreshData() { + local.data = makeData(20) + }, + stressTest() { + local.data = makeData(1_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/column-visibility/src/makeData.ts b/examples/alpine/column-visibility/src/makeData.ts new file mode 100644 index 0000000000..d274a17f45 --- /dev/null +++ b/examples/alpine/column-visibility/src/makeData.ts @@ -0,0 +1,47 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + return makeDataLevel() +} diff --git a/examples/alpine/column-visibility/src/vite-env.d.ts b/examples/alpine/column-visibility/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/column-visibility/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/column-visibility/tsconfig.json b/examples/alpine/column-visibility/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/column-visibility/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/column-visibility/vite.config.js b/examples/alpine/column-visibility/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/column-visibility/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/examples/alpine/custom-plugin/index.html b/examples/alpine/custom-plugin/index.html new file mode 100644 index 0000000000..32d60b261b --- /dev/null +++ b/examples/alpine/custom-plugin/index.html @@ -0,0 +1,169 @@ + + + + + + TanStack Alpine Table - Custom Plugin (Density) + + +
+
+ + +
+
+ + + + + + + + +
+
+
+ + + + + +
Page
+ + + of + + +
+ + | Go to page: + + + +
+
+ Showing + + of + Rows +
+
+

+    
+ + + diff --git a/examples/alpine/custom-plugin/package.json b/examples/alpine/custom-plugin/package.json new file mode 100644 index 0000000000..5bab86cae5 --- /dev/null +++ b/examples/alpine/custom-plugin/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-custom-plugin", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/custom-plugin/src/index.css b/examples/alpine/custom-plugin/src/index.css new file mode 100644 index 0000000000..6475c1d0be --- /dev/null +++ b/examples/alpine/custom-plugin/src/index.css @@ -0,0 +1,153 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input, +.filter-select, +.wide-action-button, +.primary-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.filter-select { + width: 9rem; +} + +.wide-action-button { + width: 16rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.primary-action { + color: #fff; + background: #3b82f6; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/custom-plugin/src/main.ts b/examples/alpine/custom-plugin/src/main.ts new file mode 100644 index 0000000000..644e5e011b --- /dev/null +++ b/examples/alpine/custom-plugin/src/main.ts @@ -0,0 +1,259 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + assignTableAPIs, + columnFilteringFeature, + createColumnHelper, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTable, + filterFns, + functionalUpdate, + makeStateUpdater, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { + Column, + OnChangeFn, + RowData, + TableFeature, + TableFeatures, + Updater, +} from '@tanstack/alpine-table' +import type { Person } from './makeData' + +// TypeScript setup for our new feature with all of the same type-safety as stock TanStack Table features + +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} + +declare module '@tanstack/alpine-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} + +// Here is all of the actual javascript code for our new feature +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' // cycle through the 3 options + } + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) + }, + }, + }) + }, +} +// end of custom feature code + +// app code +const features = tableFeatures({ + columnFilteringFeature, + rowSortingFeature, + rowPaginationFeature, + densityPlugin, // pass in our plugin just like any other stock feature + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns, + sortFns, +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }), + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.accessor('visits', { + header: () => 'Visits', + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), +]) + +type PersonColumn = Column + +Alpine.data('table', () => { + // The density state is owned externally (in Alpine-reactive state) and passed + // back into the table via the `state` option + `onDensityChange` handler that + // our custom feature exposes. + const local = Alpine.reactive({ + data: makeData(1_000), + density: 'md', + }) as { data: Array; density: DensityState } + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + debugTable: true, + state: { + // passing the density state to the table, TS is still happy :) + get density() { + return local.density + }, + }, + onDensityChange: (updater) => { + // raise density state changes to our own state management + local.density = + typeof updater === 'function' ? updater(local.density) : updater + }, + }) + + return { + table, + FlexRender, + // padding driven by our custom density feature + densityPadding() { + return local.density === 'sm' + ? '4px' + : local.density === 'md' + ? '8px' + : '16px' + }, + // pick the indicator for a column's current sort direction + sortIndicator(isSorted: false | 'asc' | 'desc') { + return { asc: ' 🔼', desc: ' 🔽' }[isSorted as string] ?? '' + }, + // the demo filters numeric columns with a min/max range, others with text + isNumberColumn(column: PersonColumn) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + return typeof firstValue === 'number' + }, + setTextFilter(column: PersonColumn, value: string) { + column.setFilterValue(value) + }, + rangeValue(column: PersonColumn, index: 0 | 1) { + return ( + (column.getFilterValue() as [unknown, unknown] | undefined)?.[index] ?? + '' + ) + }, + setRangeMin(column: PersonColumn, value: string) { + column.setFilterValue((old: [unknown, unknown] | undefined) => [ + value, + old?.[1], + ]) + }, + setRangeMax(column: PersonColumn, value: string) { + column.setFilterValue((old: [unknown, unknown] | undefined) => [ + old?.[0], + value, + ]) + }, + goToPage(value: string) { + table.setPageIndex(value ? Number(value) - 1 : 0) + }, + setPageSize(value: string) { + table.setPageSize(Number(value)) + }, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(1_000_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/custom-plugin/src/makeData.ts b/examples/alpine/custom-plugin/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/alpine/custom-plugin/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + 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/alpine/custom-plugin/src/vite-env.d.ts b/examples/alpine/custom-plugin/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/custom-plugin/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/custom-plugin/tsconfig.json b/examples/alpine/custom-plugin/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/custom-plugin/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/custom-plugin/vite.config.js b/examples/alpine/custom-plugin/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/custom-plugin/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/examples/alpine/expanding/index.html b/examples/alpine/expanding/index.html new file mode 100644 index 0000000000..a5af369b53 --- /dev/null +++ b/examples/alpine/expanding/index.html @@ -0,0 +1,182 @@ + + + + + + TanStack Alpine Table - Expanding + + +
+
+ + +
+
+ + + + + + + +
+
+
+ + + + + + Page + + + of + + + + + | Go to page: + + +
+
+
+ + of + + rows +
+

+    
+ + + diff --git a/examples/alpine/expanding/package.json b/examples/alpine/expanding/package.json new file mode 100644 index 0000000000..7007b6aff8 --- /dev/null +++ b/examples/alpine/expanding/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-expanding", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/expanding/src/index.css b/examples/alpine/expanding/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/expanding/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/expanding/src/main.ts b/examples/alpine/expanding/src/main.ts new file mode 100644 index 0000000000..f524d2e53c --- /dev/null +++ b/examples/alpine/expanding/src/main.ts @@ -0,0 +1,136 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + columnFilteringFeature, + createExpandedRowModel, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTable, + filterFns, + rowExpandingFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { Column, ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + rowPaginationFeature, + rowSortingFeature, + rowSelectionFeature, + expandedRowModel: createExpandedRowModel(), + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns, + sortFns, +}) + +// The `firstName` column renders an interactive selection checkbox + expander in +// its header and cell. Because Alpine cannot process directives inside `x-html`, +// those controls are rendered directly in `index.html` (special-cased by column +// id), so here the column just exposes the plain value. +const columns: Array> = [ + { + accessorKey: 'firstName', + header: () => 'First Name', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, +] + +type PersonColumn = Column + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(100, 5, 3) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + getSubRows: (row) => row.subRows, + debugTable: true, + }) + + return { + table, + FlexRender, + // the demo filters numeric columns with a min/max range, others with text + isNumberColumn(column: PersonColumn) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + return typeof firstValue === 'number' + }, + setTextFilter(column: PersonColumn, value: string) { + column.setFilterValue(value) + }, + rangeValue(column: PersonColumn, index: 0 | 1) { + return ( + (column.getFilterValue() as [unknown, unknown] | undefined)?.[index] ?? + '' + ) + }, + setRangeMin(column: PersonColumn, value: string) { + column.setFilterValue((old: [unknown, unknown] | undefined) => [ + value, + old?.[1], + ]) + }, + setRangeMax(column: PersonColumn, value: string) { + column.setFilterValue((old: [unknown, unknown] | undefined) => [ + old?.[0], + value, + ]) + }, + goToPage(value: string) { + table.setPageIndex(value ? Number(value) - 1 : 0) + }, + refreshData() { + local.data = makeData(100, 5, 3) + }, + stressTest() { + local.data = makeData(1_000, 5, 3) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/expanding/src/makeData.ts b/examples/alpine/expanding/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/expanding/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/expanding/src/vite-env.d.ts b/examples/alpine/expanding/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/expanding/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/expanding/tsconfig.json b/examples/alpine/expanding/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/expanding/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/expanding/vite.config.js b/examples/alpine/expanding/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/expanding/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/examples/alpine/filters-faceted/index.html b/examples/alpine/filters-faceted/index.html new file mode 100644 index 0000000000..6a5d4d7bd4 --- /dev/null +++ b/examples/alpine/filters-faceted/index.html @@ -0,0 +1,177 @@ + + + + + + TanStack Alpine Table - Faceted Filters + + +
+
+ + +
+ +
+ + + + + + + +
+
+
+ + + + + +
Page
+ + + of + + +
+ + | Go to page: + + + +
+
+ Showing + + of + + Rows +
+
+ + + diff --git a/examples/alpine/filters-faceted/package.json b/examples/alpine/filters-faceted/package.json new file mode 100644 index 0000000000..6bbfe412bd --- /dev/null +++ b/examples/alpine/filters-faceted/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-filters-faceted", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/filters-faceted/src/index.css b/examples/alpine/filters-faceted/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/filters-faceted/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/filters-faceted/src/main.ts b/examples/alpine/filters-faceted/src/main.ts new file mode 100644 index 0000000000..82df1766d2 --- /dev/null +++ b/examples/alpine/filters-faceted/src/main.ts @@ -0,0 +1,185 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + columnFacetingFeature, + columnFilteringFeature, + createFacetedMinMaxValues, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createPaginatedRowModel, + createTable, + filterFns, + globalFilteringFeature, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { Column, ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + columnFacetingFeature, + rowPaginationFeature, + facetedRowModel: createFacetedRowModel(), + facetedMinMaxValues: createFacetedMinMaxValues(), + facetedUniqueValues: createFacetedUniqueValues(), + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns, +}) + +const columns: Array> = [ + { + header: 'Name', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: (props) => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, + ], + }, + ], + }, +] + +type PersonColumn = Column + +// small debounce helper, mirroring the Lit example's 500ms debounce +function debounce>( + fn: (...args: TArgs) => void, + wait: number, +) { + let timer: ReturnType | undefined + return (...args: TArgs) => { + clearTimeout(timer) + timer = setTimeout(() => fn(...args), wait) + } +} + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(1_000) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + globalFilterFn: 'includesString', + debugTable: true, + }) + + const setGlobalFilter = debounce( + (value: string) => table.setGlobalFilter(value), + 500, + ) + const setColumnFilter = debounce( + (column: PersonColumn, value: unknown) => column.setFilterValue(value), + 500, + ) + + return { + table, + FlexRender, + // numeric columns get a faceted min/max range, others a faceted datalist search + isNumberColumn(column: PersonColumn) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + return typeof firstValue === 'number' + }, + facetMin(column: PersonColumn) { + return column.getFacetedMinMaxValues()?.[0] ?? '' + }, + facetMax(column: PersonColumn) { + return column.getFacetedMinMaxValues()?.[1] ?? '' + }, + uniqueCount(column: PersonColumn) { + return column.getFacetedUniqueValues().size + }, + uniqueValues(column: PersonColumn) { + return Array.from(column.getFacetedUniqueValues().keys()) + .sort() + .slice(0, 5000) + }, + rangeValue(column: PersonColumn, index: 0 | 1) { + return ( + (column.getFilterValue() as [unknown, unknown] | undefined)?.[index] ?? + '' + ) + }, + onGlobalFilter(value: string) { + setGlobalFilter(value) + }, + onTextFilter(column: PersonColumn, value: string) { + setColumnFilter(column, value) + }, + onRangeMin(column: PersonColumn, value: string) { + setColumnFilter(column, (old: [number, number] | undefined) => [ + value ? Number(value) : undefined, + old?.[1], + ]) + }, + onRangeMax(column: PersonColumn, value: string) { + setColumnFilter(column, (old: [number, number] | undefined) => [ + old?.[0], + value ? Number(value) : undefined, + ]) + }, + goToPage(value: string) { + table.setPageIndex(value ? Number(value) - 1 : 0) + }, + pageSizes: [10, 20, 30, 40, 50], + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(100_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/filters-faceted/src/makeData.ts b/examples/alpine/filters-faceted/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/alpine/filters-faceted/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + 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/alpine/filters-faceted/src/vite-env.d.ts b/examples/alpine/filters-faceted/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/filters-faceted/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/filters-faceted/tsconfig.json b/examples/alpine/filters-faceted/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/filters-faceted/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/filters-faceted/vite.config.js b/examples/alpine/filters-faceted/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/filters-faceted/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/examples/alpine/filters/index.html b/examples/alpine/filters/index.html new file mode 100644 index 0000000000..658e2f0128 --- /dev/null +++ b/examples/alpine/filters/index.html @@ -0,0 +1,131 @@ + + + + + + TanStack Alpine Table - Filters + + +
+
+ + +
+ + + + + + + +
+
+
+ + + + + + Page + + + of + + + +
+

+    
+ + + diff --git a/examples/alpine/filters/package.json b/examples/alpine/filters/package.json new file mode 100644 index 0000000000..b990a9e23f --- /dev/null +++ b/examples/alpine/filters/package.json @@ -0,0 +1,22 @@ +{ + "name": "tanstack-alpine-table-example-filters", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/filters/src/index.css b/examples/alpine/filters/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/filters/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/filters/src/main.ts b/examples/alpine/filters/src/main.ts new file mode 100644 index 0000000000..cde4fc6f44 --- /dev/null +++ b/examples/alpine/filters/src/main.ts @@ -0,0 +1,133 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createTable, + filterFns, + metaHelper, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { Column, ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + +const features = tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns, + columnMeta: metaHelper(), +}) + +const columns: Array> = [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + }, + { + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + }, + { + accessorKey: 'age', + header: () => 'Age', + meta: { + filterVariant: 'range', + }, + }, + { + accessorKey: 'visits', + header: () => 'Visits', + meta: { + filterVariant: 'range', + }, + }, + { + accessorKey: 'status', + header: 'Status', + meta: { + filterVariant: 'select', + }, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + meta: { + filterVariant: 'range', + }, + }, +] + +type PersonColumn = Column + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(1_000) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + debugTable: true, + }) + + return { + table, + FlexRender, + // which filter UI a column wants + filterVariant(column: PersonColumn) { + return column.columnDef.meta?.filterVariant ?? 'text' + }, + // text filter + setTextFilter(column: PersonColumn, value: string) { + column.setFilterValue(value) + }, + // range filter (one input per bound) + rangeValue(column: PersonColumn, index: 0 | 1) { + return ( + (column.getFilterValue() as [unknown, unknown] | undefined)?.[index] ?? + '' + ) + }, + setRangeMin(column: PersonColumn, value: string) { + column.setFilterValue((old: [number, number] | undefined) => [ + value === '' ? undefined : Number(value), + old?.[1], + ]) + }, + setRangeMax(column: PersonColumn, value: string) { + column.setFilterValue((old: [number, number] | undefined) => [ + old?.[0], + value === '' ? undefined : Number(value), + ]) + }, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(100_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/filters/src/makeData.ts b/examples/alpine/filters/src/makeData.ts new file mode 100644 index 0000000000..6311127267 --- /dev/null +++ b/examples/alpine/filters/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/alpine/filters/src/vite-env.d.ts b/examples/alpine/filters/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/filters/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/filters/tsconfig.json b/examples/alpine/filters/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/filters/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/filters/vite.config.js b/examples/alpine/filters/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/filters/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/examples/alpine/grouping/index.html b/examples/alpine/grouping/index.html new file mode 100644 index 0000000000..7c9339cff9 --- /dev/null +++ b/examples/alpine/grouping/index.html @@ -0,0 +1,142 @@ + + + + + + TanStack Alpine Table - Grouping + + +
+
+ + +
+
+ + + + + + + +
+
+
+ + + + + + Page + + + of + + + + + | Go to page: + + + +
+
+ + Rows +
+
+ + + diff --git a/examples/alpine/grouping/package.json b/examples/alpine/grouping/package.json new file mode 100644 index 0000000000..5048b2c7ab --- /dev/null +++ b/examples/alpine/grouping/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-grouping", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/grouping/src/index.css b/examples/alpine/grouping/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/grouping/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/grouping/src/main.ts b/examples/alpine/grouping/src/main.ts new file mode 100644 index 0000000000..b735b31bc5 --- /dev/null +++ b/examples/alpine/grouping/src/main.ts @@ -0,0 +1,117 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + aggregationFns, + columnFilteringFeature, + columnGroupingFeature, + createColumnHelper, + createExpandedRowModel, + createFilteredRowModel, + createGroupedRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTable, + filterFns, + rowExpandingFeature, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnFilteringFeature, + columnGroupingFeature, + rowExpandingFeature, + rowPaginationFeature, + rowSortingFeature, + expandedRowModel: createExpandedRowModel(), + filteredRowModel: createFilteredRowModel(), + groupedRowModel: createGroupedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns, + sortFns, + aggregationFns, +}) + +const columnHelper = createColumnHelper() + +// Grouping toggle buttons (header) and expander toggles (grouped cells) are +// interactive and rendered directly in `index.html`, because Alpine cannot +// process directives inside `x-html`. +const columns: Array> = columnHelper.columns( + [ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + getGroupingValue: (row) => `${row.firstName} ${row.lastName}`, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => 'Last Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: () => 'Age', + aggregatedCell: ({ getValue }) => + Math.round(getValue() * 100) / 100, + aggregationFn: 'median', + }), + columnHelper.accessor('visits', { + header: () => 'Visits', + aggregationFn: 'sum', + aggregatedCell: ({ getValue }) => getValue().toLocaleString(), + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + cell: ({ getValue }) => Math.round(getValue() * 100) / 100 + '%', + aggregationFn: 'mean', + aggregatedCell: ({ getValue }) => + Math.round(getValue() * 100) / 100 + '%', + }), + ], +) + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(10_000) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + debugTable: true, + }) + + return { + table, + FlexRender, + cellBackground(cell: any) { + if (cell.getIsGrouped()) return '#0aff0082' + if (cell.getIsAggregated()) return '#ffa50078' + if (cell.getIsPlaceholder()) return '#ff000042' + return 'white' + }, + goToPage(value: string) { + table.setPageIndex(value ? Number(value) - 1 : 0) + }, + refreshData() { + local.data = makeData(10_000) + }, + stressTest() { + local.data = makeData(200_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/grouping/src/makeData.ts b/examples/alpine/grouping/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/grouping/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/grouping/src/vite-env.d.ts b/examples/alpine/grouping/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/grouping/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/grouping/tsconfig.json b/examples/alpine/grouping/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/grouping/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/grouping/vite.config.js b/examples/alpine/grouping/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/grouping/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/examples/alpine/pagination/index.html b/examples/alpine/pagination/index.html new file mode 100644 index 0000000000..48eda4d612 --- /dev/null +++ b/examples/alpine/pagination/index.html @@ -0,0 +1,116 @@ + + + + + + TanStack Alpine Table - Pagination + + +
+
+ + +
+
+ + + + + + + +
+
+
+ + + + + +
Page
+ + + of + + +
+ + | Go to page: + + + +
+
+ Showing + + of + Rows +
+

+    
+ + + diff --git a/examples/alpine/pagination/package.json b/examples/alpine/pagination/package.json new file mode 100644 index 0000000000..002c75ad9b --- /dev/null +++ b/examples/alpine/pagination/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-pagination", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/pagination/src/index.css b/examples/alpine/pagination/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/pagination/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/pagination/src/main.ts b/examples/alpine/pagination/src/main.ts new file mode 100644 index 0000000000..5c47f2b1ba --- /dev/null +++ b/examples/alpine/pagination/src/main.ts @@ -0,0 +1,79 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + createColumnHelper, + createPaginatedRowModel, + createTable, + rowPaginationFeature, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { Person } from './makeData' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }), + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.accessor('visits', { + header: () => 'Visits', + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), +]) + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(1_000) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + debugTable: true, + }) + + return { + table, + FlexRender, + pageSizes: [10, 20, 30, 40, 50], + goToPage(value: string) { + table.setPageIndex(value ? Number(value) - 1 : 0) + }, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(100_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/pagination/src/makeData.ts b/examples/alpine/pagination/src/makeData.ts new file mode 100644 index 0000000000..d274a17f45 --- /dev/null +++ b/examples/alpine/pagination/src/makeData.ts @@ -0,0 +1,47 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + return makeDataLevel() +} diff --git a/examples/alpine/pagination/src/vite-env.d.ts b/examples/alpine/pagination/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/pagination/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/pagination/tsconfig.json b/examples/alpine/pagination/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/pagination/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/pagination/vite.config.js b/examples/alpine/pagination/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/pagination/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/examples/alpine/row-pinning/index.html b/examples/alpine/row-pinning/index.html new file mode 100644 index 0000000000..7c36798bbe --- /dev/null +++ b/examples/alpine/row-pinning/index.html @@ -0,0 +1,250 @@ + + + + + + TanStack Alpine Table - Row Pinning + + +
+
+ + +
+
+ + + + + + + + + + + + +
+
+
+ + + + + + Page + + + of + + + + + | Go to page: + + + +
+
+ + + diff --git a/examples/alpine/row-pinning/package.json b/examples/alpine/row-pinning/package.json new file mode 100644 index 0000000000..4d43647ea8 --- /dev/null +++ b/examples/alpine/row-pinning/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-row-pinning", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/row-pinning/src/index.css b/examples/alpine/row-pinning/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/row-pinning/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/row-pinning/src/main.ts b/examples/alpine/row-pinning/src/main.ts new file mode 100644 index 0000000000..b3eab74a81 --- /dev/null +++ b/examples/alpine/row-pinning/src/main.ts @@ -0,0 +1,149 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + columnFilteringFeature, + createExpandedRowModel, + createFilteredRowModel, + createPaginatedRowModel, + createTable, + filterFns, + rowExpandingFeature, + rowPaginationFeature, + rowPinningFeature, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { Column, ColumnDef, Row } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + rowPinningFeature, + rowExpandingFeature, + columnFilteringFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns, +}) + +// The `pin` column (pin buttons) and the `firstName` cell (expander + value) +// render interactive controls directly in `index.html`, because Alpine cannot +// process directives inside `x-html`. Those columns expose plain values here. +const columns: Array> = [ + { + id: 'pin', + header: () => 'Pin', + cell: () => '', + }, + { + accessorKey: 'firstName', + header: () => 'First Name', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + }, + { + accessorKey: 'age', + header: () => 'Age', + }, + { + accessorKey: 'visits', + header: () => 'Visits', + }, + { + accessorKey: 'status', + header: 'Status', + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + }, +] + +type PersonColumn = Column + +// Sticky offset styling for a pinned row (mirrors the Lit renderPinnedRow). +function pinnedRowStyle( + row: Row, + bottomCount: number, +) { + const isPinnedTop = row.getIsPinned() === 'top' + let style = 'background-color:lightblue;position:sticky;' + if (isPinnedTop) { + style += `top:${row.getPinnedIndex() * 26 + 48}px` + } else { + style += `bottom:${(bottomCount - 1 - row.getPinnedIndex()) * 26}px` + } + return style +} + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(1_000, 2, 2) }) + + const table = createTable({ + debugTable: true, + features, + columns, + get data() { + return local.data + }, + initialState: { + pagination: { pageSize: 20, pageIndex: 0 }, + }, + getSubRows: (row) => row.subRows, + keepPinnedRows: true, + debugAll: true, + }) + + return { + table, + FlexRender, + pinnedRowStyle, + isNumberColumn(column: PersonColumn) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + return typeof firstValue === 'number' + }, + setTextFilter(column: PersonColumn, value: string) { + column.setFilterValue(value) + }, + rangeValue(column: PersonColumn, index: 0 | 1) { + return ( + (column.getFilterValue() as [unknown, unknown] | undefined)?.[index] ?? + '' + ) + }, + setRangeMin(column: PersonColumn, value: string) { + column.setFilterValue((old: [unknown, unknown] | undefined) => [ + value, + old?.[1], + ]) + }, + setRangeMax(column: PersonColumn, value: string) { + column.setFilterValue((old: [unknown, unknown] | undefined) => [ + old?.[0], + value, + ]) + }, + goToPage(value: string) { + table.setPageIndex(value ? Number(value) - 1 : 0) + }, + refreshData() { + local.data = makeData(1_000, 2, 2) + }, + stressTest() { + local.data = makeData(200_000, 2, 2) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/row-pinning/src/makeData.ts b/examples/alpine/row-pinning/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/row-pinning/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/row-pinning/src/vite-env.d.ts b/examples/alpine/row-pinning/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/row-pinning/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/row-pinning/tsconfig.json b/examples/alpine/row-pinning/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/row-pinning/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/row-pinning/vite.config.js b/examples/alpine/row-pinning/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/row-pinning/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/examples/alpine/row-selection/index.html b/examples/alpine/row-selection/index.html new file mode 100644 index 0000000000..6f5c25d19a --- /dev/null +++ b/examples/alpine/row-selection/index.html @@ -0,0 +1,113 @@ + + + + + + TanStack Alpine Table - Row Selection + + +
+
+ + +
+
+ + + + + + + +
+
+
+ + + + + + Page + + + of + + + +
+
+ + + diff --git a/examples/alpine/row-selection/package.json b/examples/alpine/row-selection/package.json new file mode 100644 index 0000000000..502674232e --- /dev/null +++ b/examples/alpine/row-selection/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-row-selection", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/row-selection/src/index.css b/examples/alpine/row-selection/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/row-selection/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/row-selection/src/main.ts b/examples/alpine/row-selection/src/main.ts new file mode 100644 index 0000000000..b3010b6032 --- /dev/null +++ b/examples/alpine/row-selection/src/main.ts @@ -0,0 +1,97 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createTable, + filterFns, + rowPaginationFeature, + rowSelectionFeature, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + rowSelectionFeature, + columnFilteringFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns, +}) + +// The `select` column renders interactive checkboxes in its header (select-all) +// and cells (per-row). Because Alpine cannot process directives inside `x-html`, +// those checkboxes are rendered directly in `index.html` (special-cased by +// column id), so here the column just exposes a plain value. +const columns: Array> = [ + { + id: 'select', + header: () => '', + cell: () => '', + }, + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + }, + { + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + }, + { + accessorKey: 'age', + header: () => 'Age', + }, + { + accessorKey: 'visits', + header: () => 'Visits', + }, + { + accessorKey: 'status', + header: 'Status', + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(50_000) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + enableRowSelection: true, + debugTable: true, + }) + + return { + table, + FlexRender, + refreshData() { + local.data = makeData(50_000) + }, + stressTest() { + local.data = makeData(1_000_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/row-selection/src/makeData.ts b/examples/alpine/row-selection/src/makeData.ts new file mode 100644 index 0000000000..fc070cd5d2 --- /dev/null +++ b/examples/alpine/row-selection/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?: Array +} + +const range = (len: number) => { + const arr: Array = [] + 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: Array) { + const makeDataLevel = (depth = 0): Array => { + 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/alpine/row-selection/src/vite-env.d.ts b/examples/alpine/row-selection/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/row-selection/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/row-selection/tsconfig.json b/examples/alpine/row-selection/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/row-selection/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/row-selection/vite.config.js b/examples/alpine/row-selection/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/row-selection/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/examples/alpine/sorting-dynamic-data/index.html b/examples/alpine/sorting-dynamic-data/index.html new file mode 100644 index 0000000000..4c3be3c306 --- /dev/null +++ b/examples/alpine/sorting-dynamic-data/index.html @@ -0,0 +1,70 @@ + + + + + + TanStack Alpine Table - Sorting Dynamic Data + + +
+
+ + +
+
+
+ + +
+ + + + + + + +
+
+

+    
+ + + diff --git a/examples/alpine/sorting-dynamic-data/package.json b/examples/alpine/sorting-dynamic-data/package.json new file mode 100644 index 0000000000..0da6dd8600 --- /dev/null +++ b/examples/alpine/sorting-dynamic-data/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-sorting-dynamic-data", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/sorting-dynamic-data/src/index.css b/examples/alpine/sorting-dynamic-data/src/index.css new file mode 100644 index 0000000000..4a8ff51c96 --- /dev/null +++ b/examples/alpine/sorting-dynamic-data/src/index.css @@ -0,0 +1,139 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input, +.number-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/sorting-dynamic-data/src/main.ts b/examples/alpine/sorting-dynamic-data/src/main.ts new file mode 100644 index 0000000000..cc0e523b63 --- /dev/null +++ b/examples/alpine/sorting-dynamic-data/src/main.ts @@ -0,0 +1,122 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + createSortedRowModel, + createTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef, SortFn } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const sortStatusFn: SortFn = ( + 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: Array> = [ + { + 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: () => '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: () => 'Visits', + sortUndefined: 'last', // force undefined values to the end + }, + { + accessorKey: 'status', + header: 'Status', + sortFn: 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', + // sortFn: 'datetime' //make sure table knows this is a datetime column (usually can detect if no null values) + }, +] + +// The base dataset. A `visits` multiplier is applied on top of this to +// demonstrate the sorted row model reacting to dynamic data changes. +const baseData: Array = makeData(1_000) + +Alpine.data('table', () => { + const local = Alpine.reactive({ + data: [...baseData], + multiplier: 1, + }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + debugTable: true, + }) + + return { + table, + FlexRender, + // pick the indicator for a column's current sort direction + sortIndicator(isSorted: false | 'asc' | 'desc') { + return { asc: ' 🔼', desc: ' 🔽' }[isSorted as string] ?? '' + }, + // multiply the `visits` value for every row, recomputing the row model + applyMultiplier(value: string) { + local.multiplier = Number(value) || 1 + local.data = baseData.map((d) => ({ + ...d, + visits: d.visits ? d.visits * local.multiplier : undefined, + })) + }, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(1_000_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/sorting-dynamic-data/src/makeData.ts b/examples/alpine/sorting-dynamic-data/src/makeData.ts new file mode 100644 index 0000000000..fc070cd5d2 --- /dev/null +++ b/examples/alpine/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?: Array +} + +const range = (len: number) => { + const arr: Array = [] + 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: Array) { + const makeDataLevel = (depth = 0): Array => { + 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/alpine/sorting-dynamic-data/src/vite-env.d.ts b/examples/alpine/sorting-dynamic-data/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/sorting-dynamic-data/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/sorting-dynamic-data/tsconfig.json b/examples/alpine/sorting-dynamic-data/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/sorting-dynamic-data/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/sorting-dynamic-data/vite.config.js b/examples/alpine/sorting-dynamic-data/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/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/examples/alpine/sorting/index.html b/examples/alpine/sorting/index.html new file mode 100644 index 0000000000..a7a113354d --- /dev/null +++ b/examples/alpine/sorting/index.html @@ -0,0 +1,58 @@ + + + + + + TanStack Alpine Table - Sorting + + +
+
+ + +
+ + + + + + + +
+
+

+    
+ + + diff --git a/examples/alpine/sorting/package.json b/examples/alpine/sorting/package.json new file mode 100644 index 0000000000..c79d753af0 --- /dev/null +++ b/examples/alpine/sorting/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-sorting", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/sorting/src/index.css b/examples/alpine/sorting/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/sorting/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/sorting/src/main.ts b/examples/alpine/sorting/src/main.ts new file mode 100644 index 0000000000..3e2ba07855 --- /dev/null +++ b/examples/alpine/sorting/src/main.ts @@ -0,0 +1,107 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + createSortedRowModel, + createTable, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef, SortFn } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const sortStatusFn: SortFn = ( + 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: Array> = [ + { + 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: () => '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: () => 'Visits', + sortUndefined: 'last', // force undefined values to the end + }, + { + accessorKey: 'status', + header: 'Status', + sortFn: 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', + // sortFn: 'datetime' //make sure table knows this is a datetime column (usually can detect if no null values) + }, +] + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(1_000) }) + + const table = createTable({ + features, + columns, + get data() { + return local.data + }, + debugTable: true, + }) + + return { + table, + FlexRender, + // pick the indicator for a column's current sort direction + sortIndicator(isSorted: false | 'asc' | 'desc') { + return { asc: ' 🔼', desc: ' 🔽' }[isSorted as string] ?? '' + }, + refreshData() { + local.data = makeData(1_000) + }, + stressTest() { + local.data = makeData(100_000) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/sorting/src/makeData.ts b/examples/alpine/sorting/src/makeData.ts new file mode 100644 index 0000000000..fc070cd5d2 --- /dev/null +++ b/examples/alpine/sorting/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?: Array +} + +const range = (len: number) => { + const arr: Array = [] + 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: Array) { + const makeDataLevel = (depth = 0): Array => { + 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/alpine/sorting/src/vite-env.d.ts b/examples/alpine/sorting/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/sorting/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/sorting/tsconfig.json b/examples/alpine/sorting/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/sorting/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/sorting/vite.config.js b/examples/alpine/sorting/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/sorting/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/examples/alpine/sub-components/index.html b/examples/alpine/sub-components/index.html new file mode 100644 index 0000000000..20f5bbbfdf --- /dev/null +++ b/examples/alpine/sub-components/index.html @@ -0,0 +1,86 @@ + + + + + + TanStack Alpine Table - Sub Components + + +
+
+ + +
+
+ + + + + +
+
+
+ + Rows +
+
+ + + diff --git a/examples/alpine/sub-components/package.json b/examples/alpine/sub-components/package.json new file mode 100644 index 0000000000..5fb3f73ac4 --- /dev/null +++ b/examples/alpine/sub-components/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-alpine-table-example-sub-components", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/alpine-table": "^9.0.0-beta.23", + "alpinejs": "^3.15.12" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/alpinejs": "^3.13.11", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/alpine/sub-components/src/index.css b/examples/alpine/sub-components/src/index.css new file mode 100644 index 0000000000..de3acbaf1b --- /dev/null +++ b/examples/alpine/sub-components/src/index.css @@ -0,0 +1,133 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +* { + box-sizing: border-box; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Demo layout helpers for the plain example UI. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.filter-row, +.page-controls { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.controls, +.page-controls { + gap: 0.5rem; + flex-wrap: wrap; +} + +.inline-controls { + gap: 0.25rem; +} + +.filter-row { + gap: 0.5rem; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.page-size-input, +.filter-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.filter-input { + width: 6rem; +} diff --git a/examples/alpine/sub-components/src/main.ts b/examples/alpine/sub-components/src/main.ts new file mode 100644 index 0000000000..734bb97958 --- /dev/null +++ b/examples/alpine/sub-components/src/main.ts @@ -0,0 +1,100 @@ +import Alpine from 'alpinejs' +import { + FlexRender, + createExpandedRowModel, + createTable, + rowExpandingFeature, + tableFeatures, +} from '@tanstack/alpine-table' +import { makeData } from './makeData' +import './index.css' +import type { ColumnDef, Row } from '@tanstack/alpine-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), +}) + +// The `expander` column renders an interactive expand button, and the +// `firstName` cell renders a depth-indented value. Because Alpine cannot process +// directives inside `x-html`, those are rendered directly in `index.html` +// (special-cased by column id), so here those columns expose plain values. +const columns: Array> = [ + { + id: 'expander', + header: () => '', + cell: () => '', + }, + { + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: () => 'Last Name', + footer: (props) => props.column.id, + }, + { + accessorKey: 'age', + header: () => 'Age', + footer: (props) => props.column.id, + }, + { + accessorKey: 'visits', + header: () => 'Visits', + footer: (props) => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: (props) => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + }, +] + +// Renders the expanded detail panel for a row (mirrors the Lit sub-component). +function renderSubComponent(row: Row) { + return `
${JSON.stringify(
+    row.original,
+    null,
+    2,
+  )}
` +} + +Alpine.data('table', () => { + const local = Alpine.reactive({ data: makeData(10, 5) }) + + const table = createTable({ + debugTable: true, + features, + columns, + get data() { + return local.data + }, + getRowCanExpand: () => true, + }) + + return { + table, + FlexRender, + renderSubComponent, + refreshData() { + local.data = makeData(10, 5) + }, + stressTest() { + local.data = makeData(100, 5) + }, + } +}) + +window.Alpine = Alpine +Alpine.start() diff --git a/examples/alpine/sub-components/src/makeData.ts b/examples/alpine/sub-components/src/makeData.ts new file mode 100644 index 0000000000..d63c724b74 --- /dev/null +++ b/examples/alpine/sub-components/src/makeData.ts @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) arr.push(i) + return arr +} + +const newPerson = (): Person => ({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], +}) + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map( + (): Person => ({ + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + }), + ) + } + return makeDataLevel() +} diff --git a/examples/alpine/sub-components/src/vite-env.d.ts b/examples/alpine/sub-components/src/vite-env.d.ts new file mode 100644 index 0000000000..99f6333028 --- /dev/null +++ b/examples/alpine/sub-components/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +import type Alpine from 'alpinejs' + +declare global { + interface Window { + Alpine: typeof Alpine + } +} diff --git a/examples/alpine/sub-components/tsconfig.json b/examples/alpine/sub-components/tsconfig.json new file mode 100644 index 0000000000..5b0a1fed9c --- /dev/null +++ b/examples/alpine/sub-components/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true + }, + "include": ["src", "vite.config.js", "vite.config.ts"] +} diff --git a/examples/alpine/sub-components/vite.config.js b/examples/alpine/sub-components/vite.config.js new file mode 100644 index 0000000000..fa3b238ac6 --- /dev/null +++ b/examples/alpine/sub-components/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/package.json b/package.json index 44db922b4c..9cebe82480 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clean": "find . -name 'dist' -type d -prune -exec rm -rf {} +", "clean:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf {} +", "dev": "pnpm run watch", - "copy:readme": "cp README.md packages/table-core/README.md && cp README.md packages/table-devtools/README.md && cp README.md packages/react-table/README.md && cp README.md packages/react-table-devtools/README.md && cp README.md packages/preact-table/README.md && cp README.md packages/preact-table-devtools/README.md && cp README.md packages/angular-table/README.md && cp README.md packages/solid-table/README.md && cp README.md packages/solid-table-devtools/README.md && cp README.md packages/vue-table/README.md && cp README.md packages/vue-table-devtools/README.md && cp README.md packages/lit-table/README.md && cp README.md packages/svelte-table/README.md && cp README.md packages/match-sorter-utils/README.md", + "copy:readme": "cp README.md packages/table-core/README.md && cp README.md packages/table-devtools/README.md && cp README.md packages/react-table/README.md && cp README.md packages/react-table-devtools/README.md && cp README.md packages/preact-table/README.md && cp README.md packages/preact-table-devtools/README.md && cp README.md packages/angular-table/README.md && cp README.md packages/solid-table/README.md && cp README.md packages/solid-table-devtools/README.md && cp README.md packages/vue-table/README.md && cp README.md packages/vue-table-devtools/README.md && cp README.md packages/lit-table/README.md && cp README.md packages/svelte-table/README.md && cp README.md packages/match-sorter-utils/README.md && cp README.md packages/alpine-table/README.md", "generate-docs": "node scripts/generateDocs.js && pnpm run copy:readme", "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", "lint:fix:all": "pnpm run format && nx run-many --targets=lint --fix", @@ -76,5 +76,23 @@ "typescript": "6.0.3", "vite": "^8.1.0", "vitest": "^4.1.9" + }, + "overrides": { + "@tanstack/alpine-table": "workspace:*", + "@tanstack/angular-table": "workspace:*", + "@tanstack/angular-table-devtools": "workspace:*", + "@tanstack/lit-table": "workspace:*", + "@tanstack/match-sorter-utils": "workspace:*", + "@tanstack/preact-table": "workspace:*", + "@tanstack/preact-table-devtools": "workspace:*", + "@tanstack/react-table": "workspace:*", + "@tanstack/react-table-devtools": "workspace:*", + "@tanstack/solid-table": "workspace:*", + "@tanstack/solid-table-devtools": "workspace:*", + "@tanstack/svelte-table": "workspace:*", + "@tanstack/table-core": "workspace:*", + "@tanstack/table-devtools": "workspace:*", + "@tanstack/vue-table": "workspace:*", + "@tanstack/vue-table-devtools": "workspace:*" } } diff --git a/packages/alpine-table/README.md b/packages/alpine-table/README.md new file mode 100644 index 0000000000..689cb0a7df --- /dev/null +++ b/packages/alpine-table/README.md @@ -0,0 +1,128 @@ +
+ TanStack Table +
+ +
+ +
+ + npm downloads + + + github stars + + + bundle size + +
+ +
+ + semantic-release + +Best of JS + Follow @TanStack +
+ +### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) + + + +# TanStack Table + +> [!NOTE] +> You may know TanStack Table by the adapter names: +> +> - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) +> - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) +> - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) +> - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) +> - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) + +A headless table library for building powerful datagrids with full control over markup, styles, and behavior. + +- Framework‑agnostic core with bindings for React, Vue & Solid +- 100% customizable — bring your own UI, components, and styles +- Sorting, filtering, grouping, aggregation & row selection +- Lightweight, virtualizable & server‑side friendly + +### Read the Docs → + +## Using an AI Coding Agent? + +TanStack Table ships [TanStack Intent](https://github.com/TanStack/intent) skills inside each adapter package. After installing the library, run: + +```sh +npx @tanstack/intent@latest install +``` + +to add skill-loading guidance for your agent (Claude Code, Cursor, Copilot, etc.). The same CLI also exposes `intent list` to browse available skills and `intent load ` to print one for inspection. Skills version with the library — your agent gets guidance that matches the version of `@tanstack/-table` you installed. Only available for v9 and above. + +## Get Involved + +- We welcome issues and pull requests! +- Participate in [GitHub discussions](https://github.com/TanStack/table/discussions) +- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ) +- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions + +## Partners + + + + + + + +
+ + + + + CodeRabbit + + + + + + + + Cloudflare + + + + + + + + AG Grid + + +
+ +
+Table & you? +

+We're looking for TanStack Table Partners to join our mission! Partner with us to push the boundaries of TanStack Table and build amazing things together. +

+LET'S CHAT +
+ +## Explore the TanStack Ecosystem + +- TanStack Config – Tooling for JS/TS packages +- TanStack DB – Reactive sync client store +- TanStack DevTools – Unified devtools panel +- TanStack Form – Type‑safe form state +- TanStack Pacer – Debouncing, throttling, batching
+- TanStack Query – Async state & caching +- TanStack Ranger – Range & slider primitives +- TanStack Router – Type‑safe routing, caching & URL state +- TanStack Start – Full‑stack SSR & streaming +- TanStack Store – Reactive data store +- TanStack Virtual – Virtualized rendering + +… and more at TanStack.com » + + diff --git a/packages/alpine-table/eslint.config.js b/packages/alpine-table/eslint.config.js new file mode 100644 index 0000000000..e472c69e73 --- /dev/null +++ b/packages/alpine-table/eslint.config.js @@ -0,0 +1,10 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + rules: {}, + }, +] diff --git a/packages/alpine-table/package.json b/packages/alpine-table/package.json new file mode 100644 index 0000000000..10156014d5 --- /dev/null +++ b/packages/alpine-table/package.json @@ -0,0 +1,68 @@ +{ + "name": "@tanstack/alpine-table", + "version": "9.0.0-beta.23", + "description": "Headless UI for building powerful tables & datagrids for Alpine.", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/table.git", + "directory": "packages/alpine-table" + }, + "homepage": "https://tanstack.com/table", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "alpine", + "table", + "alpine-table", + "datagrid" + ], + "type": "module", + "types": "./dist/index.d.cts", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./flex-render": { + "import": "./dist/flex-render.js", + "require": "./dist/flex-render.cjs" + }, + "./static-functions": { + "import": "./dist/static-functions.js", + "require": "./dist/static-functions.cjs" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "engines": { + "node": ">=16" + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "clean": "rimraf ./build && rimraf ./dist", + "test:eslint": "eslint ./src", + "test:types": "tsc", + "test:build": "publint --strict", + "build": "tsdown" + }, + "dependencies": { + "@tanstack/store": "^0.11.0", + "@tanstack/table-core": "workspace:*" + }, + "devDependencies": { + "@types/alpinejs": "^3.13.11", + "alpinejs": "^3.15.12" + }, + "peerDependencies": { + "alpinejs": "^3.15.12" + } +} diff --git a/packages/alpine-table/src/createTable.ts b/packages/alpine-table/src/createTable.ts new file mode 100644 index 0000000000..6aea8d17e5 --- /dev/null +++ b/packages/alpine-table/src/createTable.ts @@ -0,0 +1,148 @@ +import Alpine from 'alpinejs' +import { constructTable } from '@tanstack/table-core' +import { FlexRender, flexRender } from './flexRender' +import { alpineReactivity } from './reactivity' +import type { + RowData, + Table, + TableFeatures, + TableOptions, +} from '@tanstack/table-core' + +export type AlpineTable< + TFeatures extends TableFeatures, + TData extends RowData, +> = Table & { + /** + * A lower-level helper to render the content of a cell, header, or footer from a render function and its context. + */ + flexRender: typeof flexRender + + /** + * A convenience helper to render a cell, header, or footer object. Call from `x-html`, e.g. `FlexRender({ header })`. + */ + FlexRender: typeof FlexRender +} + +export function createTable< + TFeatures extends TableFeatures, + TData extends RowData, +>(tableOptions: TableOptions): AlpineTable { + const mergedOptions: TableOptions = { + ...tableOptions, + features: { + coreReactivityFeature: alpineReactivity(), + ...tableOptions.features, + }, + mergeOptions: ( + defaultOptions: TableOptions, + newOptions: Partial>, + ) => { + return { + ...defaultOptions, + ...newOptions, + } + }, + } + + const table = constructTable(mergedOptions) as unknown as AlpineTable< + TFeatures, + TData + > + + table.flexRender = flexRender + table.FlexRender = FlexRender + + const reactivity = Alpine.reactive({ _ver: 0 }) + + table.store.subscribe(() => { + reactivity._ver++ + }) + + // Reactively sync options when external Alpine-reactive getters change (e.g. + // a `get data()` backed by `Alpine.reactive`). Reading the option getters + // inside the effect registers the dependencies, so the effect re-runs when + // they change and re-applies the live values via `setOptions`. + // + // `setOptions` writes to the options store, not the state store, so a `data` + // (or other option) change does not emit on `table.store` and would not bump + // `_ver` on its own. We bump `_ver` here so the template re-pulls derived + // APIs like `getRowModel()`, which recompute from the new options. The effect + // never reads `_ver`, so writing it does not re-trigger this effect. + let initialized = false + Alpine.effect(() => { + const state = tableOptions.state as Record | undefined + if (state) { + for (const key in state) { + void state[key] + } + } + void tableOptions.data + + table.setOptions((prev) => ({ + ...prev, + ...tableOptions, + })) + + if (initialized) { + reactivity._ver++ + } + initialized = true + }) + + const proxyCache = new WeakMap() + + const toReactiveProxy = (value: TValue): TValue => { + if (typeof value !== 'object' || value === null) { + return value + } + + // Built-in exotic objects (Map, Set, Date, etc.) rely on internal slots and + // throw "incompatible receiver" when their getters/methods run with a Proxy + // as the receiver (e.g. `getFacetedUniqueValues().size`). Return them as-is; + // the read that produced them already tracked `_ver` at the call site. + if ( + value instanceof Map || + value instanceof Set || + value instanceof WeakMap || + value instanceof WeakSet || + value instanceof Date || + value instanceof RegExp || + value instanceof Promise + ) { + return value + } + + const cachedProxy = proxyCache.get(value) + if (cachedProxy) { + return cachedProxy as TValue + } + + const proxy = new Proxy(value, { + get(target, prop, receiver) { + if (prop === '__v_skip') { + return true + } + + const resolvedValue = Reflect.get(target, prop, receiver) + + if (typeof resolvedValue === 'function') { + return (...args: Array) => { + void reactivity._ver + return toReactiveProxy( + (resolvedValue as Function).apply(target, args), + ) + } + } + + void reactivity._ver + return toReactiveProxy(resolvedValue) + }, + }) + + proxyCache.set(value, proxy) + return proxy + } + + return toReactiveProxy(table) +} diff --git a/packages/alpine-table/src/createTableHook.ts b/packages/alpine-table/src/createTableHook.ts new file mode 100644 index 0000000000..894030a6ea --- /dev/null +++ b/packages/alpine-table/src/createTableHook.ts @@ -0,0 +1,48 @@ +import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core' +import { createTable } from './createTable' +import type { AlpineTable } from './createTable' +import type { RowData, TableFeatures, TableOptions } from '@tanstack/table-core' + +export type CreateTableHookOptions = Omit< + TableOptions, + 'columns' | 'data' | 'state' +> + +export type AppAlpineTable< + TFeatures extends TableFeatures, + TData extends RowData, +> = AlpineTable + +export type AppColumnHelper< + TFeatures extends TableFeatures, + TData extends RowData, +> = ReturnType> + +export function createTableHook({ + ...defaultTableOptions +}: CreateTableHookOptions) { + function createAppColumnHelper(): AppColumnHelper< + TFeatures, + TData + > { + return coreCreateColumnHelper() + } + + function createAppTable( + tableOptions: Omit, 'features'>, + ): AppAlpineTable { + // Merge default options with provided options (provided takes precedence) + const mergedOptions = { + ...defaultTableOptions, + ...tableOptions, + } as TableOptions + + return createTable(mergedOptions) + } + + return { + appFeatures: defaultTableOptions.features as TFeatures, + createAppColumnHelper, + createAppTable, + } +} diff --git a/packages/alpine-table/src/flex-render.ts b/packages/alpine-table/src/flex-render.ts new file mode 100644 index 0000000000..791821b61e --- /dev/null +++ b/packages/alpine-table/src/flex-render.ts @@ -0,0 +1 @@ +export * from './flexRender' diff --git a/packages/alpine-table/src/flexRender.ts b/packages/alpine-table/src/flexRender.ts new file mode 100644 index 0000000000..095bf9fcb7 --- /dev/null +++ b/packages/alpine-table/src/flexRender.ts @@ -0,0 +1,100 @@ +import type { + Cell, + CellData, + Header, + RowData, + TableFeatures, +} from '@tanstack/table-core' + +/** + * Renders an Alpine table value with the provided context props. + * + * Use this lower-level helper for custom header, cell, or footer renderers when + * you already have the render function and context. `FlexRender` is the + * convenience wrapper for table cell/header/footer objects. Renderers typically + * return a string of markup that you render into the DOM with `x-html`. + * + * @example + * ```ts + * flexRender(cell.column.columnDef.cell, cell.getContext()) + * ``` + */ +export function flexRender( + render: any, + props: TProps, +): any { + if (typeof render === 'function') { + return render(props) + } + return render +} + +/** + * Simplified wrapper of `flexRender`. Use this utility function to render headers, cells, or footers with custom markup. + * Only one prop (`cell`, `header`, or `footer`) may be passed. + * @example + * ```html + * + * + * + * ``` + * + * This replaces calling `flexRender` directly like this: + * ```ts + * flexRender(cell.column.columnDef.cell, cell.getContext()) + * flexRender(header.column.columnDef.header, header.getContext()) + * flexRender(footer.column.columnDef.footer, footer.getContext()) + * ``` + */ +export type FlexRenderProps< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, +> = + | { cell: Cell; header?: never; footer?: never } + | { + header: Header + cell?: never + footer?: never + } + | { + footer: Header + cell?: never + header?: never + } + +/** + * Simplified wrapper of `flexRender`. Use this utility function to render headers, cells, or footers with custom markup. + * Only one prop (`cell`, `header`, or `footer`) may be passed. + * @example + * ```html + * + * + * + * ``` + */ +export function FlexRender< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, +>(props: FlexRenderProps): any { + if ('cell' in props && props.cell) { + return flexRender(props.cell.column.columnDef.cell, props.cell.getContext()) + } + + if ('header' in props && props.header) { + return flexRender( + props.header.column.columnDef.header, + props.header.getContext(), + ) + } + + if ('footer' in props && props.footer) { + return flexRender( + props.footer.column.columnDef.footer, + props.footer.getContext(), + ) + } + + return null +} diff --git a/packages/alpine-table/src/index.ts b/packages/alpine-table/src/index.ts new file mode 100644 index 0000000000..4e92d00c34 --- /dev/null +++ b/packages/alpine-table/src/index.ts @@ -0,0 +1,5 @@ +export * from '@tanstack/table-core' + +export * from './flexRender' +export * from './createTable' +export * from './createTableHook' diff --git a/packages/alpine-table/src/reactivity.ts b/packages/alpine-table/src/reactivity.ts new file mode 100644 index 0000000000..2e94e93045 --- /dev/null +++ b/packages/alpine-table/src/reactivity.ts @@ -0,0 +1,41 @@ +import { batch, createAtom } from '@tanstack/store' +import type { + TableAtomOptions, + TableReactivityBindings, +} from '@tanstack/table-core/reactivity' + +/** + * Creates the table-core reactivity bindings used by the Alpine adapter. + * + * Alpine uses TanStack Store atoms directly. Table instance reads + * are then exposed to Alpine through the proxy wrapper in `createTable`. + */ +export function alpineReactivity(): TableReactivityBindings { + return { + createOptionsStore: true, + wrapExternalAtoms: false, + addSubscription: () => { + throw new Error( + 'Feature not supported in current reactivity implementation', + ) + }, + unmount: () => { + throw new Error( + 'Feature not supported in current reactivity implementation', + ) + }, + schedule: (fn) => queueMicrotask(() => fn()), + batch, + untrack: (fn) => fn(), + createReadonlyAtom: (fn: () => T, options?: TableAtomOptions) => { + return createAtom(() => fn(), { + compare: options?.compare, + }) + }, + createWritableAtom: (value: T, options?: TableAtomOptions) => { + return createAtom(value, { + compare: options?.compare, + }) + }, + } +} diff --git a/packages/alpine-table/src/static-functions.ts b/packages/alpine-table/src/static-functions.ts new file mode 100644 index 0000000000..e0cb951593 --- /dev/null +++ b/packages/alpine-table/src/static-functions.ts @@ -0,0 +1 @@ +export * from '@tanstack/table-core/static-functions' diff --git a/packages/alpine-table/tsconfig.json b/packages/alpine-table/tsconfig.json new file mode 100644 index 0000000000..eb63835950 --- /dev/null +++ b/packages/alpine-table/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "eslint.config.js", "vite.config.ts"] +} diff --git a/packages/alpine-table/tsdown.config.ts b/packages/alpine-table/tsdown.config.ts new file mode 100644 index 0000000000..c305d9b999 --- /dev/null +++ b/packages/alpine-table/tsdown.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: [ + './src/index.ts', + './src/static-functions.ts', + './src/flex-render.ts', + ], + format: ['esm', 'cjs'], + unbundle: true, + dts: true, + sourcemap: true, + clean: true, + minify: false, + fixedExtension: false, + exports: true, + publint: { + strict: true, + }, +}) diff --git a/packages/alpine-table/vite.config.ts b/packages/alpine-table/vite.config.ts new file mode 100644 index 0000000000..abed6b2116 --- /dev/null +++ b/packages/alpine-table/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({}) diff --git a/packages/angular-table/README.md b/packages/angular-table/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/angular-table/README.md +++ b/packages/angular-table/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/lit-table/README.md b/packages/lit-table/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/lit-table/README.md +++ b/packages/lit-table/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/match-sorter-utils/README.md b/packages/match-sorter-utils/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/match-sorter-utils/README.md +++ b/packages/match-sorter-utils/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/preact-table-devtools/README.md b/packages/preact-table-devtools/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/preact-table-devtools/README.md +++ b/packages/preact-table-devtools/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/preact-table/README.md b/packages/preact-table/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/preact-table/README.md +++ b/packages/preact-table/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/react-table-devtools/README.md b/packages/react-table-devtools/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/react-table-devtools/README.md +++ b/packages/react-table-devtools/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/react-table/README.md b/packages/react-table/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/react-table/README.md +++ b/packages/react-table/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/solid-table-devtools/README.md b/packages/solid-table-devtools/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/solid-table-devtools/README.md +++ b/packages/solid-table-devtools/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/solid-table/README.md b/packages/solid-table/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/solid-table/README.md +++ b/packages/solid-table/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/svelte-table/README.md b/packages/svelte-table/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/svelte-table/README.md +++ b/packages/svelte-table/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/table-core/README.md b/packages/table-core/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/table-core/README.md +++ b/packages/table-core/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/table-devtools/README.md b/packages/table-devtools/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/table-devtools/README.md +++ b/packages/table-devtools/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/vue-table-devtools/README.md b/packages/vue-table-devtools/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/vue-table-devtools/README.md +++ b/packages/vue-table-devtools/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/packages/vue-table/README.md b/packages/vue-table/README.md index e82fa87596..689cb0a7df 100644 --- a/packages/vue-table/README.md +++ b/packages/vue-table/README.md @@ -39,6 +39,7 @@ > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) > - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) A headless table library for building powerful datagrids with full control over markup, styles, and behavior. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d725b92e23..ab7b5ea595 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: '@types/react': 19.2.16 + '@tanstack/alpine-table': workspace:* '@tanstack/angular-table-devtools': workspace:* '@tanstack/angular-table': workspace:* '@tanstack/lit-table': workspace:* @@ -101,6 +102,606 @@ importers: specifier: ^4.1.9 version: 4.1.9(@types/node@26.0.0)(jsdom@29.1.1)(vite@8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0)) + examples/alpine/basic-app-table: + dependencies: + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/basic-create-table: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/basic-external-atoms: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + '@tanstack/store': + specifier: ^0.11.0 + version: 0.11.0 + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/basic-external-state: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/column-groups: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/column-ordering: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/column-pinning: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/column-pinning-split: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/column-pinning-sticky: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/column-resizing: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/column-resizing-performant: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/column-sizing: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/column-visibility: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/custom-plugin: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/expanding: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/filters: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/filters-faceted: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/grouping: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/pagination: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/row-pinning: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/row-selection: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/sorting: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/sorting-dynamic-data: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + + examples/alpine/sub-components: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/alpine-table': + specifier: workspace:* + version: link:../../../packages/alpine-table + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + devDependencies: + '@rollup/plugin-replace': + specifier: ^6.0.3 + version: 6.0.3(rollup@4.61.1) + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + examples/angular/basic-app-table: dependencies: '@angular/common': @@ -8847,6 +9448,22 @@ importers: specifier: ^3.3.5 version: 3.3.5(typescript@6.0.3) + packages/alpine-table: + dependencies: + '@tanstack/store': + specifier: ^0.11.0 + version: 0.11.0 + '@tanstack/table-core': + specifier: workspace:* + version: link:../table-core + devDependencies: + '@types/alpinejs': + specifier: ^3.13.11 + version: 3.13.11 + alpinejs: + specifier: ^3.15.12 + version: 3.15.12 + packages/angular-table: dependencies: '@tanstack/angular-store': @@ -14181,6 +14798,9 @@ packages: '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/alpinejs@3.13.11': + resolution: {integrity: sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -14654,6 +15274,9 @@ packages: '@vue/language-core@3.3.5': resolution: {integrity: sha512-UkKu5nhX89fg4VhlG/FOeI10G3cj/7radKT/cy9BT4Q9qJmJlSTAc/dP63Xqs29aypN4f39xUV6PsLNk/dcD6g==} + '@vue/reactivity@3.1.5': + resolution: {integrity: sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==} + '@vue/reactivity@3.5.38': resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==} @@ -14668,6 +15291,9 @@ packages: peerDependencies: vue: 3.5.38 + '@vue/shared@3.1.5': + resolution: {integrity: sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==} + '@vue/shared@3.5.35': resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} @@ -14806,6 +15432,9 @@ packages: alien-signals@3.2.1: resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + alpinejs@3.15.12: + resolution: {integrity: sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -24461,6 +25090,8 @@ snapshots: dependencies: tslib: 2.8.1 + '@types/alpinejs@3.13.11': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -25058,6 +25689,10 @@ snapshots: path-browserify: 1.0.1 picomatch: 4.0.4 + '@vue/reactivity@3.1.5': + dependencies: + '@vue/shared': 3.1.5 + '@vue/reactivity@3.5.38': dependencies: '@vue/shared': 3.5.38 @@ -25080,6 +25715,8 @@ snapshots: '@vue/shared': 3.5.38 vue: 3.5.38(typescript@6.0.3) + '@vue/shared@3.1.5': {} + '@vue/shared@3.5.35': {} '@vue/shared@3.5.38': {} @@ -25247,6 +25884,10 @@ snapshots: alien-signals@3.2.1: {} + alpinejs@3.15.12: + dependencies: + '@vue/reactivity': 3.1.5 + ansi-colors@4.1.3: {} ansi-escapes@7.3.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 43c2fc97b4..28e7b1bb04 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,7 @@ packages: overrides: '@types/react': 19.2.16 + '@tanstack/alpine-table': 'workspace:*' '@tanstack/angular-table-devtools': 'workspace:*' '@tanstack/angular-table': 'workspace:*' '@tanstack/lit-table': 'workspace:*' diff --git a/scripts/config.js b/scripts/config.js index a903cf15a2..f6ba085cba 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -16,6 +16,10 @@ export const packages = [ name: '@tanstack/table-devtools', packageDir: 'packages/table-devtools', }, + { + name: '@tanstack/alpine-table', + packageDir: 'packages/alpine-table', + }, { name: '@tanstack/angular-table', packageDir: 'packages/angular-table', diff --git a/scripts/generateDocs.js b/scripts/generateDocs.js index 6c86b2e087..48d288001a 100644 --- a/scripts/generateDocs.js +++ b/scripts/generateDocs.js @@ -75,6 +75,15 @@ await generateReferenceDocs({ outputDir: resolve(__dirname, '../docs/framework/lit/reference'), exclude: ['packages/table-core/**/*'], }, + { + name: 'alpine-table', + entryPoints: [ + resolve(__dirname, '../packages/alpine-table/src/index.ts'), + ], + tsconfig: resolve(__dirname, '../packages/alpine-table/tsconfig.json'), + outputDir: resolve(__dirname, '../docs/framework/alpine/reference'), + exclude: ['packages/table-core/**/*'], + }, ], }) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index d0e50db85e..cc9846e6a2 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -1,4 +1,5 @@ export default [ + './packages/alpine-table/vite.config.ts', './packages/angular-table/vite.config.ts', './packages/lit-table/vite.config.ts', './packages/match-sorter-utils/vite.config.ts',