Svelte Integration for Datatable The Problem

When using the datatable component with custom cell renderers, the underlying TanStack Table library expects cell content to be returned as Lit templates. This creates a problem for Svelte users:

// ❌ This forces Svelte users to import Lit
import { html } from 'lit'

const columns = [
  {
    id: 'status',
    cell: (row) => html`<dap-ds-badge variant="success">${row.status}</dap-ds-badge>`
  }
]

Why this is a problem:

  • Svelte developers must learn and import Lit's html tagged template function
  • Adds unnecessary Lit as a dependency in Svelte projects
  • Defeats the purpose of framework-agnostic web components
  • Can't use Svelte components in table cells without Lit
The Solution: createSvelteCell Helper

The design system provides a createSvelteCell helper that allows you to render Svelte components in datatable cells without importing Lit.

Installation

The Svelte helper is included in the main design system package:

npm i dap-design-system
Basic Usage
import { createSvelteCell } from 'dap-design-system/helpers/svelte'
import StatusBadge from './StatusBadge.svelte'

const columns = [
  {
    id: 'status',
    header: 'Status',
    cell: (info) => createSvelteCell({
      component: StatusBadge,
      props: {
        status: info.row.original.status,
        variant: 'success'
      }
    })
  }
]
How It Works

The createSvelteCell helper:

  1. Automatically detects Svelte version (4 or 5)
  2. Uses the appropriate mounting API (mount() for Svelte 5, new Component() for Svelte 4)
  3. Properly handles component lifecycle (creation and destruction)
  4. Manages cleanup when cells are removed (pagination, sorting, filtering)
Type Signature
interface SvelteCellOptions {
  component: any
  props?: Record<string, any>
  on?: Record<string, (...args: any[]) => void>
  context?: Map<any, any> | Record<string, any>
}

function createSvelteCell(
  componentOrOptions: any | SvelteCellOptions,
  props?: Record<string, any>
): RenderCallbackContent
Complete Example with Remote Data (Svelte 5)

This example demonstrates:

  • Server-side pagination
  • Loading states
  • Svelte 5 components as cell renderers
  • Event handling with callback props
1. Create Your Svelte Components

StatusBadge.svelte:

&lt;script lang=&quot;ts&quot;&gt;
  interface Props {
    status: string;
    onclick?: (status: string) =&gt; void;
  }

  let { status, onclick }: Props = $props();

  // Determine variant based on status
  const variant = $derived(
    status === &#39;active&#39; ? &#39;success&#39; : status === &#39;pending&#39; ? &#39;warning&#39; : &#39;danger&#39;
  );

  function handleClick() {
    onclick?.(status);
  }
&lt;/script&gt;

&lt;dap-ds-badge {variant} onclick={handleClick} style=&quot;cursor: pointer;&quot;&gt;
  {status}
&lt;/dap-ds-badge&gt;

UserAvatar.svelte:

