Submit & draft saves

Your application decides where to send data and when to persist a draft.

Submit

TSX
<Tungsten schema={schema} config={config} dataSources={sources}
  onSubmit={async (values) => {
    await fetch('/api/forms', { method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(values) })
  }} />

The callback receives validated values after file uploads. Hidden and disabled fields retain submitted values without blocking validation; inactive invalid sections are revealed. Endpoint callbacks observe the settled HTTP outcome. Without an endpoint, your callback promise determines the outcome and your application displays feedback.

TSX
const config = {
  apis: { main: { baseUrl: 'https://example.com/api/' } },
  autosave: { endpoint: { api: 'main', endpoint: 'drafts/42' }, debounceMs: 200 },
  submission: { endpoint: { api: 'main', endpoint: 'forms/42/submit' } },
}
<Tungsten schema={schema} config={config}
  onSave={(values, outcome) => console.log(outcome)} />

Autosave uses a trailing 200 ms debounce by default and permits overlapping requests. Each body contains values, timestamp, revision and writerId; enforce ordering on your server. Endpoint feedback defaults on. Set showStatus to false to render your own feedback.

Save a draft

onStateChange reports completed evaluations, including invalid values. Persist snapshot.values rather than the rendered field state. Implement debouncing in your host application; 200 ms is a reasonable starting point.

Draft persistence is owned by your application. Use server-enforced revisions or conditional writes so an older, slower request cannot overwrite a newer draft. Unix timestamps can tag drafts, but client clocks alone do not establish reliable ordering.

TSX
import { useEffect, useRef, useState } from 'react'
import { Tungsten } from '@myoon/tungsten/react'

// Load savedValues and its ETag from your draft endpoint first.
function DraftForm({ savedValues, savedETag, debounceMs = 200 }) {
  const timer = useRef()
  const revision = useRef(savedETag)
  const writes = useRef(Promise.resolve())
  const blocked = useRef(false)
  const [saveError, setSaveError] = useState('')
  useEffect(() => () => clearTimeout(timer.current), [])

  function saveDraft(snapshot) {
    clearTimeout(timer.current)
    timer.current = setTimeout(() => {
      writes.current = writes.current.then(async () => {
        if (blocked.current) return
        const response = await fetch('/api/drafts/contact', {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json',
            'If-Match': revision.current },
          body: JSON.stringify({ values: snapshot.values,
            savedAt: Date.now() }),
        })
        if (!response.ok) throw new Error('Draft save failed; reload before continuing.')
        const nextRevision = response.headers.get('ETag')
        if (!nextRevision) throw new Error('The draft endpoint must return an ETag.')
        revision.current = nextRevision
      }).catch(error => {
        blocked.current = true
        setSaveError(error.message)
      })
    }, debounceMs)
  }

  return <>
    <Tungsten schema={schema} initialValues={savedValues}
      onStateChange={saveDraft} onSubmit={submitForm} />
    {saveError && <p role="alert">{saveError}</p>}
  </>
}

This example queues writes from one form and stops saving after a failure. The server must atomically check If-Match, reject a stale revision with 412, and return the new ETag on success. Reload or resolve a conflict before saving again. For cross-origin endpoints, expose the ETag response header through CORS. savedAt is a Unix timestamp in milliseconds for display, not the ordering mechanism.

Reload a draft

TSX
<Tungsten schema={schema} initialValues={savedValues} onSubmit={save} />

Unknown keys and incompatible types reject the whole load. Valid saved select values survive the first options response. See Files for the difference between stored uploads and session-only local files.