Custom Columns
Customize how data is displayed in your tables by adding computed columns, custom renderers, and custom components.
Add Custom Columns
Columns are defined in your composable and control how data appears in table views.
Basic Column Definition
// layers/shop/composables/useProducts.ts
export function useShopProducts() {
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'price', label: 'Price' },
{ key: 'inStock', label: 'In Stock' }
]
return {
columns,
// ...other exports
}
}
Computed Columns
Add virtual columns that don't exist in your data but are computed on the fly.
Using Render Function
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'price', label: 'Price' },
// Add computed column
{
key: 'status',
label: 'Status',
render: (row) => row.inStock ? 'Available' : 'Out of Stock'
}
]
Computing Values
const columns = [
{ key: 'name', label: 'Product' },
{ key: 'price', label: 'Price' },
{ key: 'cost', label: 'Cost' },
// Calculate profit
{
key: 'profit',
label: 'Profit',
render: (row) => row.price - row.cost
},
// Calculate margin percentage
{
key: 'margin',
label: 'Margin %',
render: (row) => {
const margin = ((row.price - row.cost) / row.price) * 100
return `${margin.toFixed(2)}%`
}
}
]
Formatting Values
const columns = [
{
key: 'price',
label: 'Price',
render: (row) => {
// Format as currency
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(row.price)
}
},
{
key: 'createdAt',
label: 'Created',
render: (row) => {
// Format date
return new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(row.createdAt))
}
},
{
key: 'quantity',
label: 'Quantity',
render: (row) => {
// Add thousands separator
return row.quantity.toLocaleString()
}
}
]
Custom Components
Use custom Vue components for complex column rendering.
Basic Component Column
const columns = [
{ key: 'name', label: 'Product' },
{ key: 'price', label: 'Price' },
// Add custom component
{
key: 'actions',
label: '',
component: 'ProductActions' // Your custom component
}
]
Then create the component:
<!-- layers/shop/components/products/ProductActions.vue -->
<script setup lang="ts">
defineProps<{
row: any
}>()
const { open } = useCrouton()
</script>
<template>
<div class="flex gap-2">
<UButton size="xs" @click="open('update', 'shopProducts', [row.id])">
Edit
</UButton>
<UButton size="xs" color="red" @click="open('delete', 'shopProducts', [row.id])">
Delete
</UButton>
</div>
</template>
Badge Component
Display status badges:
const columns = [
{ key: 'name', label: 'Product' },
{
key: 'status',
label: 'Status',
component: 'ProductStatus'
}
]
<!-- layers/shop/components/products/ProductStatus.vue -->
<script setup lang="ts">
defineProps<{
row: any
}>()
const statusColor = computed(() => {
switch (row.status) {
case 'active': return 'green'
case 'draft': return 'yellow'
case 'archived': return 'gray'
default: return 'gray'
}
})
</script>
<template>
<UBadge :color="statusColor">
{{ row.status }}
</UBadge>
</template>
Dynamic Columns
Use computed columns for reactive behavior.
Based on User Permissions
export function useShopProducts() {
const { hasPermission } = useAuth()
const columns = computed(() => [
{ key: 'name', label: 'Name' },
{ key: 'price', label: 'Price' },
// Show cost only to admins
...(hasPermission('view_cost') ? [{
key: 'cost',
label: 'Cost'
}] : []),
// Show profit only to managers
...(hasPermission('view_profit') ? [{
key: 'profit',
label: 'Profit',
render: (row) => row.price - row.cost
}] : [])
])
return { columns }
}
Based on Screen Size
export function useShopProducts() {
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const columns = computed(() => [
{ key: 'name', label: 'Name' },
// Hide on mobile
...(!isMobile.value ? [
{ key: 'description', label: 'Description' },
{ key: 'category', label: 'Category' }
] : []),
{ key: 'price', label: 'Price' },
{ key: 'actions', label: '', component: 'ProductActions' }
])
return { columns }
}
Default Columns with Special Rendering
Some default columns use custom rendering automatically:
updatedBy Column
The updatedBy column automatically displays user information using a CardMini component:
<template>
<CroutonList
:rows="items"
:columns="columns"
collection="shopProducts"
/>
<!-- updatedBy column automatically shows user card -->
</template>
The updatedBy field:
- Is automatically added to all collections when
useMetadata: truein the generator - Links to the users collection
- Displays with a CardMini component showing user details
- Can be hidden with
:hide-default-columns="{ updatedBy: true }"
updatedBy to your schema JSON files - it's metadata, not a business field. The generator adds it automatically.Displaying Related Data
Show data from related collections.
useCollectionQuery patterns, see Querying Data.Client-Side Lookup
export function useShopProducts() {
const { items: categories } = await useCollectionQuery('shopCategories')
// Create lookup map
const categoryMap = computed(() =>
Object.fromEntries(categories.value.map(c => [c.id, c]))
)
const columns = [
{ key: 'name', label: 'Product' },
{ key: 'price', label: 'Price' },
// Look up category name
{
key: 'category',
label: 'Category',
render: (row) => categoryMap.value[row.categoryId]?.name || 'N/A'
}
]
return { columns }
}
Server-Side Join
If your API returns joined data:
const columns = [
{ key: 'name', label: 'Product' },
{ key: 'price', label: 'Price' },
// Access nested data directly
{
key: 'category.name',
label: 'Category'
}
]
Sortable Columns
Enable sorting on columns:
const columns = [
{
key: 'name',
label: 'Name',
sortable: true
},
{
key: 'price',
label: 'Price',
sortable: true
},
{
key: 'createdAt',
label: 'Created',
sortable: true
}
]
Column Alignment
Control text alignment:
const columns = [
{
key: 'name',
label: 'Name',
align: 'left' // Default
},
{
key: 'price',
label: 'Price',
align: 'right' // Right-align numbers
},
{
key: 'status',
label: 'Status',
align: 'center' // Center badges
}
]
Best Practices
Keep Render Functions Simple
// Good: Simple, readable
{
key: 'status',
label: 'Status',
render: (row) => row.inStock ? 'Available' : 'Sold Out'
}
// Bad: Complex logic in render
{
key: 'status',
label: 'Status',
render: (row) => {
const now = new Date()
const created = new Date(row.createdAt)
const daysSince = (now - created) / (1000 * 60 * 60 * 24)
if (row.featured && daysSince < 7) return 'New & Featured'
if (row.featured) return 'Featured'
if (daysSince < 7) return 'New'
if (row.inStock) return 'Available'
return 'Sold Out'
}
}
// Better: Extract to computed or helper function
const getProductStatus = (row) => {
const now = new Date()
const created = new Date(row.createdAt)
const daysSince = (now - created) / (1000 * 60 * 60 * 24)
if (row.featured && daysSince < 7) return 'New & Featured'
if (row.featured) return 'Featured'
if (daysSince < 7) return 'New'
if (row.inStock) return 'Available'
return 'Sold Out'
}
{
key: 'status',
label: 'Status',
render: (row) => getProductStatus(row)
}
Use Components for Complex UI
Move complex rendering to components:
// Instead of complex render function
{
key: 'actions',
label: 'Actions',
component: 'ProductActions' // Better for complex UI
}
Cache Lookups
Cache expensive computations:
export function useShopProducts() {
const { items: categories } = await useCollectionQuery('shopCategories')
// Cache the lookup map
const categoryMap = computed(() =>
Object.fromEntries(categories.value.map(c => [c.id, c]))
)
// Reuse cached map in render functions
const columns = [
{
key: 'category',
label: 'Category',
render: (row) => categoryMap.value[row.categoryId]?.name || 'N/A'
}
]
return { columns }
}
Related Topics
- Custom Components - Rich UI components
- Working with Relations - Display related data
- Queries - Fetch data for columns