&lt;script lang=&quot;ts&quot;&gt;
  interface Props {
    name: string;
    email: string;
    onAvatarClick?: (name: string) =&gt; void;
  }

  let { name, email, onAvatarClick }: Props = $props();

  // Generate initials from name
  const initials = $derived(
    name
      .split(&#39; &#39;)
      .map((n) =&gt; n[0])
      .join(&#39;&#39;)
      .toUpperCase()
      .slice(0, 2)
  );

  function handleAvatarClick() {
    onAvatarClick?.(name);
  }
&lt;/script&gt;

&lt;div class=&quot;user-cell&quot;&gt;
  &lt;dap-ds-avatar
    initials={initials}
    size=&quot;sm&quot;
    onclick={handleAvatarClick}
    interactive
    aria-label=&quot;View profile for {name}&quot;
  /&gt;
  &lt;div class=&quot;user-info&quot;&gt;
    &lt;span class=&quot;user-name&quot;&gt;{name}&lt;/span&gt;
    &lt;span class=&quot;user-email&quot;&gt;{email}&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;style&gt;
  .user-cell {
    display: flex;
    align-items: center;
    gap: 0.75rem;
  }

  .user-info {
    display: flex;
    flex-direction: column;
  }

  .user-name {
    font-weight: 500;
  }

  .user-email {
    font-size: 0.875rem;
    color: var(--dds-color-text-secondary, #666);
  }
&lt;/style&gt;

ActionButtons.svelte:

&lt;script lang=&quot;ts&quot;&gt;
  interface RowData {
    id: number;
    name: string;
    email: string;
    status: string;
  }

  interface Props {
    rowData: RowData;
    onEdit?: (rowData: RowData) =&gt; void;
    onDelete?: (rowData: RowData) =&gt; void;
  }

  let { rowData, onEdit, onDelete }: Props = $props();

  function handleEdit() {
    onEdit?.(rowData);
  }

  function handleDelete() {
    onDelete?.(rowData);
  }
&lt;/script&gt;

&lt;div class=&quot;action-buttons&quot;&gt;
  &lt;dap-ds-button variant=&quot;outline&quot; size=&quot;sm&quot; onclick={handleEdit}&gt;
    &lt;dap-ds-icon-system-edit-line slot=&quot;prefix&quot; /&gt;
    Edit
  &lt;/dap-ds-button&gt;
  &lt;dap-ds-button variant=&quot;outline&quot; size=&quot;sm&quot; color=&quot;danger&quot; onclick={handleDelete}&gt;
    &lt;dap-ds-icon-system-delete-bin-line slot=&quot;prefix&quot; /&gt;
    Delete
  &lt;/dap-ds-button&gt;
&lt;/div&gt;

&lt;style&gt;
  .action-buttons {
    display: flex;
    gap: 0.5rem;
  }
&lt;/style&gt;
2. Set Up Your Datatable Component

+page.svelte:

&lt;script lang=&quot;ts&quot;&gt;
  import { onMount } from &#39;svelte&#39;;
  import { createSvelteCell } from &#39;dap-design-system/helpers/svelte&#39;;
  import StatusBadge from &#39;$lib/components/StatusBadge.svelte&#39;;
  import UserAvatar from &#39;$lib/components/UserAvatar.svelte&#39;;
  import ActionButtons from &#39;$lib/components/ActionButtons.svelte&#39;;

  // Define User type
  interface User {
    id: number;
    name: string;
    email: string;
    status: string;
  }

  // State
  let datatable: HTMLElement | null = $state(null);
  let currentPage = $state(0);
  const pageSize = 10;

  // Mock data generator (replace with API call)
  function generateMockUsers(page: number, pageSize: number) {
    const total = 100;
    const start = (page - 1) * pageSize;
    const end = Math.min(start + pageSize, total);

    const statuses = [&#39;active&#39;, &#39;pending&#39;, &#39;inactive&#39;];
    const data: User[] = [];

    for (let i = start; i &lt; end; i++) {
      data.push({
        id: i + 1,
        name: `User ${i + 1}`,
        email: `user${i + 1}@example.com`,
        status: statuses[i % 3]
      });
    }

    return { data, total };
  }

  // Event handlers
  function handleEdit(rowData: User) {
    alert(`Editing user: ${rowData.name}`);
  }

  function handleDelete(rowData: User) {
    if (confirm(`Delete user ${rowData.name}?`)) {
      fetchData();
    }
  }

  function handleStatusClick(status: string) {
    console.log(&#39;Status clicked:&#39;, status);
  }

  function handleAvatarClick(name: string) {
    alert(`Avatar clicked for: ${name}`);
  }

  // Column definitions using createSvelteCell for Svelte components
  const columns = [
    {
      id: &#39;id&#39;,
      header: &#39;ID&#39;,
      accessorKey: &#39;id&#39;,
      size: 60
    },
    {
      id: &#39;user&#39;,
      header: &#39;User&#39;,
      size: 250,
      cell: (info: { row: { original: User } }) =&gt; {
        return createSvelteCell({
          component: UserAvatar,
          props: {
            name: info.row.original.name,
            email: info.row.original.email,
            onAvatarClick: handleAvatarClick
          }
        });
      }
    },
    {
      id: &#39;status&#39;,
      header: &#39;Status&#39;,
      cell: (info: { row: { original: User } }) =&gt; {
        return createSvelteCell({
          component: StatusBadge,
          props: {
            status: info.row.original.status,
            onclick: handleStatusClick
          }
        });
      }
    },
    {
      id: &#39;actions&#39;,
      header: &#39;Actions&#39;,
      cell: (info: { row: { original: User } }) =&gt; {
        return createSvelteCell({
          component: ActionButtons,
          props: {
            rowData: info.row.original,
            onEdit: handleEdit,
            onDelete: handleDelete
          }
        });
      }
    }
  ];

  // Fetch data function
  function fetchData() {
    if (!datatable) return;

    // Update loading state and clear data
    (datatable as any).loading = true;
    (datatable as any).data = [];

    // Simulate network delay
    setTimeout(() =&gt; {
      const result = generateMockUsers(currentPage + 1, pageSize);

      // Update datatable with new data
      (datatable as any).data = result.data;
      (datatable as any).rowCount = result.total;
      (datatable as any).pagination = { pageIndex: currentPage, pageSize };
      (datatable as any).loading = false;
    }, 500);
  }

  // Initialize datatable
  function initTable() {
    const container = document.getElementById(&#39;datatable-container&#39;);
    if (!container) return;

    // Create datatable element
    datatable = document.createElement(&#39;dap-ds-datatable&#39;);

    // Set static properties
    (datatable as any).columns = columns;
    (datatable as any).pager = true;
    (datatable as any).manualPagination = true;
    datatable.setAttribute(&#39;loading-type&#39;, &#39;spinner&#39;);

    // Add event listener for pagination
    datatable.addEventListener(&#39;dds-pagination-change&#39;, ((event: CustomEvent) =&gt; {
      const newPage = event.detail.pagination.pageIndex;
      if (newPage !== currentPage) {
        currentPage = newPage;
        fetchData();
      }
    }) as EventListener);

    container.appendChild(datatable);
  }

  // Initialize on mount
  onMount(async () =&gt; {
    // Import design system components
    await import(&#39;dap-design-system/components&#39;);
    await import(&#39;dap-design-system/icons&#39;);

    initTable();
    fetchData();
  });
&lt;/script&gt;

&lt;main&gt;
  &lt;h1&gt;Datatable with Svelte&lt;/h1&gt;
  &lt;div id=&quot;datatable-container&quot;&gt;&lt;/div&gt;
&lt;/main&gt;
API Reference createSvelteCell(options)

Creates a Svelte component renderer for datatable cells.

Parameters:

  • options.component (any, required): The Svelte component to render
  • options.props (Object, optional): Props to pass to the component
  • options.on (Object, optional): Event handlers (for Svelte 4 event dispatching)
  • options.context (Map | Object, optional): Context to pass to the component

Shorthand syntax:

// Instead of passing options object:
createSvelteCell({
  component: MyComponent,
  props: { value: 'test' }
})

// You can use shorthand:
createSvelteCell(MyComponent, { value: 'test' })

Returns: A render callback that the datatable will use to mount the Svelte component.

Event Handling Svelte 5 (Recommended)

In Svelte 5, use callback props instead of event dispatching:

&lt;!-- Component --&gt;
&lt;script lang=&quot;ts&quot;&gt;
  interface Props {
    value: string;
    onClick?: (value: string) =&gt; void;
  }

  let { value, onClick }: Props = $props();
&lt;/script&gt;

&lt;button onclick={() =&gt; onClick?.(value)}&gt;Click me&lt;/button&gt;
// Usage
createSvelteCell({
  component: MyComponent,
  props: {
    value: 'test',
    onClick: (value) => console.log('clicked', value)
  }
})
Svelte 4 (Legacy)

For Svelte 4 components using createEventDispatcher:

&lt;!-- Component --&gt;
&lt;script&gt;
  import { createEventDispatcher } from &#39;svelte&#39;;
  const dispatch = createEventDispatcher();
&lt;/script&gt;

&lt;button on:click={() =&gt; dispatch(&#39;click&#39;, &#39;value&#39;)}&gt;Click me&lt;/button&gt;
// Usage
createSvelteCell({
  component: MyComponent,
  props: { value: 'test' },
  on: {
    click: (event) => console.log('clicked', event.detail)
  }
})
Important Notes Svelte 5 vs Svelte 4

The helper automatically detects which Svelte version you're using:

FeatureSvelte 4Svelte 5
Component APIClass-based (new Component())Function-based (mount())
Propsexport let proplet { prop } = $props()
EventscreateEventDispatcherCallback props
Reactivity$: reactive statements$derived(), $effect()
SvelteKit Configuration

No special configuration is needed for SvelteKit. The design system web components work out of the box.

Component Lifecycle

The createSvelteCell helper automatically manages component lifecycle:

  • Mount: Components are created and mounted when cells are rendered
  • Unmount: Components are properly destroyed when cells are removed (e.g., pagination, sorting)
  • Cleanup: Component instances are unmounted to prevent memory leaks
Performance Considerations

Each cell with a Svelte component creates its own component instance. For tables with many rows:

  • Consider using virtual scrolling for large datasets
  • Use manualPagination for server-side pagination (recommended)
  • Keep cell components lightweight
Troubleshooting Components Not Rendering

Make sure you've imported the design system components before using them:

onMount(async () => {
  await import('dap-design-system/components');
  await import('dap-design-system/icons');
  // Now initialize your datatable
});
TypeScript Support

The Svelte helper includes full TypeScript support:

import { createSvelteCell } from 'dap-design-system/helpers/svelte'
import type { SvelteCellOptions, SvelteCellRenderer } from 'dap-design-system/helpers/svelte'

// Type-safe options
const options: SvelteCellOptions = {
  component: MyComponent,
  props: {
    value: 'test'
  }
}

const cellRenderer = createSvelteCell(options)

// Type-safe cell renderer
interface User {
  id: number;
  status: string;
}

const statusCell: SvelteCellRenderer<User> = (row) => createSvelteCell({
  component: StatusBadge,
  props: { status: row.status }
})
Using with SvelteKit

When using SvelteKit, you can import components from $lib:

import StatusBadge from '$lib/components/StatusBadge.svelte';

Make sure your svelte.config.js has the alias configured (this is default in SvelteKit):

// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

export default {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter(),
    alias: {
      $lib: './src/lib'
    }
  }
};