NuGrid supports schema-based validation using Standard Schema v1 compatible libraries like Zod, Valibot, or Yup.
Try editing cells with invalid values (e.g., single character name, invalid email, age under 18).
<script setup lang="ts">
import type { NuGridColumn } from '#nu-grid/types'
import { z } from 'zod'
interface User {
id: number
name: string
email: string
age: number
}
const data = ref<User[]>([
{ id: 1, name: 'Alice', email: 'alice@example.com', age: 28 },
{ id: 2, name: 'Bob', email: 'bob@example.com', age: 35 },
{ id: 3, name: 'Carol', email: 'carol@example.com', age: 42 },
])
const userSchema = z.object({
id: z.number(),
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Please enter a valid email'),
age: z.number().min(18, 'Must be 18 or older').max(120, 'Age must be under 120'),
})
const columns: NuGridColumn<User>[] = [
{ accessorKey: 'id', header: 'ID', size: 60, enableEditing: false },
{ accessorKey: 'name', header: 'Name', size: 150 },
{ accessorKey: 'email', header: 'Email', size: 200 },
{ accessorKey: 'age', header: 'Age', size: 80 },
]
</script>
<template>
<div class="w-full">
<p class="mb-3 text-sm text-muted">
Try editing cells with invalid values (e.g., single character name, invalid email, age under
18).
</p>
<NuGrid
:data="data"
:columns="columns"
:editing="{ enabled: true, startClicks: 'double' }"
:validation="{
schema: userSchema,
validateOn: 'submit',
showErrors: 'always',
onInvalid: 'block',
}"
:ui="{
base: 'w-full border-separate border-spacing-0',
thead: '[&>tr]:bg-elevated/50',
th: 'py-2 border-y border-default first:border-l last:border-r first:rounded-l-lg last:rounded-r-lg',
td: 'border-b border-default',
}"
/>
</div>
</template>
Enable validation with the validation prop:
<script setup lang="ts">
import { z } from 'zod'
const productSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
price: z.coerce.number().min(0.01, 'Price must be greater than 0'),
stock: z.coerce.number().int().min(0, 'Stock must be non-negative'),
})
const validationOptions = {
schema: productSchema,
}
</script>
<template>
<NuGrid
:data="data"
:columns="columns"
:editing="{ enabled: true }"
:validation="validationOptions"
/>
</template>
interface NuGridValidationOptions<T> {
schema: StandardSchema<T>
rowRules?: RowValidationRule<T>[]
validateOn?: 'always' | 'reward'
showErrors?: 'always' | 'touched' | 'never'
icon?: string
onInvalid?: 'block' | 'allow'
}
| Option | Type | Default | Description |
|---|---|---|---|
schema | StandardSchema | required | Zod, Valibot, or Yup schema |
rowRules | array | [] | Cross-field validation rules |
validateOn | 'always' | 'reward' | 'always' | When to validate |
showErrors | 'always' | 'touched' | 'never' | 'always' | When to show errors |
icon | string | 'i-lucide-alert-circle' | Error icon |
onInvalid | 'block' | 'allow' | 'block' | Behavior on invalid |
Validate on every change:
const validationOptions = {
schema: productSchema,
validateOn: 'always',
}
Only show errors after user attempts to fix them (better UX):
const validationOptions = {
schema: productSchema,
validateOn: 'reward', // Hide errors until user tries to fix
}
Show validation errors immediately:
const validationOptions = {
schema: productSchema,
showErrors: 'always',
}
Only show errors after field has been edited:
const validationOptions = {
schema: productSchema,
showErrors: 'touched',
}
Validate but don't show visual errors (for custom handling):
const validationOptions = {
schema: productSchema,
showErrors: 'never',
}
Prevent saving invalid values (default):
const validationOptions = {
schema: productSchema,
onInvalid: 'block', // Won't save invalid values
}
Allow saving but show warnings:
const validationOptions = {
schema: productSchema,
onInvalid: 'allow', // Save anyway, just show warning
}
Validate across multiple fields with row rules:
interface Product {
price: number
cost: number
stock: number
reserved: number
}
const rowRules = [
// Profit margin check
(row: Product) => {
if (row.price < row.cost) {
return {
valid: false,
message: 'Price must be greater than cost',
failedFields: ['price', 'cost'],
}
}
return { valid: true }
},
// Stock availability check
(row: Product) => {
if (row.reserved > row.stock) {
return {
valid: false,
message: 'Reserved cannot exceed stock',
failedFields: ['stock', 'reserved'],
}
}
return { valid: true }
},
]
const validationOptions = {
schema: productSchema,
rowRules,
}
import { z } from 'zod'
const userSchema = z.object({
name: z.string()
.min(2, 'Name must be at least 2 characters')
.max(100, 'Name must be at most 100 characters'),
email: z.string()
.email('Invalid email address'),
age: z.coerce.number()
.int('Age must be a whole number')
.min(0, 'Age must be positive')
.max(150, 'Age must be realistic'),
role: z.enum(['admin', 'user', 'guest'], {
errorMap: () => ({ message: 'Invalid role' }),
}),
})
import * as v from 'valibot'
const userSchema = v.object({
name: v.pipe(
v.string(),
v.minLength(2, 'Name must be at least 2 characters'),
v.maxLength(100, 'Name must be at most 100 characters')
),
email: v.pipe(
v.string(),
v.email('Invalid email address')
),
age: v.pipe(
v.number(),
v.integer('Age must be a whole number'),
v.minValue(0, 'Age must be positive')
),
})
Add validation for specific columns in add-row:
const columns: NuGridColumn<Product>[] = [
{
accessorKey: 'price',
header: 'Price',
validateNew: (value) => {
if (value === undefined || value === null || value === '') {
return { valid: true } // Allow empty
}
const num = Number(value)
if (!Number.isFinite(num) || num <= 0) {
return { valid: false, message: 'Price must be a positive number' }
}
return { valid: true }
},
},
]
Customize the error icon:
const validationOptions = {
schema: productSchema,
icon: 'i-lucide-x-circle',
}
Access validation state programmatically:
<script setup lang="ts">
const gridRef = useTemplateRef('grid')
function checkValidity() {
const isValid = gridRef.value?.isValid()
const errors = gridRef.value?.getValidationErrors()
if (!isValid) {
console.log('Validation errors:', errors)
}
}
</script>
<script setup lang="ts">
import { z } from 'zod'
interface Product {
id: number
name: string
category: string
price: number
cost: number
stock: number
}
// Schema validation
const productSchema = z.object({
id: z.coerce.number().optional(),
name: z.string()
.min(2, 'Name must be 2-100 characters')
.max(100, 'Name must be 2-100 characters')
.optional(),
category: z.string().min(1, 'Category is required').optional(),
price: z.coerce.number().min(0.01, 'Price must be > 0').optional(),
cost: z.coerce.number().min(0, 'Cost must be >= 0').optional(),
stock: z.coerce.number().int().min(0, 'Stock must be >= 0').optional(),
})
// Row-level validation
const rowRules = [
(row: Product) => {
if (row.price < row.cost) {
return {
valid: false,
message: 'Price must exceed cost',
failedFields: ['price', 'cost'],
}
}
return { valid: true }
},
]
// Validation options
const validationOptions = computed(() => ({
schema: productSchema,
rowRules,
validateOn: 'reward',
showErrors: 'always',
icon: 'i-lucide-alert-circle',
onInvalid: 'block',
}))
const data = ref<Product[]>([
{ id: 1, name: 'Laptop', category: 'Electronics', price: 999, cost: 700, stock: 10 },
{ id: 2, name: 'Mouse', category: 'Electronics', price: 29, cost: 15, stock: 50 },
])
const columns: NuGridColumn<Product>[] = [
{ accessorKey: 'id', header: 'ID', enableEditing: false },
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'category', header: 'Category' },
{ accessorKey: 'price', header: 'Price' },
{ accessorKey: 'cost', header: 'Cost' },
{ accessorKey: 'stock', header: 'Stock' },
]
</script>
<template>
<NuGrid
:data="data"
:columns="columns"
:editing="{ enabled: true, startClicks: 'double' }"
:validation="validationOptions"
/>
</template>