Vue 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 Vue users:

// ❌ This forces Vue 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:

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

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

Installation

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

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

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

The createVueCell helper:

  1. Creates a Vue app instance for each cell
  2. Mounts your Vue component into the cell container
  3. Properly handles component lifecycle (creation and destruction)
  4. Manages cleanup when cells are removed (pagination, sorting, filtering)
Type Signature
interface VueCellOptions {
  component: Component
  props?: Record<string, any>
  on?: Record<string, (...args: any[]) => void>
  children?: VNode | VNode[] | string
}

function createVueCell(
  componentOrOptions: Component | VueCellOptions,
  props?: Record<string, any>
): RenderCallbackContent
Complete Example with Remote Data

This example demonstrates:

  • Server-side pagination
  • Loading states
  • Vue components as cell renderers
  • Event handling
1. Create Your Vue Components

StatusBadge.vue:

<script setup>
const props = defineProps({
  status: { type: String, required: true },
  variant: { type: String, default: 'default' }
})

const emit = defineEmits(['click'])

const handleClick = () => {
  emit('click', props.status)
}
</script>

<template>
  <dap-ds-badge :variant="variant" @click="handleClick">
    {{ status }}
  </dap-ds-badge>
</template>

ActionsCell.vue:

<script setup>
const props = defineProps({
  rowData: { type: Object, required: true }
})

const emit = defineEmits(['edit', 'delete'])

const handleEdit = () => {
  emit('edit', props.rowData)
}

const handleDelete = () => {
  emit('delete', props.rowData)
}
</script>

<template>
  <div style="display: flex; gap: 0.5rem">
    <dap-ds-button size="xs" variant="primary" @click="handleEdit">
      Edit
    </dap-ds-button>
    <dap-ds-button size="xs" variant="danger" @click="handleDelete">
      Delete
    </dap-ds-button>
  </div>
</template>
2. Set Up Your Datatable Component

DatatableDemo.vue:

<script setup>
import { ref, watch, onMounted } from 'vue'
import { createVueCell } from 'dap-design-system/helpers/vue'
import StatusBadge from './StatusBadge.vue'
import ActionsCell from './ActionsCell.vue'

// State
const users = ref([])
const loading = ref(true)
const rowCount = ref(0)
const pagination = ref({
  pageIndex: 0,
  pageSize: 10
})

// Simulate API call
const fetchData = async () => {
  loading.value = true
  users.value = [] // Clear data to show loading state

  // Simulate network delay
  await new Promise(resolve => setTimeout(resolve, 500))

  // In a real app, this would be an API call
  const result = await fetch(`/api/users?page=${pagination.value.pageIndex}&size=${pagination.value.pageSize}`)
  const data = await result.json()

  users.value = data.users
  rowCount.value = data.total
  loading.value = false
}

// Column definitions using createVueCell
const columns = ref([
  {
    id: 'id',
    header: 'ID',
    accessorKey: 'id',
    size: 60
  },
  {
    id: 'name',
    header: 'Name',
    accessorKey: 'name'
  },
  {
    id: 'email',
    header: 'Email',
    accessorKey: 'email'
  },
  {
    id: 'status',
    header: 'Status',
    cell: (info) => {
      const row = info.row.original
      const variant = row.status === 'active' ? 'success'
        : row.status === 'pending' ? 'warning'
        : 'danger'

      return createVueCell({
        component: StatusBadge,
        props: {
          status: row.status,
          variant: variant
        },
        on: {
          click: (status) => {
            console.log('Status badge clicked:', status)
          }
        }
      })
    }
  },
  {
    id: 'actions',
    header: 'Actions',
    cell: (info) => {
      return createVueCell({
        component: ActionsCell,
        props: {
          rowData: info.row.original
        },
        on: {
          edit: (rowData) => handleEdit(rowData),
          delete: (rowData) => handleDelete(rowData)
        }
      })
    }
  }
])

const handleEdit = (rowData) => {
  alert(`Editing user: ${rowData.name}`)
}

const handleDelete = (rowData) => {
  if (confirm(`Delete user ${rowData.name}?`)) {
    fetchData()
  }
}

const handlePaginationChange = (event) => {
  pagination.value = event.detail.pagination
}

// Watch pagination changes
watch(pagination, () => {
  fetchData()
}, { deep: true })

// Initial data fetch
onMounted(() => {
  fetchData()
})
</script>

<template>
  <div class="datatable-demo">
    <h2>Vue Datatable with Remote Data</h2>

    <dap-ds-datatable
      :data="users"
      :columns="columns"
      :rowCount="rowCount"
      :pager="true"
      :pagination="pagination"
      :manualPagination="true"
      :loading="loading"
      loadingType="spinner"
      @dds-pagination-change="handlePaginationChange"
    />
  </div>
</template>
API Reference createVueCell(options)

Creates a Vue component renderer for datatable cells.

Parameters:

  • options.component (Component, required): The Vue component to render
  • options.props (Object, optional): Props to pass to the component
  • options.on (Object, optional): Event handlers for component events
  • options.children (VNode | VNode[] | string, optional): Child content

Shorthand syntax:

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

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

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

Event Handling

Events are automatically converted from Vue's naming convention to component props:

createVueCell({
  component: MyComponent,
  on: {
    click: (data) => console.log('clicked', data),      // becomes onClick
    update: (data) => console.log('updated', data),     // becomes onUpdate
    customEvent: (data) => console.log('custom', data)  // becomes onCustomEvent
  }
})
Important Notes Using .prop Modifier

When binding properties to the datatable web component in Vue, use the .prop modifier for complex values:

<dap-ds-datatable
  :data.prop="users"
  :columns.prop="columns"
  :pagination.prop="pagination"
  :manual-pagination.prop="true"
  :loading.prop="loading"
/>

This ensures values are set as properties (not attributes) on the DOM element, which is crucial for:

  • Boolean values (so loading is true not "true" string)
  • Objects (data, columns, pagination)
  • Arrays
Component Lifecycle

The createVueCell 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: Vue app instances are unmounted to prevent memory leaks
Performance Considerations

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

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

Make sure you've configured Vue to recognize dap- prefixed custom elements:

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag.startsWith('dap-')
        }
      }
    })
  ]
}
Events Not Firing

Ensure you're using the on property in createVueCell options:

// ✅ Correct
createVueCell({
  component: MyComponent,
  on: {
    click: handleClick
  }
})

// ❌ Incorrect - this sets a prop, not an event handler
createVueCell({
  component: MyComponent,
  props: {
    onClick: handleClick
  }
})
TypeScript Support

The Vue helper includes full TypeScript support:

import { createVueCell } from 'dap-design-system/helpers/vue'
import type { VueCellOptions } from 'dap-design-system/helpers/vue'

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

const cellRenderer = createVueCell(options)