SvGrid Studio · rich fields

Rich edit-form fields: an image UPLOAD field (SvFileInput - preview + file picker, stores a data URL with no backend, or a URL via an uploads handler) shown as an avatar in the grid, and a CASCADING dropdown (City computed from the chosen Country via dependentOptions, cleared when the country changes). Both are schema-driven on SvGridEditPanel. (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

Rich edit-form fields in the SvGrid Studio stack, both schema-driven on SvGridEditPanel. An upload field renders SvFileInput with a preview and file picker and stores an inline data URL with no backend, or pushes to storage when an uploads handler is given, and the grid shows the result as an avatar. A dependent dropdown computes its City options from the chosen Country through dependentOptions and clears when the country changes.

Data-app Studio · rich form fields - image upload + cascading dropdowns.

Avatar - an upload field renders SvFileInput (preview + file picker). With no upload handler it stores an inline data URL, so it works with no backend; pass uploads to push to storage instead. City - a DEPENDENT dropdown: its options are computed from the chosen Country (dependentOptions), and it clears if the country changes.

Imports, features and API used

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

Frequently asked questions

How is the uploaded image stored?

Without an uploads handler the field reads the file as a data URL and stores it in the row, which works offline. Pass uploads with a function that sends the file to storage and returns a URL to store that instead.

How do I make one field's options depend on another?

Give SvGridEditPanel dependentOptions with a function per field: city: (row) => citiesFor(row.country). The panel re-evaluates it when the country changes and clears the city if it is no longer valid.

How does the grid render the avatar?

The avatar column is mapped to a cell that renders an img from the stored URL, so the grid and the form show the same image.

Related documentation

Related articles

Source code (198-studio-form-fields.svelte)

<script lang="ts">
  /**
   * Data-app Studio · rich form fields - image upload + cascading dropdowns.
   *
   *   Avatar  - an `upload` field renders SvFileInput (preview + file picker).
   *             With no upload handler it stores an inline data URL, so it works
   *             with no backend; pass `uploads` to push to storage instead.
   *   City    - a DEPENDENT dropdown: its options are computed from the chosen
   *             Country (`dependentOptions`), and it clears if the country changes.
   */
  import { SvGrid, createServerDataSource, renderSnippet, type ServerState } from '@svgrid/grid'
  import { SvGridEditPanel, createInMemoryDataSource, schemaToColumns, type EntitySchema } from '@svgrid/enterprise'
  import SvGridStudio from '../shared/SvGridStudio.svelte'

  type Member = { id: string; name: string; avatar: string; country: string; city: string }

  const schema: EntitySchema<Member> = {
    name: 'members', label: 'Team member', idField: 'id',
    fields: [
      { field: 'id', type: 'text', primaryKey: true, readonly: true, hidden: { form: true } },
      { field: 'avatar', type: 'text', label: 'Avatar', upload: { image: true, accept: 'image/*' } },
      { field: 'name', type: 'text', required: true, minLength: 2 },
      { field: 'country', type: 'enum', options: [
        { value: 'US', label: 'United States' }, { value: 'DE', label: 'Germany' }, { value: 'JP', label: 'Japan' },
      ] },
      { field: 'city', type: 'text' }, // form control comes from dependentOptions below
    ],
  }

  const citiesByCountry: Record<string, string[]> = {
    US: ['New York', 'San Francisco', 'Los Angeles'],
    DE: ['Berlin', 'Munich', 'Hamburg'],
    JP: ['Tokyo', 'Osaka', 'Kyoto'],
  }
  const cityOptions = (values: Partial<Member>) =>
    (citiesByCountry[values.country ?? ''] ?? []).map((c) => ({ value: c, label: c }))

  const seed: Member[] = [
    { id: 'm1', name: 'Ada Lovelace', avatar: '', country: 'US', city: 'New York' },
    { id: 'm2', name: 'Alan Turing', avatar: '', country: 'DE', city: 'Berlin' },
    { id: 'm3', name: 'Grace Hopper', avatar: '', country: 'JP', city: 'Tokyo' },
  ]

  const columns = schemaToColumns(schema)
  const source = createInMemoryDataSource(seed, schema)

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

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

{#snippet AvatarCell(props: { row: Member })}
  {#if props.row.avatar}
    <img class="ff-avatar" src={props.row.avatar} alt="" />
  {:else}
    <span class="ff-avatar ff-avatar--empty">{props.row.name?.[0] ?? '?'}</span>
  {/if}
{/snippet}

<SvGridStudio
  title="Rich form fields"
  subtitle="Double-click a member: upload an <strong>avatar</strong> (image field) and pick a <strong>City</strong> that cascades from the Country."
>
  {#snippet toolbar()}
    <button class="st-btn st-btn--primary" onclick={() => (editing = null)}>+ New member</button>
  {/snippet}

  <SvGrid responsive={true}
      columnResize
    data={view.rows}
    columns={columns.map((c) => (c.field === 'avatar' ? { ...c, cell: (ctx) => renderSnippet(AvatarCell, { row: ctx.row.original as Member }) } : c))}
    loading={view.loading}
    fitColumns
    onRowDoubleClick={(e) => (editing = e.row)}
    containerHeight={280}
  />

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

<style>
  :global(.ff-avatar) {
    width: 30px; height: 30px; border-radius: 50%; object-fit: cover; display: inline-flex;
    align-items: center; justify-content: center; font-size: 12px; font-weight: 600;
    background: var(--sg-accent, #2563eb); color: var(--sg-on-accent, #fff); border: 1px solid var(--sg-border, #ddd);
  }
</style>

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