npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-table

Or install the package directly via npm:

npm install @radon-be/quasar-app-extension-zod-table

Requirements

  • 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-table and q-input.
  • Global editable and per-column editable-columns props to enable/disable editing
  • show-columns prop to explicitly show specific columns (preventing unwanted auto-inclusion of new schema properties)
  • hide-columns prop to hide specific columns from the dataset
  • column-labels prop to re-label columns
  • Support for re-labeling columns
  • Visual indicators (flat, bordered, ...) are passed-through
  • header-class and header-style props to customize the headers
  • Optional actions column
  • string data is edited in a regular q-input
  • number data in a specialised q-input and cleaned up after editing (e.g. 00034 -> 34)
  • bool data is represented as a checkbox
  • enum data as a dropdown
  • datetime support with date/time pickers for z.string().datetime() schemas
  • date and time support 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' with optionLabel and optionValue
  • Clearable fields: Optional clear button for date, time, and foreign key fields
  • Togglable Columns: Show/hide columns dynamically with the togglable-columns prop
  • 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: Enter key = save ; Esc key = cancel
  • string props can be edited with a dropdown box by adding an extraColumnOptions option

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 (not vue) and use typeof on 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 (if show-editable-toggle is 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