valueParser - transform on commit
Per-column `valueParser` refines the committed value after built-in coercion: uppercase a SKU, parse "$1,299.90" into a number, clamp a discount 0–100, round a weight. Log shows raw input → stored.
A live, editable Svelte 5 data grid example. Open the interactive demo or read the documentation.
What this example shows
Each column's `valueParser({ newValue, oldValue, rawInput, data, columnId })` refines the committed value AFTER the built-in per-`editorType` coercion, before it is written to the row. Uppercase a SKU, parse a currency string into a number, clamp a discount, round a weight - the grid stores the normalized value, not the raw keystrokes. The log shows raw input -> stored.
Source code (175-value-parser.svelte)
<!-- Documented in: docs/help/editing/parsing-values.md -->
<script lang="ts">
/**
* 175. valueParser - transform edits on commit
* ---------------------------------------------
* Each column's `valueParser({ newValue, oldValue, rawInput, data, columnId })`
* refines the committed value AFTER the built-in per-`editorType` coercion,
* before it is written to the row. Uppercase a SKU, parse a currency string
* into a number, clamp a discount, round a weight - the grid stores the
* normalized value, not the raw keystrokes. The log shows raw input -> stored.
*/
import {
SvGrid,
tableFeatures,
rowSortingFeature,
type ColumnDef,
type ValueParserParams,
} from '@svgrid/grid'
type Product = {
sku: string
name: string
price: number
discount: number
weight: number
}
let rows = $state<Product[]>([
{ sku: 'KB-101', name: 'Mechanical Keyboard', price: 129.99, discount: 15, weight: 1.23 },
{ sku: 'MS-202', name: 'Wireless Mouse', price: 49.5, discount: 0, weight: 0.21 },
{ sku: 'HD-303', name: 'Noise-Cancelling Headphones', price: 299, discount: 20, weight: 0.33 },
{ sku: 'WC-404', name: '4K Webcam', price: 89.9, discount: 10, weight: 0.15 },
{ sku: 'DK-505', name: 'Standing Desk', price: 549, discount: 5, weight: 24.5 },
])
const features = tableFeatures({ rowSortingFeature })
type LogEntry = { seq: number; col: string; raw: string; stored: string }
let log = $state<LogEntry[]>([])
let seq = 0
// Wrap a parser so the demo can show "what you typed -> what got stored".
function logged(col: string, fn: (p: ValueParserParams<Product>) => unknown) {
return (p: ValueParserParams<Product>) => {
const out = fn(p)
seq += 1
log = [{ seq, col, raw: p.rawInput, stored: String(out) }, ...log].slice(0, 9)
return out
}
}
const columns: ColumnDef<typeof features, Product>[] = [
{
field: 'sku',
header: 'SKU → UPPER-CASE',
editorType: 'text',
width: 170,
valueParser: logged('sku', ({ newValue }) =>
String(newValue).trim().toUpperCase().replace(/\s+/g, '-'),
),
},
{
field: 'name',
header: 'Name → Title Case',
editorType: 'text',
width: 240,
valueParser: logged('name', ({ newValue }) =>
String(newValue)
.trim()
.replace(/\s+/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase()),
),
},
{
field: 'price',
header: 'Price → parse "$1,299.9"',
editorType: 'text',
width: 200,
align: 'right',
format: { type: 'currency', currency: 'USD' },
// Accepts "$1,299.90", "1299.9", "1,299" -> a number rounded to 2dp.
valueParser: logged('price', ({ newValue, oldValue }) => {
const n = Number(String(newValue).replace(/[^0-9.\-]/g, ''))
return Number.isFinite(n) ? Math.round(n * 100) / 100 : oldValue
}),
},
{
field: 'discount',
header: 'Discount % → clamp 0–100',
editorType: 'number',
width: 190,
align: 'right',
valueParser: logged('discount', ({ newValue }) =>
Math.max(0, Math.min(100, Math.round(Number(newValue) || 0))),
),
},
{
field: 'weight',
header: 'Weight kg → round 2dp',
editorType: 'number',
width: 170,
align: 'right',
valueParser: logged('weight', ({ newValue }) =>
Math.round((Number(newValue) || 0) * 100) / 100,
),
},
]
</script>
<section class="flex flex-col flex-1 min-h-0 gap-3">
<p class="text-sm shrink-0" style="color: var(--sg-fg);">
Double-click a cell (or press <kbd>F2</kbd>) and type freely - a lowercase SKU,
<code>$1,299.90</code> in Price, <code>250</code> in Discount. On commit each
column's <code>valueParser</code> normalizes the value before it is stored.
</p>
<div class="shrink-0 rounded-lg border" style="border-color: var(--sg-border); background: var(--sg-header-bg);">
<div class="px-3 py-2 border-b text-xs font-semibold uppercase tracking-wider"
style="border-color: var(--sg-border); color: var(--sg-muted);">
valueParser log <span style="text-transform: none; font-weight: 400;">raw input → stored value</span>
</div>
<div class="max-h-28 overflow-y-auto px-3 py-2">
{#if log.length === 0}
<p class="text-xs italic" style="color: var(--sg-muted);">Nothing edited yet.</p>
{:else}
<ul class="space-y-1">
{#each log as e (e.seq)}
<li class="text-xs flex flex-wrap gap-x-2" style="color: var(--sg-fg);">
<code style="color: var(--site-accent-2, #22d3ee);">{e.col}</code>
<code style="color: var(--sg-fg);">"{e.raw}"</code>
<span style="color: var(--sg-muted);">→</span>
<code style="color: #34d399; font-weight: 600;">{e.stored}</code>
</li>
{/each}
</ul>
{/if}
</div>
</div>
<div class="flex-1 min-h-0">
<SvGrid responsive={true}
data={rows}
columns={columns}
features={features}
showRowNumbers={true}
enableInlineEditing={true}
enableCellSelection={true}
rowHeight={38}
containerHeight="100%"
fitColumns={true}
/>
</div>
</section>Related documentation
More Editing examples
- Inline editing - Typed editors (text/number/checkbox/date) with dirty tracking + save.
- Editor types + custom slot - Built-in select / rich-select / textarea editors plus a custom `cellEditor` snippet (a range slider) for cases the built-ins do not cover.
- List + chips editors - Two built-in editors with single & multi-select: dropdown (list) and removable tokens (chips), with options or free-form.
- Async editor options - editorOptions can return a Promise - for the whole column, or per row for a cascade. The dropdown shows Loading… while the request is in flight, results are cached (per column, or per column+row) so reopening never refetches, and api.refreshEditorOptions() invalidates when the server list changes.
- Custom cell editors - Three hand-rolled editors: native colour picker bound to a tag swatch, 5-star rating, emoji feedback mood. All write back through api.setCellValue.