Customizing Generated Code

Custom Columns

Customize table columns with computed values, custom rendering, and custom components

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: true in the generator
  • Links to the users collection
  • Displays with a CardMini component showing user details
  • Can be hidden with :hide-default-columns="{ updatedBy: true }"
Don't add updatedBy to your schema JSON files - it's metadata, not a business field. The generator adds it automatically.

Show data from related collections.

Query Examples: For complete 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 }
}