Submit & draft saves
Your application decides where to send data and when to persist a draft.
Submit
<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.
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.
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
<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.