@radon-be/quasar-app-extension-zod-table
v0.2.2
Published
Editable table that takes the column definitions from a zod schema
Downloads
434
Readme
Quasar App Extension Zod Table
An editable table component for Quasar that automatically generates columns and validation from a Zod v4 schema.
Install
To install this Quasar App Extension, run:
quasar ext add @radon-be/zod-tableOr install the package directly via npm:
npm install @radon-be/quasar-app-extension-zod-tableRequirements
- Quasar Dialog Plugin: Required for delete row confirmation dialogs. Make sure the Dialog plugin is installed in your Quasar config (
quasar.config.ts).
Features
- Strongly typed
- Schema Driven: Columns are generated automatically from Zod object definition.
- Validation: Input cells validate against the Zod schema.
- Quasar Integration: Built on top of
q-tableandq-input. - Global
editableand per-columneditable-columnsprops to enable/disable editing show-columnsprop to explicitly show specific columns (preventing unwanted auto-inclusion of new schema properties)hide-columnsprop to hide specific columns from the datasetcolumn-labelsprop to re-label columns- Support for re-labeling columns
- Visual indicators (flat, bordered, ...) are passed-through
header-classandheader-styleprops to customize the headers- Optional
actionscolumn stringdata is edited in a regularq-inputnumberdata in a specialisedq-inputand cleaned up after editing (e.g. 00034 -> 34)booldata is represented as a checkboxenumdata as a dropdowndatetimesupport with date/time pickers forz.string().datetime()schemasdateandtimesupport with specialized date/time pickers- Read-only fields: Fields marked with
z.readonly()cannot be edited - Complex/nested properties: Support for nested object properties using dot notation (e.g.,
extra.requestedAt) - Foreign key support: Dropdown with complex objects using
colEditType: 'foreign-key'withoptionLabelandoptionValue - Clearable fields: Optional clear button for date, time, and foreign key fields
- Togglable Columns: Show/hide columns dynamically with the
togglable-columnsprop - Date/Time Picker: Quick access "Now" button to set current date/time, "Clear" button to remove value
- Delete Confirmation: Confirmation dialog before deleting rows (requires Quasar Dialog plugin)
- In Edit mode:
Enterkey = save ;Esckey = cancel stringprops can be edited with a dropdown box by adding anextraColumnOptionsoption
IntelliSense
For TypeScript prop IntelliSense in Vue templates, make sure the Vue language server (Volar) is enabled in your project.
You can use the component in two ways:
- As a global component via the Quasar App Extension boot registration.
- As a named import:
import { ZodTable } from '@radon-be/quasar-app-extension-zod-table'Global component IntelliSense (Quasar project)
When using the component as a global component via the app extension, add the following to your src/env.d.ts to enable prop IntelliSense in Vue templates:
import { type ZodTable as ZodTableComponent } from '@radon-be/quasar-app-extension-zod-table'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
ZodTable: typeof ZodTableComponent
}
}
export {}Note: Augment
@vue/runtime-core(notvue) and usetypeofon the imported value — this is required for Volar to resolve prop types in templates.
Usage
Use the ZodTable component in your Vue files.
<template>
<ZodTable
title="My Users"
row-key="id"
:row-model="userSchema"
:data="rows"
editable
:editable-columns="['*']"
:update-row="handleUpdate"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { z } from 'zod'
const userSchema = z.object({
id: z.number().readonly(), // Read-only fields cannot be edited
name: z.string().min(1, 'Name is required'),
email: z.string().email(),
role: z.enum(['admin', 'user']),
age: z.number().min(18).optional(),
})
type User = z.infer<typeof userSchema>
const rows = ref<User[]>([
{ id: 1, name: 'Alice', email: '[email protected]', role: 'admin', age: 30 },
{ id: 2, name: 'Bob', email: '[email protected]', role: 'user', age: 25 },
])
const handleUpdate = async (updatedRow: User) => {
const index = rows.value.findIndex((r) => r.id === updatedRow.id)
if (index !== -1) {
rows.value[index] = updatedRow
}
}
</script>Advanced Features
Pagination and Default Sort
You can control pagination and sorting via v-model:pagination:
<ZodTable :row-model="schema" :data="rows" v-model:pagination="pagination" />const pagination = ref({
page: 1,
rowsPerPage: 10,
sortBy: 'name',
descending: false,
})If you do not bind it, the component defaults to { page: 1, rowsPerPage: 5, sortBy: null, descending: false }.
Date and Time Fields
Use Zod's datetime schema to enable date/time pickers:
const schema = z.object({
createdAt: z.string().datetime().optional(),
scheduledTime: z.string().datetime().optional(),
})Configure with extraColumnOptions:
<ZodTable
:row-model="schema"
:data="rows"
:extraColumnOptions="{
createdAt: {
colEditType: 'date',
clearable: true,
},
scheduledTime: {
colEditType: 'time',
clearable: true,
},
}"
/>Nested/Complex Properties
Define nested schemas and access them with dot notation:
const extraSchema = z.object({
notes: z.string().optional(),
requestedAt: z.string().datetime().optional(),
})
const mainSchema = z.object({
id: z.number(),
name: z.string(),
extra: extraSchema.optional(),
})Use dot notation in column-labels and extraColumnOptions:
<ZodTable
:row-model="mainSchema"
:column-labels="{
'extra.requestedAt': 'Request Date',
'extra.notes': 'Additional Notes',
}"
:extraColumnOptions="{
'extra.requestedAt': {
colEditType: 'date',
clearable: true,
},
}"
/>Foreign Key Support
Create dropdowns with complex objects:
<ZodTable
:row-model="schema"
:extraColumnOptions="{
officeId: {
colEditType: 'foreign-key',
options: [
{ id: 'A', label: 'Main Office', address: 'Street 1' },
{ id: 'B', label: 'Branch Office', address: 'Street 2' },
],
optionLabel: 'label',
optionValue: 'id',
clearable: true,
},
}"
/>The table will display the label but store the id value.
Note: When editing is disabled, enum and foreign-key columns automatically render as plain text labels instead of select dropdowns to reduce visual clutter.
Header Templates
You can override a specific header cell using a dynamic slot named header-cell-[columnKey]. The slot receives col, pagination, and standard header props for full customization.
<ZodTable :row-model="schema" :data="rows">
<template #header-cell-name="{ col, pagination }">
<div class="row items-center q-gutter-xs">
<q-icon name="badge" size="16px" />
<span>{{ col.label }}</span>
<q-icon
v-if="pagination.sortBy === col.name"
:name="pagination.descending ? 'south' : 'north'"
size="12px"
/>
</div>
</template>
</ZodTable>This follows the same dynamic naming style as body-cell-[columnKey]. Access pagination.sortBy and pagination.descending to show sort state indicators.
Controlling Column Visibility
Use show-columns and hide-columns to manage which columns appear in the table. This is particularly useful to prevent unwanted new schema properties from automatically appearing when your Zod schema evolves.
Show specific columns only:
<ZodTable :row-model="schema" :data="rows" :show-columns="['id', 'name', 'email']" />Hide specific columns:
<ZodTable :row-model="schema" :data="rows" :hide-columns="['internalId', 'createdAt']" />Combine for precise control:
<ZodTable
:row-model="schema"
:data="rows"
:show-columns="['id', 'name', 'email', 'role', 'department']"
:hide-columns="['department']"
/>In the combined example, show-columns defines the base set (id, name, email, role, department), then hide-columns removes department, resulting in only id, name, email, and role being displayed.
Props
| Prop | Type | Description |
| ------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| row-model | ZodObject | The Zod schema defining the table structure. |
| data | Array | The data rows to display. |
| row-key | String | Property name to use as a unique key for rows (required). |
| update-row | Function | Object | Inline update action. Accepts either a simple handler (row) => void \| Promise<void> or an action object with optional label/icon/visible/disabled/color, and handler(event, row) => void \| Promise<void>. |
| add-row | Function | Callback for adding rows. |
| clone-row | Function | Object | Clone action. Accepts either a simple handler (row) => void \| Promise<void> or an action object with optional label/icon/visible/disabled/color, and handler(event, row) => void \| Promise<void>. |
| delete-row | Function | Object | Delete action. Accepts either a simple handler (row) => void \| Promise<void> or an action object with optional label/icon/visible/disabled/color, optional confirm (boolean or (row) => boolean), and handler(event, row) => void \| Promise<void>. |
| editable | Boolean | Enable editing mode. Can be used as v-model:editable for two-way binding. |
| editable-columns | Array | List of keys (or ['*']) that are editable. |
| show-columns | Array | List of column keys to show. If specified, only these columns are displayed. Use together with hide-columns to fine-tune visibility—show-columns defines the base set, then hide-columns removes from it. |
| hide-columns | Array | List of column keys to hide from display. Applied after show-columns filtering. |
| togglable-columns | Array | List of keys (or ['*']) that can be toggled on/off by the user. |
| column-order | Array | Optional ordered list of column keys. Listed visible columns are placed first in this order; all other visible columns are appended after. |
| default-toggled-columns | Array | Initial visible set for togglable columns. Accepts column keys or ['*']. Non-togglable columns remain visible. |
| column-labels | Object | Map of column keys to custom labels. Supports nested properties with dot notation (e.g., 'extra.field': 'Label'). |
| header-class | String | CSS class applied to table headers. |
| header-style | String | Inline styles applied to table headers. |
| show-editable-toggle | Boolean | Show/hide the editable toggle in the table footer (default: false). |
| extraColumnOptions | Object | Advanced column configuration. Set colEditType ('date', 'time', 'datetime', 'foreign-key'), options, optionLabel, optionValue, clearable, noVerticalPadding, and noHorizontalPadding for specific columns. |
| actions | Array | Enable action buttons: ['add', 'clone', 'delete', 'goto']. |
| goto-row | Array \ | Function | Array of goto actions with key, optional icon/label/href/target/rel/visible/disabled/color, or a single handler function. Handler signature is (event, row) => void \| Promise<void>. |
| pagination | Object | Pagination model (page, rowsPerPage, sortBy, descending). Use with v-model:pagination to control the table state from the parent. |
| i18n | Object | Optional object with partial i18n label overrides. Merged with default English labels. Keys: noData, noResults, addButton, columnsLabel, rowsPerPageLabel, paginationSeparator, editableToggle, cloneButtonTitle, deleteButtonTitle, datePickerNow, datePickerClear, datePickerClose, deleteConfirmTitle, deleteConfirmMessage. |
Goto Actions (Event-First)
Goto handlers are event-first so you can inspect click modifiers like Ctrl/Cmd/Shift or middle-click:
type GotoAction<Row> = {
key: string
label?: string | ((row: Row) => string)
icon?: string | ((row: Row) => string)
href?: string | ((row: Row) => string)
target?: string | ((row: Row) => string)
rel?: string | ((row: Row) => string)
visible?: boolean | ((row: Row) => boolean)
disabled?: boolean | ((row: Row) => boolean)
color?: string | ((row: Row) => string)
handler: (event: Event, row: Row) => void | Promise<void>
}When href is provided, the goto action button is rendered as a link-capable Quasar button while still executing handler on click.
Row Actions
Clone, update, and delete actions can be passed either as a simple synchronous/asynchronous function or as a row-aware action object. delete-row also accepts confirm on the object form.
type RowAction<Row> = {
label?: string | ((row: Row) => string)
icon?: string | ((row: Row) => string)
visible?: boolean | ((row: Row) => boolean)
disabled?: boolean | ((row: Row) => boolean)
color?: string | ((row: Row) => string)
handler: (event: Event, row: Row) => void | Promise<void>
}
type DeleteRowActionConfig<Row> = RowAction<Row> & {
confirm?: boolean | ((row: Row) => boolean)
}
type RowActionHandler<Row> = (row: Row) => void | Promise<void>Column Cell Padding
For custom column templates, you can remove top/bottom table-cell padding per column via extraColumnOptions:
<ZodTable
:row-model="schema"
:extraColumnOptions="{
'extra.description': {
noVerticalPadding: true,
noHorizontalPadding: true,
},
}"
/>Example:
const gotoDetails = (event: Event, row: User) => {
const url = `/users/${row.id}`
// Open in a new tab on Ctrl/Cmd click or middle click
if ((event as MouseEvent).ctrlKey || (event as MouseEvent).metaKey || (event as MouseEvent).button === 1) {
window.open(url, '_blank', 'noopener,noreferrer')
return
}
// Open in a new window on Shift click
if ((event as MouseEvent).shiftKey) {
window.open(url, '_blank', 'popup')
return
}
// Default navigation
// router.push(url)
}V-Model
v-model:editable- Two-way binding for the editable state. When the user toggles the editable mode in the table footer (ifshow-editable-toggleis enabled), this binding will update.
Events
| Event | Payload | Description |
| -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| row-click | (event: MouseEvent, row: Row) | Emitted when a row is clicked. Useful for custom navigation or row selection logic. |
| update-togglable-columns | ColumnKey[] | Emitted when the user toggles column visibility. Payload is an array of currently visible column keys. Use this to store column preferences in localStorage or other persistent storage. |
Persisting Column Visibility
You can persist column visibility preferences to localStorage:
<template>
<ZodTable
:row-model="schema"
:data="rows"
:togglable-columns="['*']"
:default-toggled-columns="defaultColumns"
v-model:editable="isEditing"
:show-editable-toggle="true"
@update-togglable-columns="handleColumnToggle"
/>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
const isEditing = ref(false)
const defaultColumns = ref<string[]>(['name', 'email'])
onMounted(() => {
// Load saved column preferences from localStorage
const saved = localStorage.getItem('tableColumns')
if (saved) {
defaultColumns.value = JSON.parse(saved)
}
// Load saved editable state
const wasEditing = localStorage.getItem('tableEditable')
if (wasEditing === 'true') {
isEditing.value = true
}
})
function handleColumnToggle(columns: string[]) {
// Save column visibility to localStorage
localStorage.setItem('tableColumns', JSON.stringify(columns))
}
// Watch editable state and save it
watch(() => isEditing.value, (val) => {
localStorage.setItem('tableEditable', String(val))
})
</script>
## Internationalization (i18n)
The ZodTable component includes default labels and messages in English. You can customize labels globally for your entire app or per-component using the `i18n` prop.
### Global i18n Setup (Recommended)
For the best experience, set up i18n globally so all `ZodTable` instances use the same language without passing props:
**Create a boot file** `src/boot/zodTable-i18n.ts`:
```typescript
import { boot } from 'quasar/wrappers'
import { ref } from 'vue'
import { provideZodTableI18n, type I18nLabels } from '@radon-be/quasar-app-extension-zod-table'
export const i18nLabels = ref<Partial<I18nLabels>>({
addButton: 'Add',
deleteButtonTitle: 'Delete',
// ... other overrides
})
export default boot(({ app }) => {
provideZodTableI18n(i18nLabels, app)
})Register the boot file in quasar.config.ts:
boot: ['zodTable-i18n']Now all ZodTable instances in your app will use these labels without needing to pass the i18n prop.
Per-Component i18n Override
You can still override labels on individual components:
<ZodTable :row-model="schema" :data="rows" :i18n="{ addButton: 'Create' }" />Prop overrides take precedence over global settings.
Dynamic Language Switching
Change language at runtime by updating the exported ref:
// In a language switcher component
import { i18nLabels } from 'boot/zodTable-i18n'
function switchToDutch() {
i18nLabels.value = { addButton: 'Toevoegen', columnsLabel: 'Kolommen' /* ... */ }
}Plain Vite/Vue Apps
For apps without a Quasar CLI boot file, use createZodTableI18nPlugin() in main.ts:
import { ref } from 'vue'
import { createZodTableI18nPlugin } from '@radon-be/quasar-app-extension-zod-table'
app.use(
createZodTableI18nPlugin(
ref({
addButton: 'Add',
deleteButtonTitle: 'Delete',
// ... other overrides
}),
),
)Per-Component i18n
Alternatively, if you don't use global setup, pass i18n to individual components:
<template>
<ZodTable
:row-model="schema"
:data="rows"
:i18n="{
addButton: 'Add',
columnsLabel: 'Columns',
rowsPerPageLabel: 'Rows per page',
}"
/>
</template>Available i18n Labels
The following labels can be customized:
| Key | Default Value | Description |
| ---------------------- | ------------------------------------------- | -------------------------------------------------- |
| noData | 'No data available' | Message shown when table has no data |
| noResults | 'No results found' | Message shown when no results match filter |
| addButton | 'Add' | Add row button label |
| columnsLabel | 'Columns' | Column selector label |
| rowsPerPageLabel | 'Rows per page' | Rows per page selector label |
| paginationSeparator | 'of' | Separator in pagination text (e.g., "1-10 of 100") |
| editableToggle | 'Editable' | Editable mode toggle title |
| cloneButtonTitle | 'Clone' | Clone row button title |
| deleteButtonTitle | 'Delete' | Delete row button title |
| datePickerNow | 'Now' | Date picker "set to now" button |
| datePickerClear | 'Clear' | Date picker clear button |
| datePickerClose | 'Close' | Date picker close button |
| deleteConfirmTitle | 'Confirm Delete' | Delete confirmation dialog title |
| deleteConfirmMessage | 'Are you sure you want to delete this row?' | Delete confirmation message |
Dutch Example
// src/boot/zodTable-i18n.ts
import { ref } from 'vue'
import { provideZodTableI18n } from '@radon-be/quasar-app-extension-zod-table'
const i18nLabels = ref({
noData: 'Geen data beschikbaar',
noResults: 'Geen resultaten gevonden',
addButton: 'Toevoegen',
columnsLabel: 'Kolommen',
rowsPerPageLabel: 'Rijen per pagina',
paginationSeparator: 'van',
editableToggle: 'Editable',
cloneButtonTitle: 'Klonen',
deleteButtonTitle: 'Verwijderen',
datePickerNow: 'Nu',
datePickerClear: 'Wissen',
datePickerClose: 'Sluiten',
deleteConfirmTitle: 'Verwijdering bevestigen',
deleteConfirmMessage: 'Weet je zeker dat je deze rij wilt verwijderen?',
})
export default boot(({ app }) => {
provideZodTableI18n(i18nLabels, app)
})Spanish Example
const i18nLabels = ref({
noData: 'Sin datos disponibles',
noResults: 'No hay resultados',
addButton: 'Añadir',
columnsLabel: 'Columnas',
rowsPerPageLabel: 'Filas por página',
paginationSeparator: 'de',
editableToggle: 'Editable',
cloneButtonTitle: 'Clonar',
deleteButtonTitle: 'Eliminar',
datePickerNow: 'Ahora',
datePickerClear: 'Limpiar',
datePickerClose: 'Cerrar',
deleteConfirmTitle: 'Confirmar eliminación',
deleteConfirmMessage: '¿Estás seguro de que deseas eliminar esta fila?',
})License
MIT
