SvGrid Studio · computed & hooks

Business logic on the schema. `total` is a COMPUTED field (qty * price) - read-only in grid + form, recomputed live as you type, never stored or submitted. withEntityRules materializes it onto every row and runs the schema hooks: beforeCreate stamps createdAt, and a cross-field validate rejects a non-positive quantity. (requires @svgrid/enterprise)

A live, editable Svelte 5 data grid example from the SvGrid gallery (Studio). See the SvGrid documentation for the full API.

About this example

Business logic on the entity schema in the SvGrid Studio stack. total is a computed field defined as qty times price: read-only in the grid and the form, recomputed live as you type and never stored or submitted. withEntityRules materializes it onto every row and runs the schema's hooks, beforeCreate stamps createdAt and a cross-field validate rejects a non-positive quantity before the save reaches the data source.

Data-app Studio · computed fields + hooks - business logic on the schema.

total is a COMPUTED field (qty * price): read-only in the grid + form, recomputed live as you type, never stored or submitted. withEntityRules materializes it onto every row and runs the schema's hooks: beforeCreate stamps createdAt, and validate (cross-field) rejects a non-positive qty.

Imports, features and API used

Imports: @svgrid/grid, @svgrid/enterprise, ../shared/SvGridStudio.svelte

Frequently asked questions

How is a computed field declared?

As a schema field with a compute function of the row. The form shows it read-only and re-runs the function on every keystroke; withEntityRules adds the value to rows read from the source so the grid column has it too.

Where do hooks run?

In withEntityRules, which wraps the data source. beforeCreate mutates the row before insert, validate runs before create and update and returns messages that the form displays.

Is the computed value sent to the server?

No. It is stripped from the payload on save, so the database never stores a value that could drift from its inputs.

Related documentation

Related articles

  • SvGrid Studio Tips and Tricks: Build Svelte Data Apps, Fast - Practical SvGrid Studio tips - AI app generation, a designer that emits real SvelteKit, schema-driven edit forms, code-behind with ctx.grid, production auth/data/deploy toggles, and real data-source binding - each with a snippet.
  • SvGrid vs TanStack Table - A Deep Dive - A concrete architectural comparison of SvGrid and TanStack Table's Svelte adapter - how each handles reactivity, rendering, and feature composition, with code that shows exactly where they diverge.
  • SvGrid vs svelte-headless-table - A practical comparison of SvGrid and svelte-headless-table covering reactivity model, rendering approach, feature scope, and when each is the right choice for a Svelte 5 project.

Source code (199-studio-computed-hooks.svelte)

<script lang="ts">
  /**
   * Data-app Studio · computed fields + hooks - business logic on the schema.
   *
   * `total` is a COMPUTED field (`qty * price`): read-only in the grid + form,
   * recomputed live as you type, never stored or submitted. `withEntityRules`
   * materializes it onto every row and runs the schema's `hooks`: `beforeCreate`
   * stamps `createdAt`, and `validate` (cross-field) rejects a non-positive qty.
   */
  import { SvGrid, createServerDataSource, type ServerState } from '@svgrid/grid'
  import {
    SvGridEditPanel,
    createInMemoryDataSource,
    withEntityRules,
    schemaToColumns,
    type EntitySchema,
  } from '@svgrid/enterprise'
  import SvGridStudio from '../shared/SvGridStudio.svelte'

  type Order = {
    id: string
    product: string
    qty: number
    price: number
    total: number
    createdAt: string
  }

  const schema: EntitySchema<Order> = {
    name: 'orders',
    label: 'Order',
    idField: 'id',
    fields: [
      { field: 'id', type: 'text', primaryKey: true, readonly: true, hidden: { form: true } },
      { field: 'product', type: 'text', required: true, minLength: 2 },
      { field: 'qty', type: 'number', label: 'Qty', required: true, min: 1 },
      { field: 'price', type: 'number', label: 'Unit price ($)', required: true, min: 0 },
      // Computed: derived from qty * price. Read-only, live, never stored.
      { field: 'total', type: 'number', label: 'Total ($)', computed: (r) => Number(r.qty) * Number(r.price) },
      { field: 'createdAt', type: 'text', label: 'Created', readonly: true },
    ],
    hooks: {
      // Cross-field validation (also enforced live in the form).
      validate: (v) => (Number(v.qty) <= 0 ? { qty: 'Quantity must be at least 1' } : null),
      // Stamp a created date on new rows.
      beforeCreate: (v) => ({ ...v, createdAt: new Date().toISOString().slice(0, 10) }),
    },
  }

  const seed: Order[] = [
    { id: 'o1', product: 'Widget', qty: 3, price: 20, total: 0, createdAt: '2026-06-01' },
    { id: 'o2', product: 'Gadget', qty: 1, price: 150, total: 0, createdAt: '2026-06-03' },
    { id: 'o3', product: 'Sprocket', qty: 8, price: 12.5, total: 0, createdAt: '2026-06-05' },
    { id: 'o4', product: 'Cog', qty: 25, price: 3, total: 0, createdAt: '2026-06-09' },
    { id: 'o5', product: 'Flange', qty: 4, price: 45, total: 0, createdAt: '2026-06-11' },
  ]

  const columns = schemaToColumns(schema)
  // withEntityRules materializes `total` on read + runs hooks on writes.
  const source = withEntityRules(createInMemoryDataSource(seed, schema), schema)

  let view = $state<ServerState<Order>>({
    rows: [], total: 0, loading: false, saving: false, error: null,
    pageIndex: 0, pageSize: 8, pageCount: 1, sortModel: [], filterModel: {},
  })
  const controller = createServerDataSource(source, {
    pageSize: 8, optimistic: true, getRowId: (r) => r.id, onChange: (s) => (view = s),
  })
  controller.refresh()

  let editing = $state<Order | null | undefined>(undefined)
  let genId = 6
  async function save({ mode, id, values }: { mode: 'create' | 'edit'; id: string | null; values: Partial<Order> }) {
    if (mode === 'create') {
      await controller.createRow({ id: `o${genId++}`, ...values })
      controller.setPage(view.pageCount - 1)
    } else if (id) {
      await controller.updateRow(id, values)
    }
    editing = undefined
  }
