Skip to content

useDataForm ​

The main Svelte store for schema-driven forms. It manages field state, applies events and handlers, resolves groups and scoped actions, and returns proxied fields for dynamic overrides.

Basic Usage ​

svelte
<script lang="ts">
import { useDataForm, getRenderer } from '@ybyra/svelte'
import { Scope } from '@ybyra/core'

const formStore = useDataForm({
  schema: PersonSchema.provide(),
  scope: Scope.add,
  events: personEvents,
  handlers: personHandlers,
  component: componentContract,
  translate: t,
})

let form = $derived($formStore)
</script>

<form>
  {#each form.fields as field (field.name)}
    {@const Renderer = getRenderer(field.config.component)}
    {@const props = formStore.getFieldProps(field.name)}
    <Renderer {...props} />
  {/each}

  {#each form.actions as action (action.name)}
    <button onclick={action.execute}>
      {action.label}
    </button>
  {/each}
</form>

Store API ​

useDataForm returns a Readable store extended with mutation methods. Use $formStore to subscribe to the reactive snapshot and call methods on the store object itself.

typescript
const formStore = useDataForm({ /* ... */ })

// reactive snapshot via auto-subscription
let form = $derived($formStore)

// mutation methods on the store
formStore.setValue('name', 'John')
formStore.reset()

Options ​

typescript
interface UseDataFormOptions {
  schema: SchemaProvide
  scope: ScopeValue
  events?: Record<string, Record<string, EventFn>>
  handlers?: Record<string, HandlerFn>
  hooks?: SchemaHooks
  context?: Record<string, unknown>
  component: ComponentContract
  initialValues?: Record<string, unknown>
  translate?: TranslateContract
}
OptionRequiredDescription
schemaYesOutput of SchemaDefinition.provide()
scopeYesCurrent scope (Scope.add, Scope.edit, etc.)
eventsNoField event handlers from SchemaDefinition.events()
handlersNoAction handlers from SchemaDefinition.handlers()
hooksNoLifecycle hooks from SchemaDefinition.hooks()
contextNoContextual data (e.g., route params with record ID)
componentYesComponentContract implementation
initialValuesNoInitial field values
translateNoTranslation function

Snapshot Properties ​

The $formStore snapshot contains:

PropertyTypeDescription
loadingbooleanTrue while bootstrap hooks are running
stateRecord<string, unknown>Current form values
fieldsResolvedField[]Fields filtered by current scope
groupsFieldGroup[]Groups with their resolved fields
ungroupedResolvedField[]Fields not assigned to any group
sectionsFormSection[]Interleaved groups and ungrouped fields
actionsResolvedAction[]Actions filtered by current scope
errorsRecord<string, string[]>Validation errors per field
dirtybooleanTrue if any field has changed
validbooleanTrue if all validations pass

Store Methods ​

Methods are called on the store object, not the snapshot:

MethodDescription
setValue(field, value)Set a single field value
setValues(values)Set multiple field values
reset(values?)Reset form to initial or provided values
validate()Run all validations, returns boolean
getFieldProps(name)Get FieldRendererProps for a field

FieldRendererProps ​

getFieldProps() returns everything a field renderer needs:

typescript
interface FieldRendererProps {
  domain: string
  name: string
  value: unknown
  config: FieldConfig
  proxy: FieldProxy
  errors: string[]
  scope: ScopeValue
  onChange(value: unknown): void
  onBlur(): void
  onFocus(): void
}
PropDescription
domainSchema domain name
nameField name
valueCurrent field value
configField configuration (component, kind, validations, etc.)
proxyDynamic overrides (hidden, disabled, width, height, state)
errorsValidation error messages
scopeCurrent scope
onChangeValue change callback (triggers change events)
onBlurBlur callback (triggers blur events)
onFocusFocus callback (triggers focus events)

Rendering with Sections ​

svelte
<script lang="ts">
import { useDataForm, getRenderer } from '@ybyra/svelte'

const formStore = useDataForm({ /* ... */ })
let form = $derived($formStore)
</script>

<form>
  {#each form.sections as section, i}
    {#if section.kind === 'group'}
      <fieldset>
        <legend>{t(`person.groups.${section.name}`)}</legend>
        {#each section.fields as field (field.name)}
          {@const Renderer = getRenderer(field.config.component)}
          <Renderer {...formStore.getFieldProps(field.name)} />
        {/each}
      </fieldset>
    {:else}
      {#each section.fields as field (field.name)}
        {@const Renderer = getRenderer(field.config.component)}
        <Renderer {...formStore.getFieldProps(field.name)} />
      {/each}
    {/if}
  {/each}
</form>

ResolvedField ​

typescript
interface ResolvedField {
  name: string
  config: FieldConfig
  proxy: FieldProxy
}

ResolvedAction ​

typescript
interface ResolvedAction {
  name: string
  config: ActionConfig
  execute(): void | Promise<void>
}

FieldGroup ​

typescript
interface FieldGroup {
  name: string
  config: GroupConfig
  fields: ResolvedField[]
}

FormSection ​

typescript
type FormSection =
  | { kind: 'group'; name: string; config: GroupConfig; fields: ResolvedField[] }
  | { kind: 'ungrouped'; fields: ResolvedField[] }

Released under the MIT License.