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
htmltagged 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
createVueCell HelperThe design system provides a createVueCell helper that allows you to render Vue components in datatable cells without importing Lit.
The Vue helper is included in the main design system package:
npm i dap-design-system
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'
}
})
}
]
The createVueCell helper:
- Creates a Vue app instance for each cell
- Mounts your Vue component into the cell container
- Properly handles component lifecycle (creation and destruction)
- Manages cleanup when cells are removed (pagination, sorting, filtering)
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
This example demonstrates:
- Server-side pagination
- Loading states
- Vue components as cell renderers
- Event handling
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>
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>
createVueCell(options)Creates a Vue component renderer for datatable cells.
Parameters:
options.component(Component, required): The Vue component to renderoptions.props(Object, optional): Props to pass to the componentoptions.on(Object, optional): Event handlers for component eventsoptions.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.
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
}
})
.prop ModifierWhen 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
loadingistruenot"true"string) - Objects (
data,columns,pagination) - Arrays
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
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-paginationfor server-side pagination (recommended) - Keep cell components lightweight
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-')
}
}
})
]
}
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
}
})
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)