Skip to content

Form Dialog

formDialog runs a createForm store inside a modal JSVision dialog. It owns validation, submission, cancellation, cleanup, and the difficult async period where every closing path must be sealed. Your body callback only composes controls bound to fields.

Usage

ts
import { formDialog } from '@jsvision/forms';
import { Group, Input, at } from '@jsvision/ui';
const result = await formDialog(app, {
  schema: profileSchema,
  initial: { name: '' },
  width: 40,
  height: 9,
  body: (form) => at(new Input({ value: form.field('name').value }), 2, 1, 30, 1),
});

Live example

Validate and coerce a profile, then launch the same fields through the real modal formDialog helper.

Props and public state

formDialog(host, options) creates a Form through createForm, gives it to body, and resolves to the schema's coerced output or null. The raw initial type may intentionally differ from the resolved type—for example, a text field can start as '42' and resolve through z.coerce.number() as 42. FormDialogOptions is the complete construction contract.

Configuration

FormDialogOptions fieldTypeDefaultPurpose
schemaz.ZodObjectrequiredDefines synchronous validation and the resolved output type.
initialraw field recordrequiredSupplies editable values before schema parsing or coercion.
body(form: Form) => ViewrequiredBuilds controls bound to form.field(name).value.
width, heightnumberrequiredSet the requested cell size; the desktop may clamp it.
asyncValidatorsper-field validator mapnoneAdds asynchronous field checks that OK force-runs.
asyncDebounceMsnumber300Delays validation caused by ordinary field changes.
onSubmitcoerced-value callbacknonePerforms persistence inside the sealed submit gate.
titlestringuntitled frameLabels the modal frame.
okTextstringlocalized ~O~KReplaces the default OK button text and accelerator.

The returned promise is the public result channel. Inside body, the Form exposes each field's raw value, touched/error state, asynchronous error state, and the aggregate submitting() state.

Validation and submission

OK touches every field, force-runs async validation, then calls onSubmit with coerced values. Invalid fields keep the modal open. A rejected submit also keeps it open so the body can present a durable failure message.

ts
import { formDialog } from '@jsvision/forms';
import { Input } from '@jsvision/ui';

const result = await formDialog(app, {
  schema: profileSchema, // age uses z.coerce.number()
  initial: { age: '' }, // raw editing value
  asyncValidators: { age: async (value) => (value === '13' ? 'Choose another value' : null) },
  onSubmit: async (values) => saveProfile(values), // values.age is a number
  width: 42,
  height: 9,
  body: (form) => new Input({ value: form.field('age').value }),
});

While asynchronous submission is active, OK, Cancel, Escape, close, and application quit are sealed against races. The dialog always removes its window and disposes the form, including body-builder and submit failure paths.

onSubmit rejection is intentionally not converted into a generic alert. Keep an application-owned error signal in the body, report the rejection there, and let the user retry or cancel after the seal lifts.

Sizing and layout

Width and height are required because the helper cannot infer an opaque body. The helper clamps to the desktop and wraps the standard action group when necessary. Position body controls inside the covered content area.

Best practices

  • Bind controls directly to form.field(name).value; keep a single source of truth.
  • Put persistence in onSubmit so the dialog cannot close before the save settles.
  • Surface rejected submit errors inside the body; the helper deliberately invents no error UI.

Theming

The frame uses dialog; hosted controls retain their normal Input, Label, Button, and selection roles from the active theme.

  • Dialog — the modal container and close gate.
  • Input — a common field binding.
  • API reference — generated helper signature.