Data & Editing

Validation

Validate cell values in NuGrid using schemas.

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).

ID
Name
Email
Age
1
Alice
alice@example.com
28
2
Bob
bob@example.com
35
3
Carol
carol@example.com
42
<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>

Basic Validation

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>

Validation Options

interface NuGridValidationOptions<T> {
  schema: StandardSchema<T>
  rowRules?: RowValidationRule<T>[]
  validateOn?: 'always' | 'reward'
  showErrors?: 'always' | 'touched' | 'never'
  icon?: string
  onInvalid?: 'block' | 'allow'
}

Options Reference

OptionTypeDefaultDescription
schemaStandardSchemarequiredZod, Valibot, or Yup schema
rowRulesarray[]Cross-field validation rules
validateOn'always' | 'reward''always'When to validate
showErrors'always' | 'touched' | 'never''always'When to show errors
iconstring'i-lucide-alert-circle'Error icon
onInvalid'block' | 'allow''block'Behavior on invalid

Validation Timing

Always Validate

Validate on every change:

const validationOptions = {
  schema: productSchema,
  validateOn: 'always',
}

Reward Pattern

Only show errors after user attempts to fix them (better UX):

const validationOptions = {
  schema: productSchema,
  validateOn: 'reward',  // Hide errors until user tries to fix
}

Error Display

Always Show

Show validation errors immediately:

const validationOptions = {
  schema: productSchema,
  showErrors: 'always',
}

Show on Touch

Only show errors after field has been edited:

const validationOptions = {
  schema: productSchema,
  showErrors: 'touched',
}

Never Show

Validate but don't show visual errors (for custom handling):

const validationOptions = {
  schema: productSchema,
  showErrors: 'never',
}

Block vs Allow Invalid

Block Invalid Saves

Prevent saving invalid values (default):

const validationOptions = {
  schema: productSchema,
  onInvalid: 'block',  // Won't save invalid values
}

Allow Invalid Saves

Allow saving but show warnings:

const validationOptions = {
  schema: productSchema,
  onInvalid: 'allow',  // Save anyway, just show warning
}

Row-Level Validation

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,
}

Schema Examples

With Zod

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' }),
  }),
})

With Valibot

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')
  ),
})

Column-Level Validation

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 }
    },
  },
]

Custom Error Display

Customize the error icon:

const validationOptions = {
  schema: productSchema,
  icon: 'i-lucide-x-circle',
}

Handling Validation Errors

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>

Example: Complete Validation Setup

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

Next Steps

Cell Data Types

Use built-in cell types.

Add New Rows

Enable adding new rows.