</script>

<SvGridStudio
  title="Computed fields &amp; hooks"
  subtitle="<code>total</code> is a computed field (<code>qty * price</code>) - read-only, live, never stored. <code>withEntityRules</code> materializes it and runs the schema's hooks: <code>beforeCreate</code> stamps the date, <code>validate</code> rejects qty &lt; 1."
>
  {#snippet toolbar()}
    <button class="st-btn st-btn--primary" onclick={() => (editing = null)}>+ New order</button>
    <span class="st-hint" style="margin-left:auto">Double-click a row to edit - watch Total update as you type</span>
  {/snippet}

  <SvGrid responsive={true}
      columnResize
    data={view.rows}
    {columns}
    loading={view.loading}
    fitColumns
    sortable
    externalSort
    onSortingChange={(s) => controller.setSort(s)}
    onRowDoubleClick={(e) => (editing = e.row)}
    showPagination
    externalPagination
    rowCount={view.total}
    pageIndex={view.pageIndex}
    pageSize={view.pageSize}
    onPaginationChange={({ pageIndex, pageSize }) => (pageSize !== view.pageSize ? controller.setPageSize(pageSize) : controller.setPage(pageIndex))}
    containerHeight={360}
  />

  {#if editing !== undefined}
    <SvGridEditPanel {schema} row={editing} presentation="modal" persistKey="studio" onSubmit={save} onCancel={() => (editing = undefined)} />
  {/if}
</SvGridStudio>

View this example on GitHub

More Studio examples

  • SvGrid Studio · live SQL - The Studio stack backed by a REAL Postgres running in the browser via PGlite (WASM), no server. createSqlDataSource turns the grid's sort / filter / page requests into parameterized SQL run through PGlite; the executed query is shown live under the toolbar. Full CRUD with optimistic updates against actual Postgres.
  • Northwind on PGlite - The classic Northwind sample DB (categories, customers, products, orders, order_details) seeded into a real in-browser Postgres (PGlite, WASM). Switch between a five-table JOIN exposed as a read-only VIEW and the editable base tables - each via createSqlDataSource. Sort / filter / page / edit all run as parameterized SQL; new rows get an auto id from a Postgres IDENTITY sequence.
  • SvGrid Studio · Supabase - The Studio stack over hosted Postgres on Supabase, straight from the browser via supabase-js (PostgREST) and your project's public anon key. createSupabaseDataSource maps the grid's sort / filter / page / CRUD onto the query builder, introspectSupabaseTable adapts to any table AND detects foreign keys (a FK column auto-becomes a searchable lookup in the form), and createSupabaseRealtime makes it LIVE - change a row in the Supabase dashboard and it flashes in the grid (toggle the Live pill). Paste your URL + anon key, run the one-time setup SQL, done. RLS keeps the anon key safe.
  • SvGrid Studio · relations - Foreign-key lookup fields end to end. Contacts belong to a Company: the contact form renders a searchable Company picker (SvLookupInput + createRelationLookup) that queries the Companies source and stores the id, while the grid shows the resolved company NAME. The lookup runs over the same ServerDataSource contract, so related options can come from Supabase / REST / SQL / in-memory (here both entities are in-memory).
  • SvGrid Studio · secured - A secured screen: SvAuthGate requires a signed-in user (Supabase Auth via createSupabaseAuth) before showing the Studio grid, with a login / sign-up form and a signed-in bar. Uses a mock auth client so any email + password works here; swap for your supabase-js client and Row-Level Security scopes each user to their own rows (auth = who, RLS = what).