├── .npmrc ├── .husky └── pre-commit ├── src ├── routes │ ├── +layout.ts │ ├── app.css │ ├── examples │ │ ├── row-selection │ │ │ ├── _components │ │ │ │ └── table-checkbox.svelte │ │ │ ├── +page.ts │ │ │ ├── +page.svelte │ │ │ └── README.md │ │ ├── basic │ │ │ ├── +page.ts │ │ │ ├── +page.svelte │ │ │ └── README.md │ │ ├── grid-view │ │ │ ├── +page.ts │ │ │ ├── +page.svelte │ │ │ └── README.md │ │ ├── rendering-snippets │ │ │ ├── +page.ts │ │ │ ├── +page.svelte │ │ │ └── README.md │ │ ├── rendering-components │ │ │ ├── +page.ts │ │ │ ├── _components │ │ │ │ └── data-table-cell-countdown.svelte │ │ │ ├── +page.svelte │ │ │ └── README.md │ │ ├── reactive-data │ │ │ ├── +page.ts │ │ │ ├── +page.svelte │ │ │ └── README.md │ │ ├── row-expansion │ │ │ ├── _components │ │ │ │ └── expand-button.svelte │ │ │ ├── +page.ts │ │ │ ├── +page.svelte │ │ │ └── README.md │ │ ├── +layout.ts │ │ └── +layout.svelte │ ├── api │ │ └── user-profile │ │ │ └── [n] │ │ │ └── +server.ts │ ├── +layout.svelte │ └── +page.svelte ├── lib │ ├── index.ts │ ├── table │ │ ├── index.ts │ │ ├── flex-render.svelte │ │ ├── table.svelte.ts │ │ ├── render-component.ts │ │ └── README.md │ ├── highlight.ts │ ├── components │ │ ├── highlight.svelte │ │ └── mode-button.svelte │ └── services │ │ ├── user-profile.ts │ │ └── example-registry.ts ├── app.html ├── app.d.ts ├── styles │ ├── shiki.css │ ├── sakura.css │ └── normalize.css └── vite-plugins │ └── markdown.ts ├── .prettierignore ├── static └── favicon.png ├── .prettierrc ├── .gitignore ├── .lintstagedrc.yaml ├── vite.config.ts ├── tsconfig.json ├── svelte.config.js ├── eslint.config.js ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm run check 2 | pnpx lint-staged 3 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walker-tx/svelte5-tanstack-table-reference/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/lib/table/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@tanstack/table-core'; 2 | export { default as FlexRender } from './flex-render.svelte'; 3 | export { renderComponent, renderSnippet } from './render-component'; 4 | export { createSvelteTable } from './table.svelte'; 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte", "tabWidth": 4 } }] 8 | } 9 | -------------------------------------------------------------------------------- /src/routes/app.css: -------------------------------------------------------------------------------- 1 | a { 2 | border-bottom: 1px solid var(--color-text); 3 | } 4 | 5 | code { 6 | background-color: var(--color-bg-alt); 7 | padding: 0.2em 0.3em; 8 | border-radius: 0.3em; 9 | font-size: 0.9em; 10 | } 11 | 12 | pre { 13 | border-radius: 0.2em 0.3em; 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /src/lib/highlight.ts: -------------------------------------------------------------------------------- 1 | import { type BundledTheme } from 'shiki/themes'; 2 | 3 | export const themes: Record = { 4 | light: 'github-light-default', 5 | dark: 'github-dark-default' 6 | } as const; 7 | 8 | export const themesArray = ['github-light-default', 'github-dark-default']; 9 | -------------------------------------------------------------------------------- /.lintstagedrc.yaml: -------------------------------------------------------------------------------- 1 | '*.ts': 2 | - prettier --write --list-different 3 | - eslint --fix 4 | '*.js': 5 | - prettier --write --list-different 6 | - eslint --fix 7 | '*.svelte': 8 | - prettier --write --list-different 9 | - eslint --fix 10 | '*.json': 11 | - prettier --write --list-different 12 | -------------------------------------------------------------------------------- /src/routes/examples/row-selection/_components/table-checkbox.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | import pkg from './package.json' with { type: 'json' }; 4 | import markdown from './src/vite-plugins/markdown'; 5 | 6 | export default defineConfig({ 7 | plugins: [sveltekit(), markdown], 8 | define: { 9 | __GITHUB_URL__: `"${pkg.homepage}"` 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes/examples/basic/+page.ts: -------------------------------------------------------------------------------- 1 | import type { UserProfile } from '$lib/services/user-profile'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load: PageLoad = async ({ fetch }) => { 5 | const userProfileResponse = await fetch('/api/user-profile/10'); 6 | const userProfileJson: UserProfile[] = await userProfileResponse.json(); 7 | 8 | return { 9 | userProfiles: userProfileJson 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/routes/examples/grid-view/+page.ts: -------------------------------------------------------------------------------- 1 | import type { UserProfile } from '$lib/services/user-profile'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load: PageLoad = async ({ fetch }) => { 5 | const userProfileResponse = await fetch('/api/user-profile/10'); 6 | const userProfileJson: UserProfile[] = await userProfileResponse.json(); 7 | 8 | return { 9 | userProfiles: userProfileJson 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/routes/examples/row-selection/+page.ts: -------------------------------------------------------------------------------- 1 | import type { UserProfile } from '$lib/services/user-profile'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load: PageLoad = async ({ fetch }) => { 5 | const userProfileResponse = await fetch('/api/user-profile/10'); 6 | const userProfileJson: UserProfile[] = await userProfileResponse.json(); 7 | 8 | return { 9 | userProfiles: userProfileJson 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/routes/examples/rendering-snippets/+page.ts: -------------------------------------------------------------------------------- 1 | import type { UserProfile } from '$lib/services/user-profile'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load: PageLoad = async ({ fetch }) => { 5 | const userProfileResponse = await fetch('/api/user-profile/10'); 6 | const userProfileJson: UserProfile[] = await userProfileResponse.json(); 7 | 8 | return { 9 | userProfiles: userProfileJson 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/routes/examples/rendering-components/+page.ts: -------------------------------------------------------------------------------- 1 | import type { UserProfile } from '$lib/services/user-profile'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load: PageLoad = async ({ fetch }) => { 5 | const userProfileResponse = await fetch('/api/user-profile/10'); 6 | const userProfileJson: UserProfile[] = await userProfileResponse.json(); 7 | 8 | return { 9 | userProfiles: userProfileJson 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/routes/examples/reactive-data/+page.ts: -------------------------------------------------------------------------------- 1 | import type { UserProfile } from '$lib/services/user-profile'; 2 | import type { PageLoad } from './$types'; 3 | import readme from './README.md'; 4 | 5 | export const load: PageLoad = async ({ fetch }) => { 6 | const userProfileResponse = await fetch('/api/user-profile/10'); 7 | const userProfileJson: UserProfile[] = await userProfileResponse.json(); 8 | 9 | return { 10 | userProfiles: userProfileJson, 11 | readme 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/routes/api/user-profile/[n]/+server.ts: -------------------------------------------------------------------------------- 1 | import { UserProfileService } from '$lib/services/user-profile'; 2 | import { error, json } from '@sveltejs/kit'; 3 | import type { RequestHandler } from './$types'; 4 | 5 | export const prerender = true; 6 | 7 | export const GET: RequestHandler = async ({ params }) => { 8 | const n = parseInt(params.n, 10); 9 | 10 | if (isNaN(n)) error(400, `Expected a number, but received '${params.n}'`); 11 | 12 | const profiles = UserProfileService.list(n); 13 | 14 | return json(profiles); 15 | }; 16 | -------------------------------------------------------------------------------- /src/routes/examples/row-expansion/_components/expand-button.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | 12 | type EmptyObject = Record; 13 | 14 | type PageProps = { 15 | data: TPageData; 16 | }; 17 | 18 | declare module '*.md' { 19 | export default string; 20 | } 21 | 22 | // Defined in vite.config.ts under "define" 23 | declare const __GITHUB_URL__: string; 24 | } 25 | 26 | export {}; 27 | -------------------------------------------------------------------------------- /src/routes/examples/row-expansion/+page.ts: -------------------------------------------------------------------------------- 1 | import type { UserProfile } from '$lib/services/user-profile'; 2 | import type { PageLoad } from './$types'; 3 | 4 | export const load: PageLoad = async ({ fetch }) => { 5 | const userProfileResponse = await fetch('/api/user-profile/10'); 6 | const userProfileJson: UserProfile[] = await userProfileResponse.json(); 7 | 8 | // This is hacky, but it will work with prerendering 9 | for (const profile of userProfileJson) { 10 | const friendsResponse = await fetch(`/api/user-profile/5`); 11 | profile.friends = await friendsResponse.json(); 12 | } 13 | 14 | return { 15 | userProfiles: userProfileJson 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 2 | import adapter from '@sveltejs/adapter-static'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /src/lib/components/highlight.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | {#await highlight(code)} 23 |
{code}
24 | {:then highlightedCode} 25 | 26 | {@html highlightedCode} 27 | {/await} 28 | -------------------------------------------------------------------------------- /src/lib/components/mode-button.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | 16 | 30 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | Tanstack Table v8 + Svelte 5 Reference 14 | 15 | 16 | {#if browser} 17 | 18 | {/if} 19 | 20 |
21 | {@render children()} 22 |
23 | 24 | 38 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import prettier from 'eslint-config-prettier'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | eslint.configs.recommended, 9 | ...tseslint.configs.recommended, 10 | ...svelte.configs['flat/recommended'], 11 | prettier, 12 | ...svelte.configs['flat/prettier'], 13 | { 14 | languageOptions: { 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node 18 | } 19 | } 20 | }, 21 | { 22 | files: ['**/*.svelte'], 23 | languageOptions: { 24 | parserOptions: { 25 | parser: tseslint.parser, 26 | svelteFeatures: { 27 | experimentalGenerics: true 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | rules: { 34 | '@typescript-eslint/no-explicit-any': 'off', 35 | 'no-undef': 'off' 36 | } 37 | }, 38 | { 39 | ignores: ['build/', '.svelte-kit/', 'dist/'] 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /src/routes/examples/+layout.ts: -------------------------------------------------------------------------------- 1 | import exampleRegistry from '$lib/services/example-registry'; 2 | import type { LayoutLoad } from './$types'; 3 | 4 | type ExampleEntry = (typeof exampleRegistry)[number]; 5 | 6 | export const load: LayoutLoad = async ({ url }) => { 7 | const exampleIndex = exampleRegistry.findIndex((entry) => entry.pathname === url.pathname); 8 | 9 | if (exampleIndex === -1) { 10 | throw Error(`Example not found: ${url.pathname}`); 11 | } 12 | 13 | const currentExample: ExampleEntry = exampleRegistry[exampleIndex]; 14 | const nextExample: ExampleEntry | undefined = exampleRegistry[exampleIndex + 1]; 15 | const previousExample: ExampleEntry | undefined = exampleRegistry[exampleIndex - 1]; 16 | 17 | const globs = import.meta.glob<{ default: string }>(`./*/README.md`); 18 | const exampleGlob = await globs[`./${currentExample.id}/README.md`](); 19 | 20 | return { 21 | nextExample, 22 | previousExample, 23 | currentExample: exampleRegistry[exampleIndex], 24 | readmeHtml: exampleGlob.default 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/lib/table/flex-render.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | {#if typeof content === 'string'} 23 | {content} 24 | {:else if content instanceof Function} 25 | 26 | 27 | {@const result = content(context as any)} 28 | {#if result instanceof RenderComponentConfig} 29 | {@const { component: Component, props } = result} 30 | 31 | {:else if result instanceof RenderSnippetConfig} 32 | {@const { snippet, params } = result} 33 | {@render snippet(params)} 34 | {:else} 35 | {result} 36 | {/if} 37 | {/if} 38 | -------------------------------------------------------------------------------- /src/styles/shiki.css: -------------------------------------------------------------------------------- 1 | pre.shiki { 2 | padding: 24px 0; 3 | position: relative; 4 | } 5 | 6 | pre.shiki > code { 7 | display: block; 8 | padding: 0 24px; 9 | overflow-x: auto; 10 | } 11 | 12 | html.dark .shiki, 13 | html.dark .shiki span { 14 | color: var(--shiki-dark) !important; 15 | background-color: var(--shiki-dark-bg) !important; 16 | /* Optional, if you also want font styles */ 17 | font-style: var(--shiki-dark-font-style) !important; 18 | font-weight: var(--shiki-dark-font-weight) !important; 19 | text-decoration: var(--shiki-dark-text-decoration) !important; 20 | } 21 | 22 | /** 23 | * DIFFS 24 | */ 25 | .shiki.has-diff { 26 | --shiki-diff-bg-add: #10b98124; 27 | --shiki-diff-marker-add: #18794e; 28 | --shiki-diff-bg-remove: #d73a4926; 29 | --shiki-diff-marker-remove: #b34e52; 30 | } 31 | 32 | .shiki .line.diff { 33 | display: inline-block; 34 | padding: 0 24px; 35 | margin: 0 -24px; 36 | width: calc(100%); 37 | } 38 | 39 | .shiki .line.diff::before { 40 | position: absolute; 41 | left: 10px; 42 | } 43 | 44 | html.dark .shiki .line.diff span { 45 | background-color: transparent !important; 46 | } 47 | 48 | /* Additions */ 49 | .shiki.has-diff .line.diff.add { 50 | background-color: var(--shiki-diff-bg-add) !important; 51 | color: var(--shiki-diff-marker-add) !important; 52 | } 53 | 54 | .shiki .line.diff.add::before { 55 | content: '+'; 56 | } 57 | 58 | /* Deletions */ 59 | .shiki.has-diff .line.diff.remove { 60 | background-color: var(--shiki-diff-bg-remove) !important; 61 | color: var(--shiki-diff-marker-remove) !important; 62 | } 63 | 64 | .shiki .line.diff.remove::before { 65 | content: '-'; 66 | } 67 | -------------------------------------------------------------------------------- /src/vite-plugins/markdown.ts: -------------------------------------------------------------------------------- 1 | import rehypeShiki from '@shikijs/rehype'; 2 | import { transformerNotationDiff } from '@shikijs/transformers'; 3 | import rehypeSanitize from 'rehype-sanitize'; 4 | import rehypeStringify from 'rehype-stringify'; 5 | import remarkParse from 'remark-parse'; 6 | import remarkRehype from 'remark-rehype'; 7 | import rehypeSlug from 'rehype-slug'; 8 | import { unified } from 'unified'; 9 | import type { PluginOption } from 'vite'; 10 | import { themes } from '../lib/highlight'; 11 | 12 | async function convertMarkdownToHtml(raw: string) { 13 | const file = await unified() 14 | .use(remarkParse) 15 | .use(remarkRehype) 16 | .use(rehypeSanitize) 17 | .use(rehypeShiki, { 18 | themes, 19 | transformers: [transformerNotationDiff()] 20 | }) 21 | .use(rehypeSlug) 22 | .use(rehypeStringify) 23 | .process(raw); 24 | 25 | return String(file); 26 | } 27 | 28 | const plugin: PluginOption = { 29 | name: 'markdown-parser', 30 | async transform(raw, id) { 31 | if (id.endsWith('.md')) { 32 | try { 33 | const html = await convertMarkdownToHtml(raw); 34 | return `export default ${JSON.stringify(html)}`; 35 | } catch (e) { 36 | this.error(e as Error); 37 | } 38 | } 39 | }, 40 | async handleHotUpdate(ctx) { 41 | if (ctx.file.endsWith('.md')) { 42 | const defaultRead = ctx.read; 43 | ctx.read = async function () { 44 | const raw = await defaultRead(); 45 | const html = await convertMarkdownToHtml(raw); 46 | return `export default ${JSON.stringify(html)}`; 47 | }; 48 | } 49 | } 50 | }; 51 | 52 | export default plugin; 53 | -------------------------------------------------------------------------------- /src/lib/services/user-profile.ts: -------------------------------------------------------------------------------- 1 | import { fakerEN_US as faker } from '@faker-js/faker'; 2 | 3 | export type UserProfile = { 4 | id: string; 5 | name: string; 6 | age: number; 7 | email: string; 8 | phone: string; 9 | birthdate: string; 10 | friends: UserProfile[]; 11 | }; 12 | 13 | export class UserProfileService { 14 | /** 15 | * Generates a single user profile with random data. 16 | * 17 | * @returns {UserProfile} An object containing the following properties: 18 | */ 19 | /** 20 | * Generates a single user profile with a specified number of friends. 21 | * 22 | * @param nFriends - The number of friends to include in the user profile. Defaults to 0. 23 | * @returns A `UserProfile` object containing the generated user profile data. 24 | */ 25 | static getOne(nFriends: number = 0): UserProfile { 26 | const age = faker.number.int({ min: 18, max: 99 }); 27 | 28 | return { 29 | id: faker.string.uuid(), 30 | name: faker.person.fullName(), 31 | age, 32 | birthdate: faker.date.birthdate({ mode: 'age', min: age, max: age }).toUTCString(), 33 | email: faker.internet.email(), 34 | phone: faker.phone.number({ style: 'national' }), 35 | friends: this.list(nFriends) 36 | }; 37 | } 38 | 39 | /** 40 | * Generates a list of user profiles. 41 | * 42 | * @param n - The number of user profiles to generate. 43 | * @param nFriends - The number of friends each user profile should have. Defaults to 0. 44 | * @returns An array of user profiles. 45 | */ 46 | static list(n: number, nFriends: number = 0): UserProfile[] { 47 | return Array.from({ length: n }, () => this.getOne(nFriends)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/routes/examples/basic/+page.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |

Table Demo

31 | 32 |
33 | 34 | 35 | 36 | 37 | {#each table.getHeaderGroups() as headerGroup} 38 | {#each headerGroup.headers as header} 39 | 40 | {/each} 41 | {/each} 42 | 43 | 44 | 45 | {#each table.getRowModel().rows as row} 46 | 47 | {#each row.getVisibleCells() as cell} 48 | 49 | {/each} 50 | 51 | {/each} 52 | 53 |
{header.column.columnDef.header}
{cell.getValue()}
54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte5-tanstack-table", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/walker-tx/svelte5-tanstack-table-reference", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "lint": "prettier --check . && eslint .", 13 | "format": "prettier --write .", 14 | "prepare": "husky" 15 | }, 16 | "devDependencies": { 17 | "@faker-js/faker": "^9.0.3", 18 | "@shikijs/rehype": "^1.22.0", 19 | "@shikijs/transformers": "^1.22.0", 20 | "@sveltejs/adapter-static": "^3.0.8", 21 | "@sveltejs/kit": "^2.17.3", 22 | "@sveltejs/vite-plugin-svelte": "5.0.3", 23 | "@tanstack/table-core": "^8.20.5", 24 | "@types/eslint": "^9.6.0", 25 | "eslint": "^9.21.0", 26 | "eslint-config-prettier": "^9.1.0", 27 | "eslint-plugin-svelte": "^2.46.1", 28 | "globals": "^15.0.0", 29 | "husky": "^9.1.6", 30 | "lint-staged": "^15.2.10", 31 | "lucide-svelte": "^0.452.0", 32 | "mode-watcher": "^0.4.1", 33 | "prettier": "^3.5.2", 34 | "prettier-plugin-svelte": "^3.3.3", 35 | "rehype-sanitize": "^6.0.0", 36 | "rehype-slug": "^6.0.0", 37 | "rehype-stringify": "^10.0.1", 38 | "remark-parse": "^11.0.0", 39 | "remark-rehype": "^11.1.1", 40 | "shiki": "^1.22.0", 41 | "svelte": "5.23.0", 42 | "svelte-check": "^4.1.4", 43 | "typescript": "^5.8.0", 44 | "typescript-eslint": "^8.25.0", 45 | "unified": "^11.0.5", 46 | "vite": "^6.2.1" 47 | }, 48 | "type": "module", 49 | "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c" 50 | } 51 | -------------------------------------------------------------------------------- /src/routes/examples/rendering-components/_components/data-table-cell-countdown.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 48 | 49 | {countdown} 50 | 51 | 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Link to Deployed Site](https://svelte5-and-tanstack-table-v8.vercel.app/) 2 | 3 | # Svelte 5 + TanStack Table v8 Reference 4 | 5 | TanStack v8's own adapter for Svelte (`@tanstack/svelte-table`) won't work with 6 | Svelte 5 due to its usage of the `svelte/internal` package. This SvelteKit 7 | project demonstrates a workaround for this issue while TanStack Table v9 (and 8 | the official Svelte 5 adapter) is still under development. 9 | 10 | # How to Use This Reference 11 | 12 | ## Implementing TanStack Table in Svelte 5 13 | 14 | A TanStack Table adapter is essentially an implementation of the 15 | `@tanstack/table-core` package that integrates a framework's state management 16 | and rendering API's with the core table logic. It's actually quite simple to 17 | create your own adapter, and that's what this project demonstrates. 18 | 19 | A TanStack Table implementation for Svelte 5 can be found in the `src/lib/table` 20 | directory. This is a loose copy of the work performed in [this 21 | PR](https://github.com/TanStack/table/pull/5403), but updated to accommodate the 22 | latest version of Svelte 5, and some feature requests. You can use the adapter 23 | by copying the `table` directory into your own project. 24 | 25 | Some documentation is available in the [`/src/lib/table` folder](./src/lib/table/README.md). 26 | 27 | The `examples` route also contains examples of how to use the table implementation. 28 | 29 | ## Examples 30 | 31 | Examples live in the `src/routes/examples` directory. They demonstrate how to 32 | use the table implementation in various scenarios. 33 | 34 | Each example is roughly documented in a `README.md` file in its directory. 35 | 36 | # Issues & Requests 37 | 38 | ## Bugs & Typos 39 | 40 | It's quite possible that there are issues in the way that I've implemented 41 | and/or documented some of the examples. If you find any, please submit an issue 42 | or a PR. 43 | 44 | ## Missing Examples 45 | 46 | Think an example is missing? Submit a request in issues, or submit a PR. 47 | -------------------------------------------------------------------------------- /src/routes/examples/grid-view/+page.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | {#snippet profileCard(userProfile: UserProfile)} 31 |
32 |
33 | {userProfile.name} 34 |
35 | 40 |
41 | {/snippet} 42 | 43 |

Table Demo

44 | 45 |
46 | 47 |
48 | {#each table.getCoreRowModel().rows as row} 49 | {@render profileCard(row.original)} 50 | {/each} 51 |
52 | 53 | 77 | -------------------------------------------------------------------------------- /src/routes/examples/rendering-components/+page.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |

Table Demo

37 | 38 |
39 | 40 | 41 | 42 | 43 | {#each table.getHeaderGroups() as headerGroup} 44 | {#each headerGroup.headers as header} 45 | 51 | {/each} 52 | {/each} 53 | 54 | 55 | 56 | {#each table.getRowModel().rows as row} 57 | 58 | {#each row.getVisibleCells() as cell} 59 | 65 | {/each} 66 | 67 | {/each} 68 | 69 |
46 | 50 |
60 | 64 |
70 | -------------------------------------------------------------------------------- /src/routes/examples/rendering-snippets/+page.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 | {#snippet strongSnippet(content: string)} 46 | {content} 47 | {/snippet} 48 | 49 |

Table Demo

50 | 51 |
52 | 53 | 54 | 55 | 56 | {#each table.getHeaderGroups() as headerGroup} 57 | {#each headerGroup.headers as header} 58 | 64 | {/each} 65 | {/each} 66 | 67 | 68 | 69 | {#each table.getRowModel().rows as row} 70 | 71 | {#each row.getVisibleCells() as cell} 72 | 78 | {/each} 79 | 80 | {/each} 81 | 82 |
59 | 63 |
73 | 77 |
83 | -------------------------------------------------------------------------------- /src/routes/examples/reactive-data/+page.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 |
46 |

Actions

47 |
48 | 49 | 50 |
51 | 52 |

Table Demo

53 | 54 |
55 | 56 | 57 | 58 | 59 | {#each table.getHeaderGroups() as headerGroup} 60 | {#each headerGroup.headers as header} 61 | 67 | {/each} 68 | {/each} 69 | 70 | 71 | 72 | {#each table.getRowModel().rows as row} 73 | 74 | {#each row.getVisibleCells() as cell} 75 | 81 | {/each} 82 | 83 | {/each} 84 | 85 |
62 | 66 |
76 | 80 |
86 | -------------------------------------------------------------------------------- /src/lib/services/example-registry.ts: -------------------------------------------------------------------------------- 1 | type ExampleRegistryItem = { 2 | id: string; 3 | title: string; 4 | pathname: string; 5 | githubPath: string; 6 | links?: { title: string; href: string }[]; 7 | }; 8 | 9 | const registry: ExampleRegistryItem[] = [ 10 | { 11 | id: 'basic', 12 | title: 'Basic Table', 13 | pathname: '/examples/basic', 14 | githubPath: 'src/routes/examples/basic', 15 | links: [ 16 | { 17 | title: 'Data Guide', 18 | href: 'https://tanstack.com/table/latest/docs/guide/data' 19 | }, 20 | { 21 | title: 'Column Defs Guide', 22 | href: 'https://tanstack.com/table/latest/docs/guide/column-defs' 23 | } 24 | ] 25 | }, 26 | { 27 | id: 'grid-view', 28 | title: 'Grid View', 29 | pathname: '/examples/grid-view', 30 | githubPath: 'src/routes/examples/grid-view', 31 | links: [] 32 | }, 33 | { 34 | id: 'rendering-snippets', 35 | title: 'Rendering Snippets', 36 | pathname: '/examples/rendering-snippets', 37 | githubPath: 'src/routes/examples/rendering-snippets', 38 | links: [ 39 | { 40 | title: 'Column Defs Guide', 41 | href: 'https://tanstack.com/table/latest/docs/guide/column-defs' 42 | }, 43 | { 44 | title: 'Cells Guide', 45 | href: 'https://tanstack.com/table/latest/docs/guide/cells' 46 | } 47 | ] 48 | }, 49 | { 50 | id: 'rendering-components', 51 | title: 'Rendering Components', 52 | pathname: '/examples/rendering-components', 53 | githubPath: 'src/routes/examples/rendering-components', 54 | links: [ 55 | { 56 | title: 'Column Defs Guide', 57 | href: 'https://tanstack.com/table/latest/docs/guide/column-defs' 58 | }, 59 | { 60 | title: 'Cells Guide', 61 | href: 'https://tanstack.com/table/latest/docs/guide/cells' 62 | } 63 | ] 64 | }, 65 | { 66 | id: 'reactive-data', 67 | title: 'Reactive Data', 68 | pathname: '/examples/reactive-data', 69 | githubPath: 'src/routes/examples/reactive-data', 70 | links: [ 71 | { 72 | title: 'Stable References (Data Guide)', 73 | href: 'https://tanstack.com/table/latest/docs/guide/data#give-data-a-stable-reference' 74 | } 75 | ] 76 | }, 77 | { 78 | id: 'row-selection', 79 | title: 'Row Selection', 80 | pathname: '/examples/row-selection', 81 | githubPath: 'src/routes/examples/row-selection', 82 | links: [ 83 | { 84 | title: 'Row Selection API Reference', 85 | href: 'https://tanstack.com/table/latest/docs/api/features/row-selection' 86 | } 87 | ] 88 | }, 89 | { 90 | id: 'row-expansion', 91 | title: 'Row Expansion', 92 | pathname: '/examples/row-expansion', 93 | githubPath: 'src/routes/examples/row-expansion', 94 | links: [ 95 | { 96 | title: 'Row Expansion API Reference', 97 | href: 'https://tanstack.com/table/latest/docs/api/features/expanding' 98 | } 99 | ] 100 | } 101 | ]; 102 | 103 | export default registry; 104 | -------------------------------------------------------------------------------- /src/lib/table/table.svelte.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTable, 3 | type RowData, 4 | type TableOptions, 5 | type TableOptionsResolved, 6 | type TableState 7 | } from '@tanstack/table-core'; 8 | 9 | /** 10 | * Creates a reactive TanStack table object for Svelte. 11 | * @param options Table options to create the table with. 12 | * @returns A reactive table object. 13 | * @example 14 | * ```svelte 15 | * 18 | * 19 | * 20 | * 21 | * {#each table.getHeaderGroups() as headerGroup} 22 | * 23 | * {#each headerGroup.headers as header} 24 | * 27 | * {/each} 28 | * 29 | * {/each} 30 | * 31 | * 32 | *
25 | * 26 | *
33 | * ``` 34 | */ 35 | export function createSvelteTable(options: TableOptions) { 36 | const resolvedOptions: TableOptionsResolved = mergeObjects( 37 | { 38 | state: {}, 39 | onStateChange() {}, 40 | renderFallbackValue: null, 41 | mergeOptions: ( 42 | defaultOptions: TableOptions, 43 | options: Partial> 44 | ) => { 45 | return mergeObjects(defaultOptions, options); 46 | } 47 | }, 48 | options 49 | ); 50 | 51 | const table = createTable(resolvedOptions); 52 | let state = $state>(table.initialState); 53 | 54 | function updateOptions() { 55 | table.setOptions((prev) => { 56 | return mergeObjects(prev, options, { 57 | state: mergeObjects(state, options.state || {}), 58 | 59 | onStateChange: (updater: any) => { 60 | if (updater instanceof Function) state = updater(state); 61 | else state = mergeObjects(state, updater); 62 | 63 | options.onStateChange?.(updater); 64 | } 65 | }); 66 | }); 67 | } 68 | 69 | updateOptions(); 70 | 71 | $effect.pre(() => { 72 | updateOptions(); 73 | }); 74 | 75 | return table; 76 | } 77 | 78 | /** 79 | * Merges objects together while keeping their getters alive. 80 | * Taken from SolidJS: {@link https://github.com/solidjs/solid/blob/24abc825c0996fd2bc8c1de1491efe9a7e743aff/packages/solid/src/server/rendering.ts#L82-L115} 81 | * */ 82 | export function mergeObjects(source: T): T; 83 | export function mergeObjects(source: T, source1: U): T & U; 84 | export function mergeObjects(source: T, source1: U, source2: V): T & U & V; 85 | export function mergeObjects( 86 | source: T, 87 | source1: U, 88 | source2: V, 89 | source3: W 90 | ): T & U & V & W; 91 | export function mergeObjects(...sources: any): any { 92 | const target = {}; 93 | for (let i = 0; i < sources.length; i++) { 94 | let source = sources[i]; 95 | if (typeof source === 'function') source = source(); 96 | if (source) { 97 | const descriptors = Object.getOwnPropertyDescriptors(source); 98 | for (const key in descriptors) { 99 | if (key in target) continue; 100 | Object.defineProperty(target, key, { 101 | enumerable: true, 102 | get() { 103 | for (let i = sources.length - 1; i >= 0; i--) { 104 | let s = sources[i]; 105 | if (typeof s === 'function') s = s(); 106 | const v = (s || {})[key]; 107 | if (v !== undefined) return v; 108 | } 109 | } 110 | }); 111 | } 112 | } 113 | } 114 | return target; 115 | } 116 | -------------------------------------------------------------------------------- /src/lib/table/render-component.ts: -------------------------------------------------------------------------------- 1 | import type { Component, ComponentProps, Snippet } from 'svelte'; 2 | 3 | /** 4 | * A helper class to make it easy to identify Svelte components in 5 | * `columnDef.cell` and `columnDef.header` properties. 6 | * 7 | * > NOTE: This class should only be used internally by the adapter. If you're 8 | * reading this and you don't know what this is for, you probably don't need it. 9 | * 10 | * @example 11 | * ```svelte 12 | * {@const result = content(context as any)} 13 | * {#if result instanceof RenderComponentConfig} 14 | * {@const { component: Component, props } = result} 15 | * 16 | * {/if} 17 | * ``` 18 | * */ 19 | export class RenderComponentConfig { 20 | constructor( 21 | public component: TComponent, 22 | public props: ComponentProps | Record = {} 23 | ) {} 24 | } 25 | 26 | /** 27 | * A helper class to make it easy to identify Svelte Snippets in `columnDef.cell` and `columnDef.header` properties. 28 | * 29 | * > NOTE: This class should only be used internally by the adapter. If you're 30 | * reading this and you don't know what this is for, you probably don't need it. 31 | * 32 | * @example 33 | * ```svelte 34 | * {@const result = content(context as any)} 35 | * {#if result instanceof RenderSnippetConfig} 36 | * {@const { snippet, params } = result} 37 | * {@render snippet(params)} 38 | * {/if} 39 | * ``` 40 | * */ 41 | export class RenderSnippetConfig { 42 | constructor( 43 | public snippet: Snippet<[TProps]>, 44 | public params: TProps 45 | ) {} 46 | } 47 | 48 | /** 49 | * A helper function to help create cells from Svelte components through ColumnDef's `cell` and `header` properties. 50 | * 51 | * This is only to be used with Svelte Components - use `renderSnippet` for Svelte Snippets. 52 | * 53 | * @param component A Svelte component 54 | * @param props The props to pass to `component` 55 | * @returns A `RenderComponentConfig` object that helps svelte-table know how to render the header/cell component. 56 | * @example 57 | * ```ts 58 | * // +page.svelte 59 | * const defaultColumns = [ 60 | * columnHelper.accessor('name', { 61 | * header: header => renderComponent(SortHeader, { label: 'Name', header }), 62 | * }), 63 | * columnHelper.accessor('state', { 64 | * header: header => renderComponent(SortHeader, { label: 'State', header }), 65 | * }), 66 | * ] 67 | * ``` 68 | * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} 69 | */ 70 | export const renderComponent = < 71 | TComponent extends Component, 72 | TProps extends ComponentProps 73 | >( 74 | component: TComponent, 75 | props: TProps 76 | ) => new RenderComponentConfig(component, props); 77 | 78 | /** 79 | * A helper function to help create cells from Svelte Snippets through ColumnDef's `cell` and `header` properties. 80 | * 81 | * The snippet must only take one parameter. 82 | * 83 | * This is only to be used with Snippets - use `renderComponent` for Svelte Components. 84 | * 85 | * @param snippet 86 | * @param params 87 | * @returns 88 | * @example 89 | * ```ts 90 | * // +page.svelte 91 | * const defaultColumns = [ 92 | * columnHelper.accessor('name', { 93 | * cell: cell => renderSnippet(nameSnippet, { name: cell.row.name }), 94 | * }), 95 | * columnHelper.accessor('state', { 96 | * cell: cell => renderSnippet(stateSnippet, { state: cell.row.state }), 97 | * }), 98 | * ] 99 | * ``` 100 | * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} 101 | */ 102 | export const renderSnippet = (snippet: Snippet<[TProps]>, params: TProps) => 103 | new RenderSnippetConfig(snippet, params); 104 | -------------------------------------------------------------------------------- /src/routes/examples/+layout.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | {currentExample.title} | Tanstack Table v8 + Svelte 5 Reference 18 | 19 | 20 | 21 |
22 | 35 | 36 |
37 |

{currentExample.title}

38 |
39 |
Useful Links
40 | 56 |
57 | 58 |
59 | {@render children()} 60 | 61 |

Explanation

62 | 63 |
64 | 65 | 66 | {@html readmeHtml} 67 |
68 | 69 |
70 | 71 | 92 |
93 | 94 | 133 | -------------------------------------------------------------------------------- /src/routes/examples/row-selection/+page.svelte: -------------------------------------------------------------------------------- 1 | 63 | 64 |
65 |

Actions

66 |
67 | 74 |
75 | 76 |

Table Demo

77 | 78 |
79 | 80 | 81 | 82 | 83 | {#each table.getHeaderGroups() as headerGroup} 84 | {#each headerGroup.headers as header} 85 | 91 | {/each} 92 | {/each} 93 | 94 | 95 | 96 | {#each table.getRowModel().rows as row} 97 | 98 | {#each row.getVisibleCells() as cell} 99 | 105 | {/each} 106 | 107 | {/each} 108 | 109 |
86 | 90 |
100 | 104 |
110 | 111 |

Debug

112 | 113 |
114 | 115 |

Selection State

116 | 117 | 118 | -------------------------------------------------------------------------------- /src/routes/examples/row-expansion/+page.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 |
70 |

Actions

71 |
72 | 79 |
80 | 81 |

Table Demo

82 | 83 |
84 | 85 | 86 | 87 | 88 | {#each table.getHeaderGroups() as headerGroup} 89 | {#each headerGroup.headers as header} 90 | 96 | {/each} 97 | {/each} 98 | 99 | 100 | 101 | {#each table.getRowModel().rows as row} 102 | 103 | {#each row.getVisibleCells() as cell} 104 | 110 | {/each} 111 | 112 | {/each} 113 | 114 |
91 | 95 |
105 | 109 |
115 | 116 |

Debug

117 | 118 |
119 | 120 |

Expanded State

121 | 122 | 123 | -------------------------------------------------------------------------------- /src/routes/examples/basic/README.md: -------------------------------------------------------------------------------- 1 | This guide walks through the process of creating a table in Svelte 5 using a 2 | column helper, defining column definitions, setting up the table, and rendering 3 | it in the markup. 4 | 5 | ### Defining the Column Helper 6 | 7 | A column helper is a utility that simplifies the creation of column definitions, 8 | especially when working with typed data. It ensures that the column definitions 9 | have the correct data types and structure, making your code more robust and 10 | easier to maintain. 11 | 12 | ```svelte 13 | 19 | ``` 20 | 21 | ### Making the Column Definitions 22 | 23 | Using the column helper, we define the columns that our table will display. Each 24 | column is associated with a specific field from the data and has a header label. 25 | 26 | 27 | ```svelte 28 | 40 | ``` 41 | 42 | 43 | - **Field Access**: The first argument to each `accessor` method specifies the 44 | field to extract from each data record (e.g., `name`, `age`, `email`, and 45 | `phone`). 46 | - **Header Label**: The `header` property defines what will be displayed in the 47 | table header for each column. 48 | 49 | This approach clearly maps each column to its respective field in the data, 50 | making the table definition concise and easy to understand. 51 | 52 | ### Setting Up the Table 53 | 54 | After defining the columns, the next step is to set up the table using the 55 | `createSvelteTable` function. This function initializes the table with the 56 | specified data, columns, and core row model. 57 | 58 | 59 | ```svelte 60 | 81 | ``` 82 | 83 | 84 | For more information on row models, refer to the [Row Model 85 | Documentation](https://tanstack.com/table/latest/docs/guide/row-models). 86 | 87 | ### Rendering the Table 88 | 89 | Finally, the table structure is created using Svelte's templating syntax. This 90 | includes rendering both the header and the body of the table dynamically. 91 | 92 | ```svelte 93 | 94 | 95 | 96 | {#each table.getHeaderGroups() as headerGroup} 97 | {#each headerGroup.headers as header} 98 | 99 | {/each} 100 | {/each} 101 | 102 | 103 | 104 | {#each table.getRowModel().rows as row} 105 | 106 | {#each row.getVisibleCells() as cell} 107 | 108 | {/each} 109 | 110 | {/each} 111 | 112 |
{header.column.columnDef.header}
{cell.getValue()}
113 | ``` 114 | 115 | Since all of the are plain old JavaScript objects (POJO's), we don't need to use 116 | `FlexRender` to render the cells. Use `FlexRender` when you need to render 117 | components or snippets. 118 | -------------------------------------------------------------------------------- /src/routes/examples/grid-view/README.md: -------------------------------------------------------------------------------- 1 | This guide walks you through converting the [basic table example](/examples/basic) into a grid view. 2 | 3 | ### Why would I use a table library for a grid view? 4 | 5 | Though much of the rendering API will be put to the side for this example, a 6 | grid view still benefits from the `table` object's features such as state 7 | management. 8 | 9 | ### Creating the Table 10 | 11 | We're going to start from the basic table example and convert it into a grid. 12 | 13 | ```svelte 14 | 15 | 34 | ``` 35 | 36 | ### Making the Card Snippet 37 | 38 | To display the data, we'll make a simple snippet (or component). This one accepts a `UserProfile` object as a parameter. 39 | 40 | 41 | ```svelte 42 | 43 | 62 | 63 | {#snippet profileCard(userProfile: UserProfile)} // [!code ++] 64 |
// [!code ++] 65 |
// [!code ++] 66 | {userProfile.name} // [!code ++] 67 |
// [!code ++] 68 | // [!code ++] 73 |
// [!code ++] 74 | {/snippet} // [!code ++] 75 | 76 | // [!code ++] 94 | ``` 95 | 96 | 97 | ### Displaying the Grid 98 | 99 | Now, we can display the grid by grabbing the `rows` array from `table.getCoreRowModel()`. As we iterate over each entry, we'll use the `row`'s `original` property to pass the `UserProfile` object to the `profileCard` snippet. 100 | 101 | 102 | ```svelte 103 | 104 | 107 | 108 | {#snippet profileCard(userProfile: UserProfile)} 109 |
110 |
111 | {userProfile.name} 112 |
113 | 118 |
119 | {/snippet} 120 | 121 |
// [!code ++] 122 | {#each table.getCoreRowModel().rows as row} // [!code ++] 123 | {@render profileCard(row.original)} // [!code ++] 124 | {/each} // [!code ++] 125 |
// [!code ++] 126 | 127 | 136 | ``` 137 | 138 | 139 | Now that the grid is rendered, you can apply more functionality to it using the table API, such as sorting, filtering, or pagination. 140 | -------------------------------------------------------------------------------- /src/routes/examples/reactive-data/README.md: -------------------------------------------------------------------------------- 1 | In this guide, we'll extend your knowledge of creating a table in Svelte by 2 | focusing on handling reactive data. 3 | 4 | **Before proceeding, you should already know how to create and render a basic 5 | table.** 6 | 7 | ### Adding Data to a `$state` Rune 8 | 9 | The first step is to make the data reactive by storing it in a `$state`. In 10 | Svelte 5, `$state` is a signal that allows you to track and update state changes 11 | in a reactive manner. This ensures that when the data changes, the table will 12 | automatically update to reflect those changes. 13 | 14 | ```svelte 15 | 26 | ``` 27 | 28 | ### Creating the Table 29 | 30 | To make the table reactive, we need to pass the `data` property as a getter 31 | function to `createSvelteTable`. This approach ensures that the table 32 | dynamically responds to changes in `dataState`. 33 | 34 | 35 | ```svelte 36 | 67 | ``` 68 | 69 | 70 | - **Reactive Data**: By implementing the `data` property as a getter, any change 71 | to `dataState` will automatically trigger the table to re-render with the 72 | updated data. 73 | - **Benefit**: This setup makes the table's data handling straightforward and 74 | automatically keeps it in sync with state changes. 75 | 76 | ### Mutating Data 77 | 78 | We will create two functions to modify the table data: one to add a record at 79 | the beginning and another to remove the last record. It's crucial to reassign 80 | the `dataState` variable when updating the data to ensure reactivity. 81 | 82 | 83 | ```svelte 84 | 122 | 123 |
// [!code ++] 124 |

Actions

// [!code ++] 125 |
// [!code ++] 126 | // [!code ++] 127 | // [!code ++] 128 |
// [!code ++] 129 | ``` 130 | 131 | 132 | > 🗒️ Note: The `dataState` variable must be reassigned after mutation 133 | > (`dataState = ...`) to ensure that the change is detected and triggers an 134 | > update in the UI. 135 | 136 | ### Rendering the Table in the Markup 137 | 138 | Finally, we render the table using Svelte's templating syntax. Since the table 139 | data is reactive, it will automatically update when you modify `dataState` using 140 | the functions created above. 141 | 142 | ```svelte 143 | 144 | 145 | 146 | {#each table.getHeaderGroups() as headerGroup} 147 | {#each headerGroup.headers as header} 148 | 151 | {/each} 152 | {/each} 153 | 154 | 155 | 156 | {#each table.getRowModel().rows as row} 157 | 158 | {#each row.getVisibleCells() as cell} 159 | 162 | {/each} 163 | 164 | {/each} 165 | 166 |
149 | 150 |
160 | 161 |
167 | ``` 168 | -------------------------------------------------------------------------------- /src/styles/sakura.css: -------------------------------------------------------------------------------- 1 | /* Sakura.css v1.5.0 2 | * ================ 3 | * Minimal css theme. 4 | * Project: https://github.com/oxalorg/sakura/ 5 | * 6 | * This has been adapted to use CSS variables for theming. 7 | */ 8 | 9 | /* Theme Setup */ 10 | :root { 11 | --color-blossom: #1d7484; 12 | --color-fade: #982c61; 13 | --color-bg: #f9f9f9; 14 | --color-bg-alt: #f1f1f1; 15 | --color-text: #4a4a4a; 16 | --font-size-base: 1.8rem; 17 | --font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 18 | 'Noto Sans', sans-serif; 19 | --font-family-heading: var(--font-family-base); 20 | } 21 | 22 | :root.dark { 23 | --color-blossom: #ffffff; 24 | --color-fade: #c9c9c9; 25 | --color-bg: #222222; 26 | --color-bg-alt: #4a4a4a; 27 | --color-text: #c9c9c9; 28 | --font-size-base: 1.8rem; 29 | } 30 | 31 | /* Body */ 32 | html { 33 | font-size: 62.5%; 34 | font-family: var(--font-family-base); 35 | } 36 | 37 | body { 38 | font-size: var(--font-size-base); 39 | line-height: 1.618; 40 | max-width: 54em; 41 | margin: auto; 42 | color: var(--color-text); 43 | background-color: var(--color-bg); 44 | padding: 13px; 45 | } 46 | 47 | @media (max-width: 684px) { 48 | body { 49 | font-size: calc(var(--font-size-base) * 0.85); 50 | } 51 | } 52 | 53 | @media (max-width: 382px) { 54 | body { 55 | font-size: calc(var(--font-size-base) * 0.75); 56 | } 57 | } 58 | 59 | @mixin word-wrap() { 60 | overflow-wrap: break-word; 61 | word-wrap: break-word; 62 | -ms-word-break: break-all; 63 | word-break: break-word; 64 | } 65 | 66 | h1, 67 | h2, 68 | h3, 69 | h4, 70 | h5, 71 | h6 { 72 | line-height: 1.1; 73 | font-family: var(--font-family-heading); 74 | font-weight: 700; 75 | margin-top: 3rem; 76 | margin-bottom: 1.5rem; 77 | @include word-wrap; 78 | } 79 | 80 | h1 { 81 | font-size: 2.35em; 82 | } 83 | h2 { 84 | font-size: 2em; 85 | } 86 | h3 { 87 | font-size: 1.75em; 88 | } 89 | h4 { 90 | font-size: 1.5em; 91 | } 92 | h5 { 93 | font-size: 1.25em; 94 | } 95 | h6 { 96 | font-size: 1em; 97 | } 98 | 99 | p { 100 | margin-top: 0px; 101 | margin-bottom: 2.5rem; 102 | } 103 | 104 | small, 105 | sub, 106 | sup { 107 | font-size: 75%; 108 | } 109 | 110 | hr { 111 | border-color: var(--color-blossom); 112 | } 113 | 114 | a { 115 | text-decoration: none; 116 | color: var(--color-blossom); 117 | 118 | &:visited { 119 | color: darken(var(--color-blossom), 10%); 120 | } 121 | 122 | &:hover { 123 | color: var(--color-fade); 124 | border-bottom: 2px solid var(--color-text); 125 | } 126 | } 127 | 128 | ul { 129 | padding-left: 1.4em; 130 | margin-top: 0px; 131 | margin-bottom: 2.5rem; 132 | } 133 | 134 | li { 135 | margin-bottom: 0.4em; 136 | } 137 | 138 | blockquote { 139 | margin-left: 0px; 140 | margin-right: 0px; 141 | padding-left: 1em; 142 | padding-top: 0.8em; 143 | padding-bottom: 0.8em; 144 | padding-right: 0.8em; 145 | border-left: 5px solid var(--color-blossom); 146 | margin-bottom: 2.5rem; 147 | background-color: var(--color-bg-alt); 148 | } 149 | 150 | blockquote p { 151 | margin-bottom: 0; 152 | } 153 | 154 | img, 155 | video { 156 | height: auto; 157 | max-width: 100%; 158 | margin-top: 0px; 159 | margin-bottom: 2.5rem; 160 | } 161 | 162 | /* Pre and Code */ 163 | 164 | pre { 165 | background-color: var(--color-bg-alt); 166 | display: block; 167 | padding: 1em; 168 | overflow-x: auto; 169 | margin-top: 0px; 170 | margin-bottom: 2.5rem; 171 | font-size: 0.9em; 172 | } 173 | 174 | code, 175 | kbd, 176 | samp { 177 | font-size: 0.9em; 178 | padding: 0 0.5em; 179 | background-color: var(--color-bg-alt); 180 | white-space: pre-wrap; 181 | } 182 | 183 | pre > code { 184 | padding: 0; 185 | background-color: transparent; 186 | white-space: pre; 187 | font-size: 1em; 188 | } 189 | 190 | /* Tables */ 191 | 192 | table { 193 | text-align: justify; 194 | width: 100%; 195 | border-collapse: collapse; 196 | margin-bottom: 2rem; 197 | border: 1px solid var(--color-blossom); 198 | } 199 | 200 | td, 201 | th { 202 | padding: 0.5em; 203 | border: 1px solid var(--color-blossom); 204 | } 205 | 206 | /* Buttons, forms and input */ 207 | 208 | input, 209 | textarea { 210 | border: 1px solid var(--color-text); 211 | 212 | &:focus { 213 | border: 1px solid var(--color-blossom); 214 | } 215 | } 216 | 217 | textarea { 218 | width: 100%; 219 | } 220 | 221 | .button, 222 | button, 223 | input[type='submit'], 224 | input[type='reset'], 225 | input[type='button'], 226 | input[type='file']::file-selector-button { 227 | display: inline-block; 228 | padding: 5px 10px; 229 | text-align: center; 230 | text-decoration: none; 231 | white-space: nowrap; 232 | 233 | background-color: var(--color-blossom); 234 | color: var(--color-bg); 235 | border-radius: 1px; 236 | border: 1px solid var(--color-blossom); 237 | cursor: pointer; 238 | box-sizing: border-box; 239 | 240 | &[disabled] { 241 | cursor: default; 242 | opacity: 0.5; 243 | } 244 | 245 | &:hover { 246 | background-color: var(--color-fade); 247 | color: var(--color-bg); 248 | outline: 0; 249 | } 250 | 251 | &:focus-visible { 252 | outline-style: solid; 253 | outline-width: 2px; 254 | } 255 | } 256 | 257 | textarea, 258 | select, 259 | input { 260 | color: var(--color-text); 261 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 262 | margin-bottom: 10px; 263 | background-color: var(--color-bg-alt); 264 | border: 1px solid var(--color-bg-alt); 265 | border-radius: 4px; 266 | box-shadow: none; 267 | box-sizing: border-box; 268 | 269 | &:focus { 270 | border: 1px solid var(--color-blossom); 271 | outline: 0; 272 | } 273 | } 274 | 275 | input[type='checkbox']:focus { 276 | outline: 1px dotted var(--color-blossom); 277 | } 278 | 279 | label, 280 | legend, 281 | fieldset { 282 | display: block; 283 | margin-bottom: 0.5rem; 284 | font-weight: 600; 285 | } 286 | -------------------------------------------------------------------------------- /src/routes/examples/rendering-components/README.md: -------------------------------------------------------------------------------- 1 | In this guide, we will walk through the process of rendering Svelte components 2 | inside table cells. This allows you to insert more complex or dynamic content 3 | within the table structure. 4 | 5 | **You should already know how to make a basic table before proceeding.** 6 | 7 | ### Make a Component 8 | 9 | Start out by making a component that you want to render inside the table. For 10 | this example, we'll use a countdown timer component. 11 | 12 | ```svelte 13 | 14 | 40 | 41 | 61 | 62 | {countdown} 63 | 64 | 69 | ``` 70 | 71 | ### Column Definitions 72 | 73 | Next, we define a set of columns for our table. The column definition array 74 | specifies how each cell should be rendered, and this is where `renderComponent` 75 | comes in. This function allows you to use the components defined earlier to 76 | customize the cell content. 77 | 78 | Here's an example of how we use `renderComponent` in the column 79 | definitions: 80 | 81 | 82 | ```svelte 83 | 84 | 102 | ``` 103 | 104 | 105 | > 🗒️ NOTE: You can also use `renderColumn` with the `header` field of each 106 | > column. 107 | 108 | ### Setting Up the Table 109 | 110 | After defining the columns, the next step is to set up the table using the 111 | `createSvelteTable` function. This function initializes the table with the 112 | specified data, columns, and core row model. 113 | 114 | 115 | ```svelte 116 | 141 | 142 | ``` 143 | 144 | 145 | ### Rendering the Table 146 | 147 | Finally, to render the table itself, we use the `FlexRender` component. 148 | `FlexRender` is responsible for dynamically rendering the table's cells based on 149 | the content and context provided. 150 | 151 | ```svelte 152 | 153 | 154 | 155 | {#each table.getHeaderGroups() as headerGroup} 156 | {#each headerGroup.headers as header} 157 | 160 | {/each} 161 | {/each} 162 | 163 | 164 | 165 | {#each table.getRowModel().rows as row} 166 | 167 | {#each row.getVisibleCells() as cell} 168 | 171 | {/each} 172 | 173 | {/each} 174 | 175 |
158 | 159 |
169 | 170 |
176 | ``` 177 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 |
46 |
47 |

Tanstack Table v8 + Svelte 5

48 |

49 | A reference for making awesome tables using TanStack Table and Svelte 5. Right now. 50 |

51 | 52 | GitHub Repository 53 | 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 | 62 |

Overview

63 | 64 |
65 | 66 |

67 | The @tanstack/table-svelte adapter doesn't support Svelte 5 because it uses the 68 | svelte/internal package, which is being deprecated. TanStack Table's official Svelte 69 | 5 adapter is slated to be released alongside TanStack Table v9, which is still in alpha. As a result, 70 | the core library is in a state of flux, and the Svelte adapter is not yet stable. 71 |

72 | 73 |

74 | This site, and its repository on GitHub, were made to be a 75 | reference to help developers implement their own adapter for TanStack Table v8 (which is stable) 76 | and Svelte 5. Thus, isolating your work from any volatility in the alpha branch of the 77 | core library. 78 |

79 | 80 |

Treat this reference as a cookbook, full of recipes.

81 | 82 |

Project Setup

83 | 84 |
85 | 86 |

87 | To get started, you'll need to install the core library and the Svelte adapter. You can do this 88 | by running the following command: 89 |

90 | 91 | 92 | 93 |

94 | Then, you'll need to copy the contents of the src/lib/table directory into your project 95 | directory. 96 |

97 | 98 |

99 | You can find those files here, or grab a zip of that directory 100 | by 101 | clicking here. 104 |

105 | 106 |

Now, you can get cooking!

107 | 108 |

Examples

109 | 110 |
111 | 112 | 113 | 114 | 115 | {#each table.getHeaderGroups() as headerGroup} 116 | {#each headerGroup.headers as header} 117 | 123 | {/each} 124 | {/each} 125 | 126 | 127 | 128 | {#each table.getRowModel().rows as row} 129 | 130 | {#each row.getVisibleCells() as cell} 131 | 137 | {/each} 138 | 139 | {/each} 140 | 141 |
118 | 122 |
132 | 136 |
142 | 143 |

Questions you may have

144 | 145 |
146 | 147 |

Why not just use the alpha (v9) branch?

148 | 149 |

150 | TanStack Table v9 is currently under development - it's in alpha. It changes very frequently, 151 | and therefore breaks very frequently. While things are so volatile, maintainers and contributors 152 | can't be expected to support it. 153 |

154 | 155 |

156 | This volatility applies to both @tanstack/table-core and 157 | @tanstack/table-svelte. 158 |

159 | 160 |

How can I share feedback, or get help?

161 | 162 |

163 | Submit an issue on GitHub if you'd like to share some feedback or need help. All feedback will 164 | help in the maintenance of this project, in addition to the development of the official TanStack 165 | Table adapter for Svelte 5. 166 |

167 | 168 |

169 | Also, please DO NOT submit an issue that you have with this project on the official TanStack 170 | Table repository. ONLY submit an issue there if you're sure that it's a problem with the core 171 | library, and not this project or your own implementation. 172 |

173 | 174 |
175 | 176 | 183 | 184 | 199 | -------------------------------------------------------------------------------- /src/routes/examples/rendering-snippets/README.md: -------------------------------------------------------------------------------- 1 | When you don't want to make an entire component for a small piece of reusable 2 | code, snippets are a great way to encapsulate and reuse logic. In this example, 3 | we'll create snippets for rendering text in a `` tag and email addresses 4 | as clickable mailto links. 5 | 6 | **You should already know how to make a basic table before proceeding.** 7 | 8 | ### Defining Snippets 9 | 10 | Snippets can be defined in Svelte either directly in the markup or in the script 11 | tag. Here, we use two methods to define our snippets: 12 | 13 | > 🗒️ NOTE: The implementation of snippets within this repo only works with 14 | > snippets that take one argument. 15 | 16 | #### Markup Snippet 17 | 18 | Using Svelte's `{#snippet ...}` block, you can define a reusable snippet 19 | directly in your component's markup. For example, the snippet `strongSnippet` is 20 | defined to render its content in a `` tag: 21 | 22 | ```svelte 23 | {#snippet strongSnippet(content: string)} 24 | {content} 25 | {/snippet} 26 | ``` 27 | 28 | #### Script (Raw) Snippet 29 | 30 | Snippets can also be created programmatically in the script tag using the 31 | `createRawSnippet` function. This approach is useful when you do not need to use 32 | a Svelte component in your snippet. 33 | 34 | ```svelte 35 | 52 | ``` 53 | 54 | This `mailtoSnippet` generates a clickable email link, using the provided email 55 | address. 56 | 57 | ### Column Definitions 58 | 59 | Next, we define a set of columns for our table. The column definition array 60 | specifies how each cell should be rendered, and this is where `renderSnippet` 61 | comes in. This function allows you to use the snippets defined earlier to 62 | customize the cell content. 63 | 64 | Here's an example of how we use `renderSnippet` with our snippets in the column 65 | definitions: 66 | 67 | 68 | ```svelte 69 | 98 | 99 | {#snippet strongSnippet(content: string)} 100 | {content} 101 | {/snippet} 102 | ``` 103 | 104 | 105 | > 🗒️ NOTE: You can also use `renderSnippet` with the `header` field of each 106 | > column. 107 | 108 | ### Setting Up the Table 109 | 110 | After defining the columns, the next step is to set up the table using the 111 | `createSvelteTable` function. This function initializes the table with the 112 | specified data, columns, and core row model. 113 | 114 | 115 | ```svelte 116 | 151 | 152 | {#snippet strongSnippet(content: string)} 153 | {content} 154 | {/snippet} 155 | ``` 156 | 157 | 158 | ### Rendering the Table 159 | 160 | Finally, to render the table itself, we use the `FlexRender` component. 161 | `FlexRender` is responsible for dynamically rendering the table's cells based on 162 | the content and context provided. 163 | 164 | ```svelte 165 | 166 | 167 | 168 | {#each table.getHeaderGroups() as headerGroup} 169 | {#each headerGroup.headers as header} 170 | 173 | {/each} 174 | {/each} 175 | 176 | 177 | 178 | {#each table.getRowModel().rows as row} 179 | 180 | {#each row.getVisibleCells() as cell} 181 | 184 | {/each} 185 | 186 | {/each} 187 | 188 |
171 | 172 |
182 | 183 |
189 | ``` 190 | -------------------------------------------------------------------------------- /src/lib/table/README.md: -------------------------------------------------------------------------------- 1 | # Table Adapter for Svelte 2 | 3 | This folder contains a custom adapter for Svelte 5. 4 | 5 | ## Prerequisites 6 | 7 | Make sure you have `@tanstack/table-core` installed: 8 | 9 | ```bash 10 | pnpm install @tanstack/table-core 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Defining Columns 16 | 17 | Column definitions are very similar to the existing adapter for TanStack Table 18 | v8. 19 | 20 | ```svelte 21 | 50 | ``` 51 | 52 | > 🔗 Link: The code abode came from the [basic 53 | > example](/src/routes/examples/basic). 54 | 55 | If you want to render a component in a cell or header, you can use the 56 | `renderComponent` or `renderSnippet` functions. 57 | 58 | ```svelte 59 | 87 | ``` 88 | 89 | > 🔗 Link: The code abode came from the [select 90 | > example](/src/routes/examples/select), and the 91 | > [homepage](/src/routes/+page.svelte). 92 | 93 | > 📝 NOTE: `renderSnippet` will only accept a snippet that takes one argument. 94 | 95 | ### Rendering the Table 96 | 97 | This is very similar to the existing adapter for TanStack Table, but here, 98 | you'll use `FlexRender`. 99 | 100 | ```svelte 101 | 108 | 109 | 110 | 111 | 112 | {#each table.getHeaderGroups() as headerGroup} 113 | {#each headerGroup.headers as header} 114 | 117 | {/each} 118 | {/each} 119 | 120 | 121 | 122 | {#each table.getRowModel().rows as row} 123 | 124 | {#each row.getVisibleCells() as cell} 125 | 128 | {/each} 129 | 130 | {/each} 131 | 132 |
115 | 116 |
126 | 127 |
133 | ``` 134 | 135 | > 🔗 Link: The code abode came from the [select 136 | > example](/src/routes/examples/select). 137 | 138 | If your table only has POJO's, you don't need to use `FlexRender`: 139 | 140 | ```svelte 141 | 142 | 143 | 144 | {#each table.getHeaderGroups() as headerGroup} 145 | {#each headerGroup.headers as header} 146 | 147 | {/each} 148 | {/each} 149 | 150 | 151 | 152 | {#each table.getRowModel().rows as row} 153 | 154 | {#each row.getVisibleCells() as cell} 155 | 156 | {/each} 157 | 158 | {/each} 159 | 160 |
{header.column.columnDef.header}
{cell.getValue()}
161 | ``` 162 | 163 | > 🔗 Link: The code abode came from the [basic example](/src/routes/basic). 164 | 165 | ### State 166 | 167 | In this adapter, state is used very similarly to the SolidJS adapter since both 168 | Svelte 5 and SolidJS use signals. 169 | 170 | For each state that you'd like reactivity for, you'll need to create a rune, and 171 | an updater function. 172 | 173 | ```svelte 174 | 226 | 227 | 228 | 229 | 230 | ``` 231 | 232 | > 🔗 Link: The code abode came from the [select 233 | > example](/src/routes/examples/select). 234 | 235 | ## Examples 236 | 237 | Check out the `examples` route for functional examples. 238 | -------------------------------------------------------------------------------- /src/routes/examples/row-expansion/README.md: -------------------------------------------------------------------------------- 1 | In this guide, we'll focus on enhancing a table by adding expandable rows that 2 | allow users to show and hide nested data. We'll go through the steps of creating 3 | an expansion button component, defining the columns with an expansion control, 4 | setting up a reactive state for row expansion, and ensuring that the table 5 | properly handles nested data. 6 | 7 | **You should already know how to make a basic table before proceeding.** 8 | 9 | > 🗒️ Note: This process works for tables with hierarchical data structures. 10 | 11 | ### Creating the Expansion Button 12 | 13 | We start by creating a simple expansion button component called `ExpandButton`. 14 | This component will handle the expansion control rendering for each row in the 15 | table. 16 | 17 | ```svelte 18 | 19 | 28 | 29 | 34 | ``` 35 | 36 | ### Creating an Expansion Column 37 | 38 | Next, we define the table columns using a column helper, with a specific focus 39 | on adding an expansion column that uses the `ExpandButton` component. 40 | 41 | ```svelte 42 | 43 | 69 | ``` 70 | 71 | The expansion button's `onclick` event toggles the expansion state of the 72 | respective row when clicked. 73 | 74 | ### Setting Up Expansion State 75 | 76 | To manage the row expansion state reactively, we use a `$state` rune to create a 77 | reactive signal that tracks which rows are expanded. 78 | 79 | ```svelte 80 | 107 | ``` 108 | 109 | ### Setting Up the Table with Sub-Rows 110 | 111 | When setting up the table, we need to configure both the expansion state and 112 | define how sub-rows are retrieved. This setup enables the hierarchical data 113 | display. 114 | 115 | 116 | ```svelte 117 | 172 | ``` 173 | 174 | 175 | ### Rendering the Table with Expansion Controls 176 | 177 | Finally, we render the table and include a button that allows users to expand or 178 | collapse all rows at once. 179 | 180 | ```svelte 181 |
182 |

Actions

183 |
184 | 191 |
192 | 193 | 194 | 195 | 196 | {#each table.getHeaderGroups() as headerGroup} 197 | {#each headerGroup.headers as header} 198 | 201 | {/each} 202 | {/each} 203 | 204 | 205 | 206 | {#each table.getRowModel().rows as row} 207 | 208 | {#each row.getVisibleCells() as cell} 209 | 212 | {/each} 213 | 214 | {/each} 215 | 216 |
199 | 200 |
210 | 211 |
217 | ``` 218 | 219 | The expanded rows will automatically be included in the table's row model, 220 | thanks to the `getExpandedRowModel` plugin. The table will show both parent rows 221 | and their expanded children when the expansion state changes. 222 | -------------------------------------------------------------------------------- /src/routes/examples/row-selection/README.md: -------------------------------------------------------------------------------- 1 | In this guide, we'll focus on enhancing a table by adding a selection state that 2 | allows users to select rows using checkboxes. We'll go through the steps of 3 | creating a checkbox wrapper component, defining the columns with a selection 4 | column, setting up a reactive state for row selection, and ensuring that the 5 | table responds to selection changes. 6 | 7 | **You should already know how to make a basic table before proceeding.** 8 | 9 | > 🗒️ Note: This process works for all kinds of table state. 10 | 11 | ### Creating the Checkbox Wrapper 12 | 13 | We start by creating a simple checkbox wrapper component called `TableCheckbox`. 14 | This component will handle the checkbox rendering for each row in the table. 15 | 16 | ```svelte 17 | 18 | 25 | 26 | 27 | ``` 28 | 29 | ### Creating a Selection Column 30 | 31 | Next, we define the table columns using a column helper, with a specific focus 32 | on adding a selection column that uses the `TableCheckbox` component. 33 | 34 | ```svelte 35 | 36 | 63 | ``` 64 | 65 | The checkbox's `onchange` event toggles the selection of the respective row when 66 | clicked. 67 | 68 | ### Setting Up Selection State 69 | 70 | To manage the row selection state reactively, we use a `$state` rune to create a 71 | reactive signal that tracks which rows are selected. 72 | 73 | ```svelte 74 | 102 | ``` 103 | 104 | ### Creating the Updater Function 105 | 106 | To handle changes in the row selection state, we define an updater function. 107 | This function will be called whenever a row's selection state changes. 108 | 109 | 110 | ```svelte 111 | 152 | ``` 153 | 154 | 155 | - **State Update**: The updater function modifies the `rowSelectionState`, 156 | ensuring that the state rune and the table itself are synchronized. 157 | - **Reactivity**: Reassigning the `$state` triggers the reactive updates 158 | automatically. 159 | 160 | ### Setting Up the Table 161 | 162 | When setting up the table, it is essential to make the `rowSelection` state a 163 | getter to ensure reactivity. This way, the table automatically updates when the 164 | selection state changes. 165 | 166 | 167 | ```svelte 168 | 223 | ``` 224 | 225 | 226 | ### Rendering the Table in the Markup 227 | 228 | Finally, we render the table and include a button that allows users to toggle 229 | the selection of all rows at once. 230 | 231 | ```svelte 232 |
233 |

Actions

234 |
235 | 242 |
243 | 244 | 245 | 246 | 247 | {#each table.getHeaderGroups() as headerGroup} 248 | {#each headerGroup.headers as header} 249 | 252 | {/each} 253 | {/each} 254 | 255 | 256 | 257 | {#each table.getRowModel().rows as row} 258 | 259 | {#each row.getVisibleCells() as cell} 260 | 263 | {/each} 264 | 265 | {/each} 266 | 267 |
250 | 251 |
261 | 262 |
268 | ``` 269 | -------------------------------------------------------------------------------- /src/styles/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type='checkbox'], 335 | input[type='radio'] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type='number']::-webkit-inner-spin-button, 347 | input[type='number']::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type='search'] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type='search']::-webkit-search-cancel-button, 371 | input[type='search']::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | --------------------------------------------------------------------------------