This is the abridged developer documentation for svelte-jsonschema-form v3
# svelte-jsonschema-form
> Svelte 5 library for creating forms based on JSON schema.
import { Code, Card, CardGrid, LinkCard } from '@astrojs/starlight/components';
import Themes from '@/components/themes/themes.astro';
## Installation
Choose your favorite theme:
## License
This project includes modifications of code from [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form), which is licensed under the Apache License, Version 2.0.
The rest of the project is under the MIT license.
# Components
import { Code } from '@astrojs/starlight/components'
import basicFromCode from '#/form/src/form/basic-form.svelte?raw'
The `@sjsf/form` package exports several basic ui components for creating forms:
- `Content`
- `SubmitButton`
- `Form`
- `BasicForm`
- `SimpleForm`
- `ErrorMessage`
- `Datalist`
- `Text`
- `Field`
- `HiddenIdPrefixInput`
Components are linked through the context of the form, example:
# Fields
import { Code } from '@astrojs/starlight/components'
import validationCode from '#/form/src/form/validation?raw'
import stateCode from '#/form/src/form/field-state?raw'
## Validation mode
## Field state
# ID Builder
```ts
type Path = Array;
type RPath = Readonly;
type FieldPath = Brand<"sjsf-path", RPath>;
interface IdentifiableFieldElement {
help: {};
"key-input": {};
examples: {};
title: {};
description: {};
errors: {};
oneof: {};
anyof: {};
form: {};
submit: {};
}
type FieldPseudoElement = keyof IdentifiableFieldElement | number;
interface FormIdBuilder {
fromPath: (path: FieldPath) => string;
}
```
# Merger
```typescript
export interface Merger {
mergeSchemas(a: Schema, b: Schema): Schema;
/**
* Merges schema and its `allOf` schemas into a single schema
*/
mergeAllOf(schema: Schema): Schema;
}
export interface FormMerger extends Merger {
/**
* Merges defaults of `schema` into `formData`
*/
mergeFormDataAndSchemaDefaults(
formData: SchemaValue | undefined,
schema: Schema
): SchemaValue | undefined;
}
```
# Options
```typescript
export type InitialErrors =
| ValidationError[]
// WARN: This should't be an array
| Iterable;
const UI_OPTIONS_REGISTRY_KEY = "uiOptionsRegistry";
export type UiOptionsRegistryOption = keyof UiOptionsRegistry extends never
? {
[UI_OPTIONS_REGISTRY_KEY]?: UiOptionsRegistry;
}
: {
[UI_OPTIONS_REGISTRY_KEY]: UiOptionsRegistry;
};
export interface IdBuilderFactoryOptions {
idPrefix: string;
schema: Schema;
uiSchema: UiSchemaRoot;
uiOptionsRegistry: UiOptionsRegistry;
validator: Validator;
merger: FormMerger;
valueRef: Ref;
}
export interface ValidatorFactoryOptions {
idBuilder: FormIdBuilder;
schema: Schema;
uiSchema: UiSchemaRoot;
uiOptionsRegistry: UiOptionsRegistry;
/**
* This is a getter that can be used to access the Merger lazily.
*/
merger: () => FormMerger;
}
export interface MergerFactoryOptions {
validator: Validator;
schema: Schema;
uiSchema: UiSchemaRoot;
uiOptionsRegistry: UiOptionsRegistry;
}
// from '@sjsf/form/lib/svelte.svelte'
export type Bind = [get: () => T, set: (v: T) => void];
export type Creatable =
| ((options: Options) => Result)
| (() => Result)
| Result;
export interface FormOptions extends UiOptionsRegistryOption {
schema: Schema;
theme: Theme;
translation: Translation;
resolver: (ctx: FormState) => ResolveFieldType;
idBuilder: Creatable;
validator: Creatable;
merger: Creatable;
/**
* @default DEFAULT_ID_PREFIX
*/
idPrefix?: string;
icons?: Icons;
uiSchema?: UiSchemaRoot;
extraUiOptions?: ExtraUiOptions;
fieldsValidationMode?: FieldsValidationMode;
disabled?: boolean;
initialValue?: DeepPartial;
value?: Bind;
initialErrors?: InitialErrors;
/**
* @default waitPrevious
*/
submissionCombinator?: TasksCombinator<
[event: SubmitEvent],
FormValidationResult,
unknown
>;
/**
* @default 500
*/
submissionDelayedMs?: number;
/**
* @default 8000
*/
submissionTimeoutMs?: number;
/**
* @default 300
*/
fieldsValidationDebounceMs?: number;
/**
* @default abortPrevious
*/
fieldsValidationCombinator?: TasksCombinator<
[Config, FormValue],
Update,
unknown
>;
/**
* @default 500
*/
fieldsValidationDelayedMs?: number;
/**
* @default 8000
*/
fieldsValidationTimeoutMs?: number;
/**
* Submit handler
*
* Will be called when the form is submitted and form data
* snapshot is valid
*
* Note that we rely on `validator.validateFormData` to check that the
* `formData is T`. So make sure you provide a `T` type that
* matches the validator check result.
*/
onSubmit?: (value: T, e: SubmitEvent) => void;
/**
* Submit error handler
*
* Will be called when the form is submitted and form data
* snapshot is not valid
*/
onSubmitError?: (
result: FailureValidationResult,
e: SubmitEvent,
form: FormState
) => void;
/**
* Form submission error handler
*
* Will be called when the submission fails by a different reasons:
* - submission is cancelled
* - error during validation
* - validation timeout
*/
onSubmissionFailure?: (state: FailedTask, e: SubmitEvent) => void;
/**
* Field validation error handler
*/
onFieldsValidationFailure?: (
state: FailedTask,
config: Config,
value: FormValue
) => void;
/**
* Reset handler
*
* Will be called when the form is reset.
*
* The event will be `undefined` if `reset` is called manually without passing an event.
*/
onReset?: (e?: Event) => void;
schedulerYield?: SchedulerYield;
keyedArraysMap?: KeyedArraysMap;
}
```
# Reactive flow
import Mermaid from '@/components/mermaid.astro'
import code from './reactive-flow.mmd?raw'
A simplified diagram of the form’s reactive flow:
# Schema
Currently we only support [JSON Schema Draft-07](https://json-schema.org/draft-07#draft-07)
and [discriminator.propertyName](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator) keyword.
For other drafts and schemes, adapters can be used; see the [advanced example](/examples/advanced/) `draft-2020-12` for more information.
## String formats
Some string formats has special meaning:
- `date-time` - `datetime-local` input type
- `uri` - `url` input type
- `color`, `date`, `time`, `email` - corresponding input type
- You can use `useDatePickerForDateFormat` function from
`@sjsf/form/fields/extra-widgets/date-picker` to use `datePickerWidget`
for `date` format
- `data-url` - file field (with `compat` resolver)
## Details and limitations
- Library only supports local definition referencing. The value in the `$ref` keyword should be a [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) in URI fragment identifier format.
- `$schema` keyword is ignored
- Some keywords are only involved in validation.
- `contains`
- `propertyNames`
- `not`
- `exclusiveMaximum`, `exclusiveMinimum` are not currently passed to input elements.
- `writeOnly`, `contentMediaType` and `contentEncoding` has no special meaning.
- `additionalProperties: false` produces incorrect schemas when used with schema dependencies.
- Properties declared inside the `anyOf`/`oneOf` should not overlap with properties "outside" of the `anyOf`/`oneOf`.
# State
```typescript
export type FormSubmission = Task<
[event: SubmitEvent],
ValidationResult,
unknown
>;
export type FieldsValidation = Task<
[config: Config, value: FieldValue],
Update,
unknown
>;
export interface FormState {
readonly submission: FormSubmission;
readonly fieldsValidation: FieldsValidation;
readonly isChanged: boolean;
readonly isSubmitted: boolean;
submit: (e: SubmitEvent) => void;
reset: (e?: Event) => void;
// Internals
[FORM_VALUE]: FormValue;
readonly [FORM_ID_PREFIX]: string;
readonly [FORM_ROOT_PATH]: FieldPath;
readonly [FORM_ID_FROM_PATH]: (path: FieldPath) => Id;
readonly [FORM_PATHS_TRIE_REF]: PathTrieRef;
readonly [FORM_ERRORS]: FormErrorsMap;
readonly [FORM_MARK_SCHEMA_CHANGE]: () => void;
readonly [FORM_KEYED_ARRAYS]: KeyedArraysMap;
readonly [FORM_FIELDS_VALIDATION_MODE]: number;
readonly [FORM_SCHEMA]: Schema;
readonly [FORM_UI_SCHEMA_ROOT]: UiSchemaRoot;
readonly [FORM_UI_SCHEMA]: UiSchema;
readonly [FORM_UI_OPTIONS_REGISTRY]: UiOptionsRegistry;
readonly [FORM_UI_EXTRA_OPTIONS]?: ExtraUiOptions;
readonly [FORM_VALIDATOR]: Validator;
readonly [FORM_MERGER]: FormMerger;
readonly [FORM_ICONS]?: Icons;
readonly [FORM_DISABLED]: boolean;
readonly [FORM_DATA_URL_TO_BLOB]: DataURLToBlob;
readonly [FORM_TRANSLATION]: Translation;
readonly [FORM_TRANSLATE]: Translate;
readonly [FORM_RESOLVER]: ResolveFieldType;
readonly [FORM_THEME]: Theme;
readonly [FORM_FIELDS_STATE_MAP]: SvelteMap;
}
```
## Methods
SJSF provides a set of functions to work with the form state that can either **read** (queries) or **modify** (commands) it.
:::note
Most of these methods are intended for **internal use** within form components.
:::
:::note
All the methods listed below are marked with `@query` or `@command` in their JSDoc.
:::
**Queries:**
`getComponent`, `getFieldComponent`, `getStableConfig`, `getFieldErrors`,
`getFieldErrorsByPath`, `getFieldsErrors`, `getFieldsErrorsByPath`, `hasErrors`,
`getErrors`, `hasFieldState`, `hasFieldStateByPath`, `getId`, `getSubtreePaths`,
`getChildPath`, `getPseudoPath`, `getPseudoId`, `getIdByPath`,
`getPseudoIdByPath`, `isSelect`, `isMultiSelect`, `isFilesArray`,
`retrieveSchema`, `sanitizeDataForNewSchema`, `getClosestMatchingOption`,
`getDefaultFieldState`, `retrieveUiSchema`, `uiTitleOption`, `retrieveUiOption`,
`retrieveNestedUiOption`, `uiOptionProps`, `uiOptionNestedProps`,
`retrieveTranslate`, `getFieldTitle`, `getFieldAction`,
`getFieldsValidationMode`, `validate`, `validateAsync`, `getValueSnapshot`
**Commands:**
`updateErrors`, `updateFieldErrors`, `updateFieldErrorsByPath`,
`setFieldStateByPath`, `validateField`, `validateAdditionalPropertyKey`,
`validateFileList`, `setValue`
### `query` vs `command`
In addition to the semantic difference, there is also a behavioral one when used in a **reactive context**.
When a `query` is called, the reactive variables it accesses will be **tracked**.
When a `command` is called, the accessed variables **will not be tracked**.
### `FieldPath` vs `RPath`
You may have noticed that some methods have alternate versions with the `ByPath` suffix.
Their signatures differ in the type of the `path` argument: `FieldPath` or `RPath`.
Since JavaScript lacks a primitive for **tuples**, the library uses **uniquified paths**.
When an `RPath` (a regular array) is passed, it is automatically converted into a `FieldPath` and cached in a **prefix tree**.
Each identical path returns the same reference, allowing `FieldPath` to serve as a stable **identifier** or **key** in `Map`/`WeakMap`.
Therefore, `ByPath` versions of methods are intended for situations where you **don’t have access to a `FieldPath`** or are working with **dynamically generated paths**.
## Direct modification of form state
If you are using a [controlled form](/guides/quickstart/#controlled-form),
you should consider the following aspects:
### Initialization
It is recommended to initialize the state as follows:
```ts
let value = $state(
merger.mergeFormDataAndSchemaDefaults(initialValue, schema)
);
```
### Arrays
To modify arrays, use one of the following methods:
1. Reassign
```ts
value.array = value.array.concat(123)
```
2. Use `KeyedArray` API
```ts
import { createForm, type KeyedArraysMap } from "@sjsf/form";
const keyedArraysMap: KeyedArraysMap = new WeakMap()
let value = $state({ array: [] })
const form = createForm({
keyedArraysMap,
value: [() => value, (v) => (value = v)],
// ...otherOptions
})
const api = keyedArraysMap.get(value.array)
if (api) {
api.push(123)
}
```
## Internals
The `@sjsf/form` package exports a number of utility functions (e.g. `getComponent`)
that let you safely interact with the form's internal state.
These helpers are the preferred way to read or manipulate internals,
since they are considered part of the supported API and follow semantic versioning.
For advanced cases, you may also access raw internals
by importing symbols from `@sjsf/form/internals` and using them on a `FormState` instance.
:::caution
Symbols from `@sjsf/form/internals` and the internal state itself
are not part of the public API.
They may change or be removed in any release, including patch versions.
:::
If you need functionality that isn't covered by the exported helpers,
please [open an issue](https://github.com/x0k/svelte-jsonschema-form/issues/new/choose)
or [start a discussion](https://github.com/x0k/svelte-jsonschema-form/discussions/new/choose)
on GitHub.
This way we can continue improving the API for everyone.
# Theme
import { TabItem, Tabs } from '@astrojs/starlight/components';
Essentially theme is a simple function:
```js
const fromRecord = (record) => (type) => record[type]
const theme = fromRecord({ ...components })
```
## Component types
All components can be divided into four **logical** types:
- components (generic UI element)
- `form`
- `submitButton`
- `button`
- `layout`
- `title`
- `label`
- `description`
- `help`
- `errorsList`
- widgets (input control)
- `textWidget`
- `numberWidget`
- `selectWidget`
- `checkboxWidget`
- templates (layout and presentation)
- `fieldTemplate`
- `objectTemplate`
- `objectPropertyTemplate`
- `arrayTemplate`
- `arrayItemTemplate`
- `multiFieldTemplate`
- fields (schema logic and orchestration)
- `stringField`
- `numberField`
- `integerField`
- `booleanField`
- `objectField`
- `arrayField`
- `tupleField`
- `nullField`
- `oneOfField`
- `anyOfField`
- `unknownField`
Below are examples of the components required to render the following JSON schema:
```json
{ "type": "string" }
```
```svelte
```
```svelte
{#if showMeta && ((showTitle && title) || description)}
{#if showTitle && title}
{@render action?.()}
{/if}
{#if description}
{/if}
{/if}
{@render children()}
{#if errors.length > 0}
{/if}
{#if help !== undefined}
{/if}
```
```svelte
```
```svelte
{title}
{#if config.required}
*
{/if}
```
## Foundational components
The `FoundationalComponentType` is a subset of all components
(`ComponentType`) that can be explicitly used in form elements.
For example, we can use `form` as an argument for `getComponent` because
`form` is `FoundationalComponentType`:
```svelte
```
The main purpose of this list is to determine which components you can replace
using the `ui:components` property from `UiSchema`.
This is an extensible list, but by default it corresponds to the components
listed in [Component types](#component-types).
## Extra components
If the default set of components is insufficient, you can add the necessary
components yourself.
The `@sjsf/form` library provides definitions and implementation of several extra
fields, as well as a set of definitions for extra widgets.
### Fields
Here is a list of extra fields that can be imported from `@sjsf/form/fields/extra/*`.
- `aggregated`
- `array-files`
- `array-native-files`
- `array-tags`
- `boolean-select`
- `enum`
- `file`
- `files`
- `multi-enum`
- `native-file`
- `native-files`
- `tags`
- `unknown-native-file`
To use them you can import them directly
```ts
import EnumField from "@sjsf/form/fields/extra/enum";
```
or use an `include` import
```ts
import "@sjsf/form/fields/extra/enum-include";
```
and replace the compatible field with it in `uiSchema`.
```ts
const uiSchema: UISchema = {
"ui:components": {
stringField: EnumField,
// Or if you used the `include` import
stringField: "enumField"
}
}
```
### Templates
List of extra templates that can be imported from `@sjsf/form/templates/extra/*`.
- `optional-array`
- `optional-field`
- `optional-multi-field`
- `optional-object`
These templates are intended for rendering optional fields and
differ from the original templates by the presence of the following condition:
```svelte
{#if config.required || !isNil(value)}
{@render children()}
{/if}
```
### Widgets
There are several types of extra widgets already defined in the library
(`@sjsf/form/fields/extra-widgets/*`):
- `aggregated`
- `checkboxes`
- `combobox`
- `date-picker`
- `date-range-picker`
- `file`
- `multi-select`
- `radio-buttons`
- `radio` (group)
- `range-slider` (two handles)
- `range`
- `rating`
- `switch`
- `tags`
- `textarea`
However, the ability to use them depends on the availability of a corresponding
implementation in your chosen theme.
# Translation
import { Code } from '@astrojs/starlight/components'
import code from '#/form/src/form/translation?raw'
# UI Schema
import { Code, LinkCard } from '@astrojs/starlight/components'
import uiSchemaExampleCode from './_ui-schema-example.ts?raw'
import extraUiOptionsCode from './_extra-ui-options.ts?raw'
UI schema allows you to customize the appearance of the form and influence its
behavior.
The UI schema object follows the tree structure of the form field hierarchy,
and defines how each property should be rendered.
```typescript
export interface UiSchemaContent {
/**
* Extendable set of UI options
*/
"ui:options"?: ResolvableUiOptions;
/**
* Components override
*/
"ui:components"?: Partial<{
[T in FoundationalComponentType]:
| Exclude, T>
| ComponentDefinitions[T];
}>;
items?: UiSchemaDefinition | UiSchemaDefinition[];
anyOf?: UiSchemaDefinition[];
oneOf?: UiSchemaDefinition[];
combinationFieldOptionSelector?: UiSchemaDefinition;
additionalProperties?: UiSchemaDefinition;
additionalPropertyKeyInput?: UiSchemaDefinition;
additionalItems?: UiSchemaDefinition;
}
export type UiSchema = UiSchemaContent & {
// This is should be `UiSchemaDefinition` type, but
// https://github.com/microsoft/TypeScript/issues/17867
[key: string]: UiSchemaContent[keyof UiSchemaContent];
};
export interface UiSchemaRef {
$ref: string;
}
export type UiSchemaDefinition = UiSchema | UiSchemaRef;
export type UiSchemaRoot = UiSchemaDefinition & {
"ui:globalOptions"?: UiOptions;
"ui:definitions"?: Record;
};
```
## Evaluation rules
Usually UI schema corresponds to the data structure described by json schema.
For example, with this JSON schema, the following UI schema would be correct:
Special cases:
### Ref
If the UI Schema contains a `$ref` key with a value of type `string`,
the `ui:definitions` field of the root UI schema will be searched
for the value of the `$ref` key, other fields will be ignored.
```
{
"ui:definitions": {
"foo": {
...
}
},
properties: {
foo: {
$ref: "foo"
}
}
}
```
### Array
Instead of defining indices in the UI schema, the `items` keyword should be used
to specify the UI schema for the elements of the array.
For a fixed array `items` also can be an array.
If you have additional items you should use `additionalItems` keyword
to specify the UI schema for them.
```
{
items: [, ...],
additionalItems:
}
```
### Object
You should use `additionalProperties` keyword to specify the UI schema for
additional properties.
You can use `additionalPropertyKeyInput` keyword to define an UI schema for
the additional property key input field.
### oneOf/anyOf
You can define separate UI schemas for each `oneOf/anyOf` branch
using the corresponding keyword in the UI schema.
Otherwise the UI schema of the current field will be used.
```
{
oneOf: [, ...]
}
```
## UI components
Using the `ui:components` property, you can replace any
[form component](/form/theme/#component-types) with a compatible one
using the name of the connected component or
the component itself directly.
Component `A` is compatible with component `B` if the properties and bindings
of component `B` extend the properties and bindings of component `A`.
```ts
export type CompatibleComponentType = {
[C in ComponentType]: Expand extends Expand<
ComponentProps[C]
>
? ComponentBindings[T] extends ComponentBindings[C]
? C
: never
: never;
}[ComponentType];
```
## UI options
The `UiOptions` type is an extensible set of components options. By default it
looks like this:
```typescript
type ItemTitle = (
title: string,
index: number,
fixedItemsCount: number,
itemValue?: SchemaValue | undefined
) => string;
type AdditionalPropertyKey = (key: string, attempt: number) => string;
interface UiOptions {
/**
* Overrides the title of the field.
*/
title?: string;
/**
* Overrides the description of the field (over the widget).
*/
description?: string;
/**
* List of labels for enum values in the schema.
*/
enumNames?: string[];
/**
* List of enum values that are disabled. Values are compared by strict equality.
*/
disabledEnumValues?: SchemaValue[];
/**
* Order of properties in the object schema.
* You must specify all properties or use the wildcard `*`.
*/
order?: string[];
/**
* Allow adding new properties to the object schema with `additionalProperties`.
* @default true
*/
expandable?: boolean;
/**
* Allow adding new items to the array schema.
* @default true
*/
addable?: boolean;
/**
* Allow reordering items in the array schema.
* If you want an orderable array of file fields, set this to `true` explicitly.
* @default true
*/
orderable?: boolean;
/**
* Allow removing items from the array schema.
* @default true
*/
removable?: boolean;
/**
* Allow duplicating items in the array schema.
* @default false
*/
copyable?: boolean;
/**
* Overrides the logic for creating a title for array elements
*/
itemTitle?: ItemTitle;
/**
* Overrides the logic for creating a new key for an additional property
*/
additionalPropertyKey?: AdditionalPropertyKey;
/**
* Help text for the field (under the widget).
*/
help?: string;
/**
* Hide the title of the field.
* If you want to show a title of the `boolean` field this should be set to `false` explicitly.
* @default false
*/
hideTitle?: boolean;
/**
* Overrides whether to use the `title` or `label` component in the `field` template
*/
useLabel?: boolean;
/**
* Display errors from child elements (applies only to aggregating fields like `tags`).
* @default false
*/
collectErrors?: boolean;
/**
* Overrides form translation
*/
translations?: Partial;
/**
* Field action
*/
action?: FieldAction;
/**
* A typed field action, takes precedence over `action`
*/
actions?: Partial<{
[T in ActionField]: FieldAction;
}>;
}
```
### Registry
```typescript
export interface UiOptionsRegistry {}
export type ResolvableUiOption =
| {
[K in keyof UiOptionsRegistry as UiOptionsRegistry[K] extends T
? K
: never]: `registry:${K}`;
}[keyof UiOptionsRegistry]
| T;
export type ResolvableUiOptions = {
[K in keyof UiOptions]: ResolvableUiOption;
};
```
### Conventions
- Each `component`/`widget` in the theme should define at least one UI option
to allow customization of it
- All parameters must be prefixed by the theme name (e.g. `daisyui5RadioButtons`).
- Only a `basic` theme can define options without a prefix and other themes
should use the `basic` theme UI options for the corresponding `components`/`widgets`
if their properties are compatible.
- Using UI options of one component in another (even if they are compatible)
is forbidden, e.g. `text` and `textarea` widgets must use separate options.
# Validator
Validator - a collection of functions used by the form to validate data:
- `isValid` - required. Used to correctly handle conditional keywords such as `oneOf`, `anyOf`, and `if/then/else`.
- `validateFormValue` - required unless `validateFormValueAsync` is defined.
- `validateFormValueAsync` - required unless `validateFormValue` is defined.
- `validateFieldValue` - optional
- `validateFieldValueAsync` - optional
- `validateAdditionalPropertyKey` - optional
You can easily extend/modify the validator to suit your needs.
```ts
import type { FormValueValidator, ValidatorFactoryOptions } from "@sjsf/form";
import { createFormValidator } from "@sjsf/ajv8-validator";
export function validator(options: ValidatorFactoryOptions) {
const validator = createFormValidator(options);
return {
...validator,
validateFormValue(rootSchema, formValue) {
const errors = validator.validateFormValue(rootSchema, formValue);
// Your logic
return errors
},
} satisfies FormValueValidator;
}
```
## API
```typescript
export interface Validator {
isValid(
schema: SchemaDefinition,
rootSchema: Schema,
formValue: SchemaValue | undefined
): boolean;
}
export interface ValidationError {
path: RPath;
message: string;
}
export interface SuccessValidationResult {
readonly value: Output;
readonly errors?: undefined;
}
export interface FailureValidationResult {
readonly value: FormValue;
readonly errors: ReadonlyArray;
}
export type ValidationResult =
| SuccessValidationResult
| FailureValidationResult;
export interface FormValueValidator {
validateFormValue: (
rootSchema: Schema,
formValue: FormValue
) => ValidationResult;
}
export interface AsyncFormValueValidator {
validateFormValueAsync: (
signal: AbortSignal,
rootSchema: Schema,
formValue: FormValue
) => Promise>;
}
export type AnyFormValueValidator =
| FormValueValidator
| AsyncFormValueValidator;
export type FormValidator = Validator & AnyFormValueValidator;
export type Update = T | ((data: T) => T);
export interface FieldValueValidator {
validateFieldValue: (
field: Config,
fieldValue: FieldValue
) => Update;
}
export interface AsyncFieldValueValidator {
validateFieldValueAsync: (
signal: AbortSignal,
field: Config,
fieldValue: FieldValue
) => Promise>;
}
export type AnyFieldValueValidator =
| FieldValueValidator
| AsyncFieldValueValidator;
export interface AdditionalPropertyKeyValidator {
validateAdditionalPropertyKey: (
key: string,
schema: Schema
) => Update;
}
export interface AsyncFileListValidator {
validateFileListAsync: (
signal: AbortSignal,
fileList: FileList,
config: Config
) => Promise>;
}
# Component casting
SJSF provides a rich set of interchangeable components,
but some of them cannot be used directly out of the box
(`tagsField`, `filesField`, `nativeFileField`, etc.).
Let's look at a few ways to work with them.
## Ignore errors (not recommended)
You can suppress typing errors if you just want to quickly test an idea
or see how a component looks in place.
```ts
const uiSchema: UiSchema = {
"ui:components": {
// @ts-expect-error
arrayField: "tagsField"
}
}
```
## Create an adapter component
You can create a new component that adapts the interface of the original component
(for example, `arrayField`) to the interface of the replacement component (`tagsField`).
```svelte
{
assertStrings(value);
return value;
},
(v) => (value = v)
}
/>
```
Why is this needed? Throwing an error when data formats don't match
is just one possible approach. You should define your own adaptation strategy
and implement it according to your needs.
However, if you're fine with the `assert`-based approach,
you can use the following prebuilt field components:
- `array-files`
- `array-native-files`
- `array-tags`
- `unknown-native-file`
To reduce boilerplate, you can also use the `cast` utility
from the `@sjsf/form/lib/component` module:
:::caution
This utility relies on internal details of Svelte 5 that may change even in a patch release.
Use it with care.
:::
```ts
import { cast } from "@sjsf/form/lib/component";
import type { ComponentDefinition } from "@sjsf/form";
import TagsField from "@sjsf/form/fields/extra/tags.svelte";
import { assertStrings } from '$lib/form/cast';
declare module "@sjsf/form" {
interface ComponentProps {
arrayTagsField: FieldCommonProps;
}
interface ComponentBinding {
arrayTagsField: "value";
}
}
const arrayTagsField = cast(TagsField, {
value: {
transform(props) {
assertStrings(props.value);
return props.value;
},
},
}) satisfies ComponentDefinition<"arrayField">;
```
## Extend the resolver
Instead of proving component compatibility at the type level,
you can extend the resolver so that components are applied automatically
to suitable JSON Schemas.
# Custom components
import { Tabs, TabItem, Code } from '@astrojs/starlight/components';
import attributesTypesCode from '#/form/dist/form/state/attributes.d.ts?raw'
You can create your own form components. Any built-in component can be replaced
with a custom one, giving you full control over rendering and behavior.
## Component creation
To create a component, you just need to create a Svelte component
with a compatible `$props` type.
The easiest way to do this is to use the `ComponentProps` property registry as follows:
```ts
import type { ComponentProps } from "@sjsf/form";
let {
value = $bindable(),
config,
handlers,
}: ComponentProps["numberWidget"] = $props();
```
You will then be able to replace `numberWidget` with your component via the UI schema:
```ts
import type { Schema, UiSchema } from "@sjsf/form";
import MyNumberWidget from "./my-number-widget";
const schema: Schema = {
type: "number"
}
const uiSchema: UiSchema = {
"ui:components": {
"numberWidget": MyNumberWidget
}
}
```
You can also register a new or overwrite an old component in the selected theme as follows:
```ts
import { extendByRecord, overrideByRecord } from "@sjsf/form/lib/resolver";
import { theme } from "@sjsf/some-theme";
import MyNumberWidget from "./my-number-widget";
/** Register a new component **/
declare module "@sjsf/form" {
interface ComponentProps {
// NOTE: Prefer to declare new components using some prefix to avoid
// conflicts in the future
myNumberWidget: ComponentProps["numberWidget"];
}
interface ComponentBindings {
myNumberWidget: "value";
}
}
export const myTheme = extendByRecord(theme, { myNumberWidget: MyNumberWidget });
/** Override the default component **/
export const myTheme = overrideByRecord(theme, { numberWidget: MyNumberWidget })
```
## Retrieving attributes
Each component is responsible for constructing the set of attributes it needs.
This decouples SJSF form from any specific UI library.
In simple cases, you can use information from `config` and the `uiOption` function
to generate attributes:
```ts
const { config, uiOption }: ComponentProps["arrayTemplate"] = $props();
const description = $derived(uiOption("description") ?? config.schema.description);
```
### UI options
You can define new UI options as follows:
```ts
declare module "@sjsf/form" {
interface UiOptions {
// NOTE: Prefer to declare new UI options using some prefix to avoid
// conflicts in the future
myUiOption?: boolean;
}
}
```
Then use the `uiOption` function to get the value of the UI option:
```ts
const { uiOption } = $props();
const value = $derived(uiOption("myUiOption") ?? false);
// Equivalent to:
import { retrieveUiOption } from "@sjsf/form";
const { config } = $props();
const ctx = getFormContext();
const value = $derived(retrieveUiOption(ctx, config, "myUiOption") ?? false);
```
When `uiOption` is used, the value defined via `extraUiOptions` will replace
the value from the UI schema.
You can use the `uiOptionProps` function to merge object values from
the UI schema and `extraUiOptions`:
```ts
import type { RadioGroupItemProps, WithoutChildrenOrChild } from 'bits-ui';
import { getFormContext, uiOptionProps, type ComponentProps } from '@sjsf/form';
declare module '@sjsf/form' {
interface UiOptions {
shadcnRadioItem?: Omit, 'value'>;
}
}
const ctx = getFormContext();
const { config, handlers }: ComponentProps['radioWidget'] = $props();
const itemAttributes = $derived(
uiOptionProps('shadcnRadioItem')(
{
onclick: handlers.oninput,
onblur: handlers.onblur
},
config,
ctx
)
);
```
### Helpers
For more complex interactive components, you may need to consider many properties
and their priorities.
The library provides a set of functions for forming attributes for both
standard HTML elements and custom components. These functions can be categorized
into two categories based on their level of abstraction:
1. **properties**
These functions are designed to form a set of properties by combining them in the
desired order, have the suffix `Prop`, `Props` or `Attachment`.
2. **attributes**
These functions are pre-prepared compositions of functions from the
previous category, have the suffix `Attributes`.
l.startsWith("export declare"))
.map(l => l.substring(15))
.join('\n')}
/>
```ts
import type { HTMLButtonAttributes } from "svelte/elements";
import {
composeProps,
disabledProp,
getFormContext,
uiOptionProps,
uiOptionNestedProps,
type ComponentProps,
} from "@sjsf/form";
import type { ButtonType } from "@sjsf/form/fields/components";
declare module "@sjsf/form" {
interface UiOptions {
button?: HTMLButtonAttributes;
buttons?: {
[B in ButtonType]?: HTMLButtonAttributes;
};
}
}
const { type, onclick, config, disabled }: ComponentProps["button"] = $props();
const ctx = getFormContext();
const props = $derived(composeProps(
ctx,
config,
{
disabled,
type: "button",
onclick,
} satisfies HTMLButtonAttributes,
uiOptionProps("button"),
uiOptionNestedProps("buttons", (p) => p[type]),
disabledProp
))
```
```ts
import type { HTMLInputAttributes } from "svelte/elements";
declare module "@sjsf/form" {
interface UiOptions {
number?: HTMLInputAttributes;
}
}
import { getFormContext, inputAttributes, type ComponentProps } from "@sjsf/form";
let {
value = $bindable(),
config,
handlers,
}: ComponentProps["numberWidget"] = $props();
const ctx = getFormContext();
const attributes = $derived(
inputAttributes(ctx, config, "number", handlers, {
type: "number",
style: "flex-grow: 1",
})
);
```
```ts
import type { ComponentProps as SvelteComponentProps } from 'svelte';
import { Switch as SkeletonSwitch } from '@skeletonlabs/skeleton-svelte';
import '@sjsf/form/fields/extra-widgets/switch';
declare module '@sjsf/form' {
interface UiOptions {
skeleton3Switch?: SvelteComponentProps;
}
}
import { customInputAttributes, getFormContext, type ComponentProps, getId } from '@sjsf/form';
let { config, value = $bindable(), handlers, errors }: ComponentProps['switchWidget'] = $props();
const ctx = getFormContext();
const attributes = $derived(customInputAttributes(ctx, config, 'skeleton3Switch', {
ids: {
hiddenInput: getId(ctx, config.path)
},
required: config.required,
readOnly: config.schema.readOnly,
invalid: errors.length > 0,
onCheckedChange: (e) => {
value = e.checked;
handlers.onchange?.();
},
checked: value
}))
```
## Working with enumerations
To implement widgets that use enumerations (`options: EnumOption[]`),
use the functions from the `@sjsf/form/options.svelte` module.
```ts
import { idMapper, singleOption } from "@sjsf/form/options.svelte";
const mapped = singleOption({
mapper: () => idMapper(options),
value: () => value,
update: (v) => (value = v),
});
```
Now, `mapped.current` will correspond to the `option.id` of the current option,
and when it changes, value will be assigned the corresponding `option.value`:
```svelte
{placeholder}
{#each options as option (option.id)}
{option.label}
{/each}
```
You can also use the `multipleOptions` function to implement multi-select behavior.
In this case, `mapped.current` will be an array of `option.id`:
```svelte
...
```
## Retrieving theme components
If you need to use theme components inside your custom component, you have two options:
1. **Import the component directly**
```ts
import Button from "@sjsf/your-theme/components/button.svelte";
```
2. **Use the `getComponent` function**
```ts
import { getComponent, getFormContext } from "@sjsf/form";
const { config } = $props();
const ctx = getFormContext();
const Button = $derived(getComponent(ctx, "button", config));
```
To use `getComponent` with a custom component, add it to the `FoundationalComponents` registry:
```ts
declare module "@sjsf/form" {
interface FoundationalComponents {
myNumberWidget: {};
}
}
// Now the following code works:
import { getComponent, type UiSchema } from "@sjsf/form";
const Widget = $derived(getComponent(ctx, "myNumberWidget", config));
const uiSchema: UiSchema = {
"ui:components": {
myNumberWidget: "numberWidget"
}
};
```
You can also use the `getFieldComponent` function to get the component
responsible for displaying/processing the current value:
```svelte
```
## FoundationalFieldType
To use a custom component in a `resolver` function - it must be declared as
`FoundationalComponentType` (`keyof FoundationalComponents`) and its properties
must be compatible with the `FieldCommonProps` type.
The compatibility is checked as follows:
```ts
type IsFoundationalField =
FieldCommonProps extends ComponentProps[T]
? ComponentProps[T] extends FieldCommonProps
? true
: false
: false;
```
# Dynamic forms
import { Code, Card, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components';
import FormCard from '@/components/form-card.astro'
import OneOfForm from './one-of.svelte'
import oneOfFromCode from './one-of.svelte?raw'
import DependenciesForm from './dependencies.svelte'
import dependenciesFormCode from './dependencies.svelte?raw'
import IfElseThenFrom from './if-then-else.svelte'
import ifElseThenFromCode from './if-then-else.svelte?raw'
Using the following JSON Schema keywords, you can build forms that change in
response to user input.
## oneOf / anyOf
Fields `oneOf` or `anyOf` can be used as a **virtual selector**.
This selector **does not modify form data directly** but determines which schema is active.
When the form is initialized with `initialValue`, SJSF **automatically selects the most suitable schema** based on the data.
## dependencies
The `dependencies` keyword allows you to declare relationships between fields,
where the presence or value of one field affects the schema of others.
## if/then/else
The `if/then/else` keywords allow you to define conditional schema branches based on form data.
## patternProperties
If your schema uses the `patternProperties` keyword, see the [advanced example](/examples/advanced/)
`pattern-properties-validator` to improve the user experience.
# Fields resolution
import { Code } from '@astrojs/starlight/components'
import basicResolverCode from '#/form/src/form/resolvers/basic?raw'
import compatResolverCode from '#/form/src/form/resolvers/compat?raw'
To determine which field to use to display your JSON schema,
the form uses `resolver` function.
Let's take a look at the `basic` resolver implementation
(`@sjsf/form/resolvers/basic`):
As you can see `resolver` just selects the field according to the schema type.
This approach is simple and straightforward, but in some cases it can lead to
additional work.
Suppose you have the following scheme:
```ts
const schema: Schema = {
enum: ["foo", "bar", "baz"]
}
```
For this scheme, the `basic` resolver will select `stringField`,
and `stringField` will display the `textWidget` widget by default
although you probably wanted to see select or radio widgets.
To achieve this you need to specify in the UI scheme the field:
```ts
const uiSchema: UiSchema = {
"ui:components": {
// The `selectWidget` will now be displayed by default
stringField: "enumField",
// You can also replace `selectWidget` with `radioWidget`
// selectWidget: "radioWidget"
}
}
```
Or you can modify `resolver` so that `enumField` is always used
when a scheme with `enum` is detected:
```ts
return ({ schema }) => {
if (schema.enum !== undefined) {
return "enumField"
}
...
}
```
It is recommended that you copy the `basic` resolver code into your project and
modify it to suit your needs.
As an example (or temporary solution) you can use the `compat` resolver
(`@sjsf/form/resolvers/compat`) that reproduces resolution logic from v1:
# File handling
import { Code, Card, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components';
import FormCard from '@/components/form-card.astro'
import CustomKeywordForm from './file-handling/custom-keyword.svelte'
import customKeywordCode from './file-handling/custom-keyword.svelte?raw'
import FileSizeValidatorForm from './file-handling/file-size-validator.svelte'
import fileSizeValidatorCode from './file-handling/file-size-validator.svelte?raw'
SJSF provides several ways of working with files.
## Data URL
When using `file` or `files` fields, the form automatically converts
uploaded files into Data URL strings.
**Advantages:**
- Easy-to-use format compatible with JSON Schema validators
- Files can be provided from the server (as initial values or when re-sending form data after a server-side validation error)
**Disadvantages:**
- Base64 encoding increases file size by about 30%
- Requires encoding/decoding steps to work with the actual file
- Some metadata is lost
You can use the `createDataURLtoBlob` and `fileToDataURL` functions
(available in both browser and server environments) from
the `@sjsf/form/lib/file` module to convert files to and from Data URLs.
## Native File
When using `native-file` or `native-files` fields, uploaded files are
represented directly as `File` objects.
**Advantages:**
- No encoding/decoding required
- File metadata is preserved
**Disadvantages:**
- Cannot be provided from the server
- May cause issues for JSON-oriented tools (e.g. JSON Schema validators)
### SvelteKit
With any file handling method, you must set `enctype="multipart/form-data"` on the form.
For native file handling, add the following `transport` hook in your `hooks.ts` file:
```ts
import type { Transport } from '@sveltejs/kit';
export const transport: Transport = {
File: {
encode: (v) => v instanceof File && 'file',
decode: () => undefined
}
};
```
## Preloading files
Another approach is to **preload files** into a storage backend as soon as the user selects them.
Instead of embedding file data in the form, the form value only stores a lightweight file identifier (ID).
This avoids the drawbacks of Data URLs and native `File` objects:
- Smaller payloads
- No `base64` overhead
- Files can be validated or processed on the server before submission
- Files can be provided from the server
See the [advanced example](/examples/advanced/) `preupload-file` for more information.
## Validation
SJSF provides several options for validating files.
### Custom keyword
The most flexible approach is to add a **custom JSON Schema keyword**.
This method works consistently on both the server and the client,
and the timing of validation can be controlled with the `fieldsValidationMode` option.
However, not all validators support custom keywords,
and file storage formats other than `File`
may require additional preprocessing before validation.
### FileList validator
Alternatively, you can implement the `AsyncFileListValidator` interface for your validator.
This validator runs **right after the user selects files,
but before the form state is updated**.
:::caution
If you use this validator, you must also implement equivalent validation on the server.
:::
A practical example is the `createFileSizeValidator` helper
from the `@sjsf/form/validators/file-size` module, which checks maximum file size.
Once this module is included, you can set the `maxFileSizeBytes` UI option on file fields.
# Form value type inference
import { Code } from '@astrojs/starlight/components'
import Npm from "@/components/npm.astro";
import code from './form-value-type-inference?raw'
import typebox from './form-value-type-inference-typebox?raw'
## json-schema-to-ts
You can use [json-schema-to-ts](https://github.com/ThomasAribart/json-schema-to-ts/tree/main)
to infer types from JSON schema.
### Installation
### Usage
## TypeBox
[TypeBox](https://github.com/sinclairzx81/typebox) offers a unified type that can be statically checked by TypeScript and
runtime asserted using standard Json Schema validation.
### Installation
### Usage
# Labels and icons
import { LinkCard, LinkButton, Code, Card, Icon } from '@astrojs/starlight/components';
import { withBase } from '@/lib/base-path';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Icons from './icons.svelte';
import iconsCode from './icons.svelte?raw';
import IconsShowCase from './_icons.astro';
## Translation
A required component of the form is `translate`, which is used in multiple places, including buttons and labels.
Translations are imported from `@sjsf/form/translations/{locale}`, currently supported locales:
- `en`
- `ru`
## Icons
Text is good, but you can do better by adding icons!
Hello from
There are also ready-made sets of icons for the control buttons.
# Manual mode
import { Code } from '@astrojs/starlight/components';
import FormCard from '@/components/form-card.astro';
import Form from './manual-mode.svelte';
import formCode from './manual-mode.svelte?raw';
Manual mode gives you complete control over form layout and field placement.
## Caveats
### Field names
To make the field names inference work - it is necessary to define
the type of form value, for example, like this:
```ts
const form = createForm(...)
```
### Nested structures
You must initialize nested structures (objects and arrays).
In the following example, if you want to display the `foo.bar` field,
you should initialize the `foo` object through the `initialValue` option.
```ts
const schema = {
type: "object",
properties: {
foo: {
type: "object",
properties: {
bar: {
type: "string",
},
},
},
},
} as const satisfies Schema;
const initialValue = {
foo: {}
}
```
### Required attribute
Calculating the value of the `required` property from the JSON schema can be tricky,
if it is calculated incorrectly - specify it manually.
```svelte
```
### Form context
Unlike other form components (`Form`, `SubmitButton`, etc.),
`Field` does not require setting the form context.
# Merger Setup
import { LinkCard } from '@astrojs/starlight/components';
import { withBase } from '@/lib/base-path';
import Npm from "@/components/npm.astro";
## Overview
Starting with version 3, a `merger` option is required when creating a form.
The merger is responsible for combining and processing JSON Schemas used within the form.
## Basic Configuration
The following configuration will work for most projects:
```ts
import { createFormMerger } from "@sjsf/form/mergers/modern";
const form = createForm({
merger: createFormMerger,
// ... other form options
});
```
For projects with specific requirements, you may need to customize the `createFormMerger` options to achieve the desired behavior.
## Advanced Configurations
### $ref Properties Override
Sometimes you need to override properties of a referenced schema. For example, when you have a schema like this:
```json
{
"title": "bar",
"$ref": "#/definitions/foo"
}
```
**Option 1: Override the `mergeSchemas` method**
```ts
import { createFormMerger } from "@sjsf/form/mergers/modern";
import { mergeSchemas } from "@sjsf/form/mergers/legacy";
export const merger = (options: MergerFactoryOptions) => {
return {
...createFormMerger(options),
mergeSchemas,
}
}
```
**Option 2: Provide a custom merger for specific properties**
```ts
import { createMerger as createJsonSchemaMerger } from "@sjsf/lib/json-schema";
import { createFormMerger } from "@sjsf/form/mergers/modern";
export const merger = (options: MergerFactoryOptions) => createFormMerger({
...options,
jsonSchemaMerger: createJsonSchemaMerger({
mergers: {
title: (_, r) => r, // Always use the rightmost non-undefined value
}
})
})
```
:::caution[Property Override Behavior]
When using the custom property merger approach, the `title` property will always receive the last non-`undefined` value in the merge chain.
:::
### Handling `allOf` Keyword
The modern merger (recommended since v3) can be extended to handle specific scenarios involving the `allOf` keyword.
**Deduplication for Complex Schemas**
When merging schemas containing `oneOf`, `anyOf`, or `not` within `allOf` keywords, you may encounter duplicates in the merged result. The same issue can occur with `enum` keywords containing non-primitive values.
To resolve this, configure JSON schema deduplication and intersection functions:
```ts
import { createDeduplicator, createIntersector } from "@sjsf/form/lib/array";
import { createComparator, createMerger as createJsonSchemaMerger } from "@sjsf/lib/json-schema";
import { createFormMerger } from "@sjsf/form/mergers/modern";
const { compareSchemaValues, compareSchemaDefinitions } = createComparator();
export const merger = (options: MergerFactoryOptions) => createFormMerger({
...options,
jsonSchemaMerger: createJsonSchemaMerger({
intersectJson: createIntersector(compareSchemaValues),
deduplicateJsonSchemaDef: createDeduplicator(compareSchemaDefinitions),
})
})
```
**What this configuration does:**
- `intersectJson`: Handles intersection of JSON values
- `deduplicateJsonSchemaDef`: Removes duplicate schema definitions
### Form State Behavior
To customize how default values are handled in your forms, you can configure the default form state behavior:
## Legacy Merger
If you need to preserve v2 behavior during migration or have compatibility requirements,
you can use the legacy merger. However, this approach is not recommended for new projects.
### Setup
The legacy merger requires additional dependencies:
```ts
import { createFormMerger } from "@sjsf/form/mergers/legacy";
const form = createForm({
merger: createFormMerger,
// ... other form options
});
```
# Multiple forms
import { Code } from '@astrojs/starlight/components';
import { DEFAULT_ID_PREFIX } from '@sjsf/form'
import FormCard from '@/components/form-card.astro';
import Form from './multiple-forms.svelte';
import formCode from './multiple-forms.svelte?raw';
To use multiple forms on the same page, you must specify a custom `idPrefix`
to avoid id collision in the DOM.
The default value of `idPrefix` is {DEFAULT_ID_PREFIX}.
# Prevent page reload
import { Code } from '@astrojs/starlight/components';
import FormCard from '@/components/form-card.astro';
import Form from './prevent-page-reload.svelte'
import formCode from './prevent-page-reload.svelte?raw'
You can prevent data loss due to accidentally closing or reloading a tab by using
the `preventPageReload` function.
## Example
Try to change the value of the form and reload the page.
# Programmatic control
import { Code, LinkCard } from '@astrojs/starlight/components';
import { withBase } from '@/lib/base-path';
import FormCard from '@/components/form-card.astro'
import Form from './programmatic-control.svelte';
import formCode from './programmatic-control.svelte?raw';
You can bind to the from element to control it programmatically:
Use the form methods to control and access the form state:
# Quickstart
import { Code, Card, LinkCard } from '@astrojs/starlight/components';
import pkg from '#/form/package.json'
import { withBase } from '@/lib/base-path';
import { createThemeInstall } from '@/shared'
import FormCard from '@/components/form-card.astro'
import Npm from "@/components/npm.astro";
import Deps from '@/components/peer-dependencies.astro';
import SimpleSetupForm from "./simple-setup.svelte";
import simpleSetupCode from './simple-setup.svelte?raw';
import UiSchemaForm from './ui-schema.svelte';
import uiSchemaCode from './ui-schema.svelte?raw';
import FormStateForm from './form-state.svelte';
import formStateCode from './form-state.svelte?raw';
import ControlledForm from './controlled-form.svelte';
import controlledFormCode from './controlled-form.svelte?raw';
## Installation
## Usage
Let's start with the simplest setup:
In the example above, we create a form based on json schema and
use HTML5 validation to validate the form.
Although this is an extremely simple example, it turned out to be quite verbose, and here's why:
- **Explicit Configuration**: The library favors explicit configuration over "magic" defaults.
- **Tree-Shakeable Architecture**: Each feature is located in its own [submodule](https://nodejs.org/api/packages.html#subpath-exports)
so you can import only the functionality you need, keeping your bundle lean and optimized.
- **Highly Customizable**: We provide extensive customization options so that you can tailor every aspect of the library to your needs.
## UI Schema
With the `uiSchema` parameter you can customize the appearance of the form.
:::note
We'll talk about `* as defaults` in the [Reusable defaults](/guides/reusable-defaults/) guide.
:::
## Form state
Use a factory to create a form to access its state.
:::tip
Use getters to pass reactive parameters.
:::
### "Controlled" form
You can also specify the `value` option instead of the `initialValue`
to access the internal state of the form.
:::note
I believe that such use of the library should be limited,
so here is a list of situations in which this approach can be used
(if you doubt it, welcome to the [discussions](https://github.com/x0k/svelte-jsonschema-form/discussions)):
- Simultaneous editing of data from multiple locations (e.g., form + sidebar)
:::
:::caution
Before using, please read the section - [Direct modification of form state](/form/state/#direct-modification-of-form-state)
:::
# Reusable defaults
import { Code } from '@astrojs/starlight/components';
import defaultsCode from '@/lib/form/defaults.ts?raw';
Even with a simple setup, resulting code is very verbose.
Therefore, it is convenient to create a file that re-exports the selected
resolver, theme, validator, merger and translation.
And use it like this:
:::caution
At the moment, Svelte has an [issue](https://github.com/sveltejs/svelte/issues/17220)
with overwriting module properties used as component properties
(``) during SSR.
As a workaround, you can use a shallow clone of `defaults` object or
define `export const defaults = { ... }` in a `defaults` file.
:::
```typescript
import * as defaults from '$lib/form/defaults'
const form = createForm({ ...defaults, })
```
# UI customization
import { Code, LinkCard } from '@astrojs/starlight/components'
import { withBase } from '@/lib/base-path'
import FormCard from '@/components/form-card.astro'
import CustomizableText from './customizable-text.svelte'
import customizableTextCode from './customizable-text.svelte?raw'
import ActionExample from './action-example.svelte'
import actionExampleCode from './action-example.svelte?raw'
import ExtraUiOptions from './extra-ui-options.svelte'
import extraUiOptionsCode from './extra-ui-options.svelte?raw'
## Widgets customization
In addition to the [predefined UI options](/form/ui-schema/#ui-options),
each widget and theme component extends the `UiOptions` interface
with at least one property that allows customization.
For example, the `textWidget` from `@sjsf/basic-theme`
adds the property `text?: HTMLInputAttributes`.
By specifying it in the UI options,
you can control the widget's attributes - even reactively.
Check your theme page for an extended list of UI options:
## Actions
By specifying `action?: FieldAction` in the UI options,
your snippet will be rendered on the right side of the field header.
You can also use the prebuilt actions available in the `@sjsf/form/fields/actions/*` module:
- `clear-edit.svelte` toggles between `undefined` and default value
- `clear.svelte` sets value to `undefined`
- `display-primitive-value.svelte` displays current value
## UI options registry
You can use UI options registry to ensure serializability of the UI schema.
To do this, you need to extend the `UiOptionsRegistry` interface and replace
the non-serializable value with `registry:${key}` in `UiOptions`.
Example:
:::note
After adding at least one property to the `UiOptionsRegistry`,
the `uiOptionsRegistry` parameter becomes required.
:::
```typescript
import { createForm } from "@sjsf/form"
import type { ItemTitle } from "@sjsf/form/fields/array/model";
declare module "@sjsf/form" {
interface UiOptionsRegistry {
myItemTitle: ItemTitle
}
}
const form = createForm({
...,
uiSchema: {
"ui:options": {
itemTitle: "registry:myItemTitle"
}
},
uiOptionsRegistry: {
myItemTitle: (title, index) => `${title} [${index}]`
}
})
```
## Extra UI options
This property allows you to specify UI option for all components or compute it dynamically.
:::caution
Has no effect on the `title` UI option
:::
# Validation
import { Code, Card, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components';
import { DEFAULT_FIELDS_VALIDATION_DEBOUNCE_MS } from '@sjsf/form';
import { withBase } from '@/lib/base-path'
import FormCard from '@/components/form-card.astro'
import LiveValidation from './live-validation.svelte'
import liveValidationCode from './live-validation.svelte?raw'
import FieldsValidation from './fields-validation.svelte'
import fieldsValidationCode from './fields-validation.svelte?raw'
import AsyncValidation from './async-validation.svelte'
import asyncValidationCode from './async-validation.svelte?raw'
import FocusOnFirsError from './focus-on-first-error.svelte'
import focusOnFirstErrorCode from './focus-on-first-error.svelte?raw'
import ErrorsList from './errors-list.svelte'
import errorsListCode from './errors-list.svelte?raw'
By default, form data when submitted will be validated using HTML5 validation and the provided validator.
:::tip
To disable HTML5 validation pass `novalidate` attribute to the form element.
:::
## Live validation
You can easily implement live validation by utilizing Svelte 5 reactivity
While it is possible, this approach has low efficiency because
it is usually not meaningful to validate the entire form when only one field is changed.
## Fields validation
Instead of performing a full form validation every time a field is changed,
we propose to validate only the field being changed and
full validation of the form on submission.
:::note
By default, fields validation is performed with a debounce of {DEFAULT_FIELDS_VALIDATION_DEBOUNCE_MS}ms.
This behavior can be changed using the `fieldsValidationDebounceMs` option.
:::
:::caution
Due to the use of debouncing, field validation is executed asynchronously.
This may lead to attempts to access component state after the component has been unmounted.
To prevent this issue, use the following code (not required for `SimpleForm`):
```ts
onDestroy(() => {
form.submission.abort();
form.fieldsValidation.abort();
});
```
:::
The form in the example above, will validate fields on `input` and `change` events.
## Async validation
The form supports asynchronous validation. By default:
- a new form validation process can only be started after the previous one is completed;
- a new fields validation process aborts the previous one;
- fields validation abort errors are not displayed.
:::note
You can change this behavior by passing the `submissionCombinator` and `fieldsValidationCombinator` options.
:::
Please see your validator page for more information.
## Focus on first error
You can achieve focus on the first error by using the `createFocusOnFirstError` function.
1. `focusOnFirstError` will try to find a focusable element and focus it.
2. If it's not found, it will try to find an errors list and scroll it into view.
3. If it's not found, it will return `false`, so you can extend the behavior.
## Errors list
If necessary, you can create a list of errors
## State transformation
In some cases it may be necessary to transform the form state before it is passed to the validator.
You can do this by extending the validator.
### Omit extra data
One of the transformation options you can apply is deleting unnecessary data.
For this you can use `omitExtraData` function.
:::note
The `omitExtraData` function does not perform data validation!
:::
```ts
import { createForm, type FormValueValidator } from "@sjsf/form";
import { omitExtraData } from "@sjsf/form/omit-extra-data";
import * as defaults from '@/lib/form/defaults'
import { schema, type FormValue } from "./schema";
const form = createForm({
...defaults,
validator: (options) => {
const v = defaults.validator(options)
return {
...v,
validateFormValue(rootSchema, formValue) {
const cleanData = omitExtraData(
v,
options.merger(),
options.schema,
formValue
);
return v.validateFormValue(rootSchema, cleanData);
},
} satisfies FormValueValidator;
},
schema,
onSubmit(value) {
console.log("transformed", value);
},
onSubmitError({ value, errors }) {
console.log("transformed", value);
console.log("errors", errors);
}
})
// Obtaining a value with the applied transformation
const { value } = validate(form)
```
You can also use the `withOmitExtraData(defaults.validator)` function from the
`@sjsf/form/validators/omit-extra-data` module to achieve the same effect.
# Generic backend
import { Card, Code } from '@astrojs/starlight/components'
import FormCard from '@/components/form-card.astro';
import Form from './_generic-backend.svelte';
import formCode from './_generic-backend.svelte?raw';
You can improve the experience of sending data to the server by using the `createTask` function.
# Form actions
import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
import { DEFAULT_ID_SEPARATOR, DEFAULT_ID_PSEUDO_SEPARATOR } from '@sjsf/form/id-builders/legacy';
import pkg from '#/sveltekit/package.json'
import Npm from '@/components/npm.astro';
import Deps from '@/components/peer-dependencies.astro';
import modelCode from '%/sveltekit-starter/src/lib/post.ts?raw'
import clientCode from '%/sveltekit-starter/src/routes/form-actions/+page.svelte?raw';
import serverCode from '%/sveltekit-starter/src/routes/form-actions/+page.server?raw'
import clientFlexCode from '%/sveltekit-starter/src/routes/form-actions-flex/+page.svelte?raw';
import serverFlexCode from '%/sveltekit-starter/src/routes/form-actions-flex/+page.server?raw';
Since this is a `Svelte` library, you may want to use it with the `SvelteKit`.
With this package you will be able to perform server-side validation of the form data
even if the user has `JavaScript` disabled.
## Installation
## Example
See details in the sections below
## Server
If you want to populate the form from the database,
you can return `InitialFormData` object inside the `load` function.
```typescript
import type { InitialFormData } from "@sjsf/sveltekit";
export const load = async () => {
return {
// Should match action name
form: {
schema,
initialValue: { title: "New post", content: "" },
} satisfies InitialFormData,
};
};
```
You can define an [action](https://svelte.dev/docs/kit/form-actions)
using the `createAction` function.
In this case, the callback provided as the second argument
will only be called if the form data passes validation successfully.
```ts
export const actions = {
default: createAction(
{
/* options */
},
(data, event, meta) => {
/* logic */
}
),
} satisfies Actions;
```
If any errors occur during processing,
you can return an array of `ValidationError` objects.
```ts
if (title.length > 100) {
return [{ path: ["title"], message: "Title is too long" }];
}
```
### Advanced API
By using the `createFormHandler` API, you can gain more control over the action handler, for example to:
- run checks before processing the form data
- work with dynamic schemas (call `createFormHandler` inside the action handler)
:::note
You must return the `form` (the key corresponding to the form name)
in every branch of the handler's code.
`redirect` and `error` helpers are also allowed.
:::
## Client
On the client side you can use `SvelteKitForm` component ([see example](#example)).
Or use more flexible solution:
:::note
For correct operation, you must use ID Builder from the `@sjsf/sveltekit` module.
:::
The form data type will be inferred from the `ActionData` and `PageData` type.
If the value of the form cannot be inferred from `action` and
you did not use `load` function, then the third generic parameter of
the `createMeta` function will be used as the form data type
(default type is `SchemaValue`).
According to Svelte documentation your form should always use `POST` requests.
### Progressive enhancement
By default, the form will work even with JavaScript disabled, but you should consider the following limitations of this mode of operation:
- `action` should be created with `sendData: true` to persist form data between page updates
- Form fields for `oneOf`, `anyOf`, `dependencies`, `additionalProperties` and `additionalItems`
will not expand/switch their state.
- Some widgets (like multiselect, depends on the theme) may will not work, because they require `JavaScript`.
## Comparison with Superforms
Unlike [Superforms](https://github.com/ciscoheat/sveltekit-superforms),
SJSF focuses on generating forms from JSON Schemas and managing their state,
while the SvelteKit integration is just an additional layer.
However, feel free to [open feature requests](https://github.com/x0k/svelte-jsonschema-form/issues/new/choose)
if something important is still missing.
### Missing features
- [Status messages](https://superforms.rocks/concepts/messages)
- [Snapshots](https://superforms.rocks/concepts/snapshots)
### Tainted fields
SJSF does not track the difference between the current and initial form state.
The `form.isChanged` property becomes `true` after any interaction with a form field
until the form is either successfully submitted or reset.
You can use the following code to check whether any changes have been made.
```ts
import { isSchemaValueDeepEqual } from '@sjsf/form/core';
import { getValueSnapshot } from '@sjsf/form';
import { preventPageReload } from '@sjsf/form/prevent-page-reload.svelte';
preventPageReload({
get isChanged() {
return form.isChanged && !isSchemaValueDeepEqual(initialValue, getValueSnapshot(form))
}
})
```
### Available events
- `onSuccess` — triggered when a request completes successfully,
receiving an `ActionResult>` as the result
- `onFailure` — triggered when a request fails,
receiving a `FailedTask` as the argument
- `onSubmitError` — triggered when client-side validation errors are present
- `onSubmissionFailure` — triggered when an error occurs during validation
### Loading timers
Use `request.isProcessed` and `request.isDelayed` to access loading timers.
```ts
const { form, request } = setupSvelteKitForm(meta, {
delayedMs: 500,
timeoutMs: 8000,
...,
})
```
### Multiple forms
Use `idPrefix` to create multiple forms.
**Client**
```ts
const { form, request } = setupSvelteKitForm(meta, {
idPrefix: "foo",
...,
})
```
**Server**
```ts
export const actions = {
default: createAction(
{
/* options */
},
(data: Model, _event, { idPrefix }) => {
console.log(idPrefix, data);
}
)
} satisfies Actions
```
### Submit behavior
Use the `combinator` option to control the behavior of multiple form submissions.
- `forgetPrevious`
- `abortPrevious`
- `waitPrevious`
- `throttle`
```ts
import { waitPrevious } from '@sjsf/form/lib/task.svelte'
const { form, request } = setupSvelteKitForm(meta, {
combinator: waitPrevious
...,
})
```
# Remote functions
import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
import pkg from '#/sveltekit/package.json'
import Npm from '@/components/npm.astro';
import Deps from '@/components/peer-dependencies.astro';
import { mergePackageConfigs } from '@/web-ide/layer';
import modelCode from '%/sveltekit-starter/src/lib/post.ts?raw'
import clientCode from '%/sveltekit-starter/src/routes/remote-functions/+page.svelte?raw';
import clientEnhanceCode from '%/sveltekit-starter/src/routes/remote-functions-enhance/+page.svelte?raw';
import serverCode from '%/sveltekit-starter/src/routes/remote-functions/data.remote?raw'
:::caution
Remote functions integration is experimental
:::
## Installation
## Example
## Known issues
- `experimental.async` does not apply to dependency code -
[issue](https://github.com/sveltejs/kit/issues/14788).
Add this to your `vite.config.js`
```js
optimizeDeps: {
exclude: ['@sjsf/form', '@sjsf/sveltekit/rf/client'],
}
```
- The `form.value` type on the client side is `unknown` because the `RemoteForm`
type does not include information about the validator's output type.
For now, you need to specify the type manually: `createForm(...)`.
# Contribute
## Feedback
You can leave feedback on any aspect of the library (features, documentation, etc.)
in [GitHub discussions](https://github.com/x0k/svelte-jsonschema-form/discussions).
## Development
Personally, I use [nix](https://nixos.wiki/wiki/Nix_package_manager)
[dev](https://nix.dev/manual/nix/2.26/command-ref/new-cli/nix3-develop)
environment and [mk](https://github.com/x0k/mk), although `pnpm@10` will suffice
to perform any action.
### Dev server
To start the playground, you need to execute the following commands
```shell
pnpm i
pnpm build
cd apps/playground2
pnpm run dev
```
If you are working on a theme then you can run it dev server, example:
```shell
cd packages/basic
pnpm run dev
```
### Tests
You can run all tests from the root directory using the following command:
```shell
pnpm run test
```
Unfortunately, the library does not currently have good test coverage,
so contributions with tests are extremely welcome.
### Changes
We use [changesets](https://github.com/changesets/changesets/tree/main), so
don't forget to [add](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md)
a `changeset` file using the `pnpm changeset` command.
# LLMs
The `llms.txt` files were generated using [starlight-llms-txt](https://github.com/delucis/starlight-llms-txt).
Their usefulness is somewhat limited because the code from some snippets is not inlined (contributions are welcome).
- [llms.txt](/llms.txt)
- [llms-small.txt](/llms-small.txt)
- [llms-full.txt](/llms-full.txt)
You can also use the following third-party services for working with LLMs:
- [Context7](https://context7.com/x0k/svelte-jsonschema-form)
- [DeepWiki](https://deepwiki.com/x0k/svelte-jsonschema-form)
# Migration from v2
import Npm from '@/components/npm.astro';
Before migrating, make sure you are using the latest version of SJSF and
that you are not using any deprecated APIs.
## TL;DR
Update your `defaults.ts` file:
```diff
- import { createFormValidator } from "@sjsf/ajv8-validator";
- export const validator = createFormValidator();
+ export { createFormValidator as validator } from "@sjsf/ajv8-validator";
+ export { createFormIdBuilder as idBuilder } from "@sjsf/form/id-builders/legacy";
+ export { createFormMerger as merger } from "@sjsf/form/mergers/legacy";
```
And install `json-schema-merge-allof`:
But I recommend trying to switch to a modern merger to get the benefits
of bundle size and performance - [modern merger setup](/guides/merger/).
This change should be sufficient for migrating simple projects.
However, I recommend reviewing the full list of changes,
as you might find opportunities to improve or simplify your current solution.
## Form
### Form options overhaul
- Introduced a new `idBuilder` option, replacing the former `idSeparator` and `idPseudoSeparator` properties
- The `merger` option is now required
- The `idBuilder`, `merger`, and `validator` options can now also be provided as factory functions
This change addresses several issues:
- A single validator instance was shared across all forms
- Form parameters had to be passed manually to validator and merger (`uiSchema`, `idPrefix`, etc.)
- The default merger relied on a [heavy library](https://github.com/x0k/svelte-jsonschema-form/issues/147)
as a dependency
- Uncontrolled ID generation system
Now:
- Each form creates its own validator and merger instances, passing the necessary parameters automatically
- The merger is specified explicitly, making it easy to replace if needed
- A new merger is available with significant improvements in both performance and bundle size
(`@sjsf/form/mergers/modern`)
- A controlled and configurable ID generation system
### All form options now can be reactive
All form options now support reactivity. For example:
```ts
import { createForm } from '@sjsf/form';
import { getInitialData } from './data.remote';
const initialData = $derived(await getInitialData())
const form = createForm({
get initialValue() {
return initialData;
},
get initialErrors() {
initialData;
return undefined;
}
/// ...
})
```
When `initialData` changes, the form state will be updated!
### Changed default values population behavior
Previously, objects and tuples were always initialized with a value,
even when they were not required. For example:
```json
{
"type": "object",
"properties": {
"foo": {
"type": "object",
"properties": {
"bar": {
"type": "string"
}
},
"required": ["bar"]
}
}
}
```
The `foo` object used to be initialized as an empty object (unless another initial value was provided).
This forced users to fill in the `bar` field, even though the `foo` object itself was optional.
Now, when creating a form, the form state is initialized with an empty object (for this case),
and the form can be submitted without filling in the `bar` field.
### The `validator` form option now requires `FormValidator`
This means that, in addition to the `isValid` method, an implementation of the form value validation method is now required.
You can use the `noop` validator from the `@sjsf/form/validators/noop` module to skip validation.
### The `form.context` property has been removed
Previously, the public and internal APIs were split between two objects:
`form` and `form.context`.
In v3, `form.context` and `FormInternalContext` no longer exist.
All internal fields and methods have been moved directly into `form`,
but they must be accessed via [internal symbols](/form/state/#internals).
:::caution
If you were using `ctx.value` in your custom components, replace it with `ctx[FORM_VALUE]`.
:::
### The `form.value` property has been removed
The functionality of this property has now been split across several methods:
- Use the `setValue` method to set a new form value.
- Use the `getValueSnapshot` method to retrieve a snapshot of the internal form state.
- Use the `validate/validateAsync` method to retrieve a typed value.
### The `form.errors` property has been removed
Reason — to hide the internal error storage structure from the user for future optimization.
You can access form errors and even more by using the following functions:
`hasErrors`, `getErrors`, `updateErrors`, `getFieldErrors`, `updateFieldErrors`
### The `getSnapshot` form option has been removed
To apply transformations before validation, you now need to augment the validator -
see [State transformation](/guides/validation/#state-transformation).
### `onsubmit` and `onreset` attributes
The `onsubmit` and `onreset` attributes applied to `Form`, `BasicForm`,
or via `uiSchema` override the default handlers.
If you need additional handlers, use `attachments`
or call `form.submit(e)` in your handler.
### Custom components
#### `ArrayContext` and `ObjectContext` objects update
Context constructors now accept an options object instead of a list of arguments:
```ts
const objCtx = createObjectContext({
ctx,
config: () => config,
value: () => value,
translate,
});
```
These interfaces now only include methods, making them easier to extend:
```ts
const myObjCtx = {
...objCtx,
canExpand() => someCondition && objCtx.canExpand(),
}
setObjectContext(myObjCtx)
```
#### Forced support for nullable fields
The `FieldCommonProps` interface has been updated:
```diff
export interface FieldCommonProps {
...
- value: V | undefined;
+ value: V | null | undefined;
}
```
This change introduces explicit support for nullable fields ([example](https://x0k.dev/svelte-jsonschema-form/playground3/#N4IgzgxgFgpgtgQxALlAFwJZoDYxSAMQHsAnOAAgHcspyA7AV22wQCNdyAzDGbAEzBUoGaOQQkY9JtnKsAnuT4xOCJmhAAaEGjkAHPMhBFWAKxgR1W3SSL6SmGGBShGzNrgDyp82gDqNABFlVWx1VG0sXHwAOWl3SS8zC01tPQMAbRBXbBTjJPUAXS0lFTUUbIBfLWz4gEESEgQ5fzQoINLQ5wicAxBYt3ZJesa5FJ19FEzslPERkCKQEpCw7K0seCdw8d6wNBIMOgBzEAqqrLjBxJ8W2DhdHQA1BGwGA3RI3v6WQfIr5K1tpNzsxct5kgUzjVBgBlPYHY5bD4xC4cWH7I5jNJA6ZaXbo44LJZlZCVaoomCxOCsGAkLqYHrIgYcSnU2kArHIKbSFKMKk0+bFYLEyqnLQMDDQ6DwJDhKGeMF+Gjwe5yJ4vN4gcXIZWPZ6vcrSUUgA5YDDPNX62Xkv6K1rtZYG5hkpkwYZNG724XSZ3fXBo+GO7A++Is-kkw3FDBgeJ8FAqbBgGBaKBoODYACsaowfAQmCIdDjz0TWk4RAgDDAHjoBAwJF2AFEGqQUHtXloiHAsPWAB57BABXMy+PFkDcXgCLM5vN0ACyRCUKAADFoAG7PbO55uGBAmFcADjGt16rAQYBEKRE+c2WXzeC0EjARGwK7DIAgHd0uZmDSaM4OAEk0A2AAFWwmFzXpnhyLRZl-ACgLgMAZxpQ4YB7PtPVCTZhyTEAoI8Th8DAABrDBdEwtAnC0d86F2WowAo-BnkoJoqJAHU5BtGtx2vXQwJYIDamYCi2LgFCYBE-86DQIhiDIAc0BlTVEzkuAFIQf9OGAh8YGkk4gA)).
In most cases, it’s enough to update your widget’s value binding from:
`bind:value` to `bind:value={() => value ?? undefined, (v) => (value = v)}`
#### Using `attachment` for attaching event handlers
Previously, helper functions like `inputProps`, `selectProps`, and `textareaProps`
passed event handlers as attributes, which in some cases could be overwritten by the user.
Now, instead of attributes, they use `attachment`,
where handlers are added via `addEventListener`.
If your component does not support `attachment`,
or attaches handlers to the wrong DOM node,
you should map the attributes manually:
```ts
inputAttributes(ctx, config, "text", handlers, {
// spread handlers as attributes
...handlers,
type: "text",
});
```
### `Field` component update
The `name` prop has been replaced with `path`.
### Other changes
- `fields/extra-fields/*` submodule now deprecated, migrate to `fields/extra/*`
- The `makeEventHandlers` function signature has been changed
- The `isSubmitted` and `isChanged` properties are now `readonly`
- `isSubmitted` is reset after successful processing (validation + `onSubmit` handler)
- `isChanged` becomes `true` after any interaction until
the form is successfully submitted or reset
- The `idSeparator` and `idPseudoSeparator` form parameters moved to `idBuilder`
- Removed redundant types:
- `IdPrefixOption`
- `IdSeparatorOption`
- `IdPseudoSeparator`
- `IdOptions`
- `PathToIdOptions`
- `focus-on-first-error` module changes:
- Updated signatures of `getErrorsList` and `getFocusAction`
- Removed the `GetFocusActionOptions` type
- Replace `id` property with `path` in `Config` type
- Replace `idFromPath` with `getId` function
- Replace `createChildId` with `getChildPath` function
- Replace `getErrors` with `getFieldErrors` function
## Validators
### Zod and Valibot
The `setupFormValidator` and `setupAsyncFormValidator` functions have been renamed
to `adapt` and `adaptAsync`.
They can now be used directly when calling `createForm`:
```ts
const form = createForm({
...defaults,
...adapt(schema),
});
```
### Ajv and @exodus/schemasafe
In the `precompile` submodules, the `createFormValidator` functions
have been replaced with `createFormValidatorFactory`.
For convenient use with `createForm`:
```ts
const form = createForm({
...defaults,
schema,
validator: createFormValidatorFactory({ validateFunctions }),
});
```
### Standard schema
The signatures of `createFormValueValidator` and `createAsyncFormValueValidator`
from the `@sjsf/form/validators/standard-schema` module have changed.
Now the Standard Schema is passed as the first argument without any options.
## Themes
### Basic
Now you need to import the CSS:
```ts
import "@sjsf/basic-theme/css/basic.css";
```
### daisyUI v5
File renames:
- `@sjsf/daisyui5-theme/extra-widgets/pikaday-date-picker.svelte` → `@sjsf/daisyui5-theme/extra-widgets/date-picker.svelte`
- `@sjsf/daisyui5-theme/extra-widgets/pikaday-date-picker-include` → `@sjsf/daisyui5-theme/extra-widgets/date-picker-include`
Widget changes:
- `@sjsf/daisyui5-theme/extra-widgets/cally-date-picker-include`
now includes `daisyui5CallyDatePickerWidget` instead of `datePickerWidget`
- `@sjsf/daisyui5-theme/extra-widgets/filter-radio-buttons-include`
now includes `daisyui5FilterRadioButtonsWidget` instead of `radioButtonsWidget`
### Flowbite
The `radioButtons` widget now uses [RadioButton](https://flowbite-svelte.com/docs/forms/radio#radiobutton) instead of [Button Toggle](https://flowbite-svelte.com/docs/extend/button-toggle) component.
To preserve the previous behavior you can use the following import,
witch includes `flowbite3ToggleRadioButtonsWidget` widget:
```ts
import "@sjsf/flowbite3-theme/extra-widgets/toggle-radio-buttons-include";
```
### Skeleton v3
Widget changes:
- `@sjsf/skeleton3-theme/extra-widgets/file-upload-include`
now includes `skeleton3FileUploadWidget` instead of `fileWidget`
- `@sjsf/skeleton3-theme/extra-widgets/slider-include`
now includes `skeleton3SliderWidget` instead of `rangeWidget`
### shadcn-svelte
The theme now requires the `Field*` and `ButtonGroup` components to function properly.
## SvelteKit
Now, for the integration to work correctly,
you must use ID Builder from the `@sjsf/sveltekit` module.
```ts
export { createFormIdBuilder as idBuilder } from '@sjsf/sveltekit'
```
The `makeFormDataParser` and `validateForm` functions
have been replaced with `createFormHandler`.
See the [Form actions](/integrations/sveltekit/form-actions/#advanced-api) page for more information.
The `initForm` function has been removed — specify initial values directly instead.
```ts
import type { InitialFormData } from "@sjsf/sveltekit";
export const load = async () => {
return {
form: {
schema,
initialValue: { ... },
} satisfies InitialFormData,
};
};
```
## Dependency versions have been updated
- `@sveltejs/kit@^2.48.3` (`@sjsf/sveltekit`)
- `daisyui@^5.4.0` (`@sjsf/daisyui5-theme`)
- `flowbite-svelte-icons@^3.0.0` (`@sjsf/flowbite-icons`)
- `flowbite-svelte@^1.22.0` (`@sjsf/flowbite3-theme`)
- `bits-ui@^2.14.0` (`@sjsf/shadcn4-theme`)
- New peer dependency `@jis3r/icons@^1.1.0` (`@sjsf/moving-icons`)
- `zod@^4.1.0` (`@sjsf/zod4-validator`)
## Deprecated packages were removed
- `@sjsf/zod-validator` (v3)
- `@sjsf/skeleton-theme` (tw3)
## Etc.
Feel free to get in touch about migrating to v3 on [GitHub Discussions](https://github.com/x0k/svelte-jsonschema-form/discussions).
# Basic
import { Code, Card, TabItem, Tabs } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf/basic-theme/specs';
import styles from '@sjsf/basic-theme/css/basic.css?raw';
import picoCssStyles from "@picocss/pico/css/pico.css?raw";
import basicThemePicoCssStyles from "@sjsf/basic-theme/css/pico.css?raw";
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Form from './basic/form.svelte';
import uiOptionsCode from './basic/ui-options?raw';
import { createExtraImports, createSchemas, replacer, validationEvents } from "./_demo-schema";
export const schemas = createSchemas(specs)
## Installation
### Configuration
Import the CSS:
```ts
import "@sjsf/basic-theme/css/basic.css";
```
#### Pico CSS
Usage with [Pico CSS](https://picocss.com/):
```ts
import "@picocss/pico/css/pico.css";
import "@sjsf/basic-theme/css/pico.css";
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## Supported validation events
## UI options
# daisyUI v5
import { Code, Card, TabItem, Tabs, LinkButton } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf/daisyui5-theme/specs';
import styles from '@sjsf/daisyui5-theme/styles.css?inline';
import pkg from '#/daisyui5-theme/package.json'
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro'
import Form from './daisyui5/form.svelte'
import uiOptionsCode from './daisyui5/ui-options?raw';
import { createSchemas, replacer, createExtraImports, validationEvents } from "./_demo-schema";
export const schemas = createSchemas(specs)
## Installation
### Install daisyUI v5
Install daisyUI as a Tailwind plugin
### Configuration
Register the theme source path by adding the following line to the `app.css` file:.
```
@source "../node_modules/@sjsf/daisyui5-theme/dist";
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## Supported validation events
## UI options
# Flowbite
import { Code, Card, LinkButton, Tabs, TabItem } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf/flowbite3-theme/specs';
import styles from '@sjsf/flowbite3-theme/styles.css?inline';
import pkg from '#/flowbite3-theme/package.json'
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro'
import Form from './flowbite3/form.svelte';
import uiOptionsCode from './flowbite3/ui-options?raw';
import { createSchemas, replacer, createExtraImports, validationEvents } from "./_demo-schema";
export const schemas = createSchemas(specs)
> Flowbite™ is a registered trademark of Bergside Inc.
This website is not affiliated with, endorsed by, or sponsored by Bergside Inc.
All rights to the trademark are owned by Bergside Inc.
## Installation
### Install Flowbite Svelte v3
Quickstart - Flowbite Svelte
### Configuration
Register the theme source path by adding the following line to the `app.css` file:.
```css
@source "../node_modules/@sjsf/flowbite3-theme/dist";
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## Supported validation events
## UI options
# Beer CSS
import { Code, Card, TabItem, Tabs } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf-lab/beercss-theme/specs';
import styles from "beercss/dist/cdn/beer.min.css?raw";
export const beerCssSettings = `
:host {
--size: 1rem;
--font: Inter, Roboto, "Helvetica Neue", "Arial Nova", "Nimbus Sans", Noto Sans, Arial, sans-serif;
--font-icon: "Material Symbols Outlined";
--speed1: 0.1s;
--speed2: 0.2s;
--speed3: 0.3s;
--speed4: 0.4s;
--active: rgb(128 128 128 / 0.192);
--overlay: rgb(0 0 0 / 0.5);
--elevate1: 0 0.125rem 0.125rem 0 rgb(0 0 0 / 0.32);
--elevate2: 0 0.25rem 0.5rem 0 rgb(0 0 0 / 0.4);
--elevate3: 0 0.375rem 0.75rem 0 rgb(0 0 0 / 0.48);
--top: env(safe-area-inset-top);
--bottom: env(safe-area-inset-bottom);
--left: env(safe-area-inset-left);
--right: env(safe-area-inset-right);
}
:host, .light {
--primary: #6750a4;
--on-primary: #ffffff;
--primary-container: #e9ddff;
--on-primary-container: #22005d;
--secondary: #625b71;
--on-secondary: #ffffff;
--secondary-container: #e8def8;
--on-secondary-container: #1e192b;
--tertiary: #7e5260;
--on-tertiary: #ffffff;
--tertiary-container: #ffd9e3;
--on-tertiary-container: #31101d;
--error: #ba1a1a;
--on-error: #ffffff;
--error-container: #ffdad6;
--on-error-container: #410002;
--background: #fffbff;
--on-background: #1c1b1e;
--surface: #fdf8fd;
--on-surface: #1c1b1e;
--surface-variant: #e7e0eb;
--on-surface-variant: #49454e;
--outline: #7a757f;
--outline-variant: #cac4cf;
--shadow: #000000;
--scrim: #000000;
--inverse-surface: #313033;
--inverse-on-surface: #f4eff4;
--inverse-primary: #cfbcff;
--surface-dim: #ddd8dd;
--surface-bright: #fdf8fd;
--surface-container-lowest: #ffffff;
--surface-container-low: #f7f2f7;
--surface-container: #f2ecf1;
--surface-container-high: #ece7eb;
--surface-container-highest: #e6e1e6;
}
.dark {
--primary: #cfbcff;
--on-primary: #381e72;
--primary-container: #4f378a;
--on-primary-container: #e9ddff;
--secondary: #cbc2db;
--on-secondary: #332d41;
--secondary-container: #4a4458;
--on-secondary-container: #e8def8;
--tertiary: #efb8c8;
--on-tertiary: #4a2532;
--tertiary-container: #633b48;
--on-tertiary-container: #ffd9e3;
--error: #ffb4ab;
--on-error: #690005;
--error-container: #93000a;
--on-error-container: #ffb4ab;
--background: #1c1b1e;
--on-background: #e6e1e6;
--surface: #141316;
--on-surface: #e6e1e6;
--surface-variant: #49454e;
--on-surface-variant: #cac4cf;
--outline: #948f99;
--outline-variant: #49454e;
--shadow: #000000;
--scrim: #000000;
--inverse-surface: #e6e1e6;
--inverse-on-surface: #313033;
--inverse-primary: #6750a4;
--surface-dim: #141316;
--surface-bright: #3a383c;
--surface-container-lowest: #0f0e11;
--surface-container-low: #1c1b1e;
--surface-container: #201f22;
--surface-container-high: #2b292d;
--surface-container-highest: #363438;
}`;
import pkg from 'lab/beercss-theme/package.json'
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro';
import { createExtraImports, createSchemas, replacer, validationEvents } from "../_demo-schema";
import Form from './beercss/form.svelte';
import uiOptionsCode from './beercss/ui-options?raw';
export const schemas = createSchemas(specs)
## Installation
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## Supported validation events
{/* ## UI options
*/}
# shadcn-svelte-extras
import { Code, Card, LinkButton, Tabs, TabItem } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf-lab/shadcn-extras-theme/specs';
import styles from '@sjsf-lab/shadcn-extras-theme/styles.css?inline';
import pkg from 'lab/shadcn-extras-theme/package.json'
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro';
import Form from './shadcn-extras/wrapper.svelte';
import uiOptionsCode from './shadcn-extras/ui-options?raw';
import { createSchemas, replacer, createExtraImports, validationEvents } from "../_demo-schema";
export const schemas = createSchemas(specs)
## Installation
### Install shadcn-svelte
Installation - shadcn-svelte
### Configuration
Register the theme source path by adding the following line to the `app.css` file:.
```css
@source "../node_modules/@sjsf-lab/shadcn-extras-theme/dist";
```
## Components
Since `shadcn-svelte` and `shadcn-svelte-extras` is not a component libraries
you should provide your components via `setThemeContext`.
:::note
You should prefer to use components from your code base to avoid code duplication
:::
```typescript
import { setThemeContext } from '@sjsf/shadcn4-theme';
import * as components from '@sjsf/shadcn4-theme/new-york';
import * as extraComponents from '@sjsf-lab/shadcn-extras-theme/ui';
setThemeContext({ components: { ...components, ...extraComponents } })
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
{/* ## Supported validation events
## UI options
*/}
# SVAR
import { Code, Card, TabItem, Tabs } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf-lab/svar-theme/specs';
import pkg from 'lab/svar-theme/package.json'
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro';
import Form from './svar/form.svelte';
import uiOptionsCode from './svar/ui-options?raw';
import { createExtraImports, createSchemas, replacer, validationEvents } from "../_demo-schema";
export const schemas = createSchemas(specs)
## Installation
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## Supported validation events
{/* ## UI options
*/}
# daisyUI v4
import { Code, Card, TabItem, Tabs, LinkButton } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf/daisyui-theme/specs';
import styles from '@sjsf/daisyui-theme/styles.css?inline';
import pkg from 'legacy/daisyui-theme/package.json'
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import Picker from '@/components/picker.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro'
import Form from './daisyui/form.svelte';
import uiOptionsCode from './daisyui/ui-options?raw';
import { createSchemas, replacer, createExtraImports } from "../_demo-schema";
export const schemas = createSchemas(specs)
## Installation
### Install daisyUI
Install daisyUI as a Tailwind CSS plugin
### Setup styles
There is two ways to setup styles:
1. Use tailwindcss config
```typescript
import daisyui from 'daisyui';
import { THEME_CONTENT } from '@sjsf/daisyui-theme/preset'
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}', THEME_CONTENT],
plugins: [daisyui],
}
```
2. Inject prepared styles (not recommended)
```typescript
// Inject them as you like
import daisyStyles from "@sjsf/daisyui-theme/styles.css?inline";
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## UI options
# Flowbite tw3
import { Code, Card, LinkButton, Tabs, TabItem } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf/flowbite-theme/specs';
import styles from '@sjsf/flowbite-theme/styles.css?inline';
import pkg from 'legacy/flowbite-theme/package.json';
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import Picker from '@/components/picker.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro'
import { createSchemas, replacer, createExtraImports } from "../_demo-schema";
import Form from './flowbite/form.svelte';
import uiOptionsCode from './flowbite/ui-options?raw';
export const schemas = createSchemas(specs)
> Flowbite™ is a registered trademark of Bergside Inc.
This website is not affiliated with, endorsed by, or sponsored by Bergside Inc.
All rights to the trademark are owned by Bergside Inc.
## Installation
### Install Flowbite Svelte
Quickstart - Flowbite Svelte
### Setup styles
There is two ways to setup styles:
1. Tailwind configuration
```typescript
import flowbite from 'flowbite/plugin';
import { THEME_CONTENT, FLOWBITE_CONTENT } from '@sjsf/flowbite-theme/preset'
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}', THEME_CONTENT, FLOWBITE_CONTENT],
plugins: [flowbite],
}
```
2. Inject prepared styles (not recommended)
```typescript
// Inject them as you like
import flowbiteStyles from "@sjsf/flowbite-theme/styles.css?inline";
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## UI options
# shadcn-svelte tw3
import { Code, Card, LinkButton, Tabs, TabItem } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf/shadcn-theme/specs';
import styles from '@sjsf/shadcn-theme/styles.css?inline';
import pkg from 'legacy/shadcn-theme/package.json';
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro'
import Form from './shadcn/form.svelte';
import uiOptionsCode from './shadcn/ui-options?raw';
import { createSchemas, replacer, createExtraImports } from "../_demo-schema";
export const schemas = createSchemas(specs)
## Installation
:::note
This is `shadcn-svelte` based theme (Tailwind v3).
:::
### Install shadcn-svelte
Installation - shadcn-svelte
### Setup styles
There is two ways to setup styles:
1. Use tailwindcss config
```typescript
import { THEME_CONTENT } from '@sjsf/shadcn-theme/preset'
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}', THEME_CONTENT],
// other tailwind config
...
}
```
2. Inject prepared styles (not recommended)
```typescript
// Inject them as you like
import shadcnStyles from '@sjsf/shadcn-theme/styles.css?inline';
```
## Components
Since `shadcn-svelte` is not a component library you should provide your components via `setThemeContext`.
:::note
You should prefer to use components from your code base to avoid code duplication
:::
```typescript
import { setThemeContext } from '@sjsf/shadcn-theme';
import * as components from '@sjsf/shadcn-theme/default';
// or import * as components from '@sjsf/shadcn-theme/new-york';
setThemeContext({ components })
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
:::caution
The following widgets `combobox` and `select` replace each other -
use one or import the replacement widget directly.
:::
## Widgets demo
## UI options
# Skeleton v3
import { Code, Card, TabItem, Tabs, LinkButton } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf/skeleton3-theme/specs';
import styles from "@sjsf/skeleton3-theme/styles.css?inline";
import pkg from 'legacy/skeleton3-theme/package.json'
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro'
import Form from './skeleton3/form.svelte';
import uiOptionsCode from './skeleton3/ui-options?raw';
import { createSchemas, replacer, createExtraImports, validationEvents } from "../_demo-schema";
export const schemas = createSchemas(specs)
## Installation
### Install Skeleton v3
Install and configure Skeleton for SvelteKit
### Configuration
Register the theme source path by adding the following line to the `app.css` file:.
```css
@source "../node_modules/@sjsf/skeleton3-theme/dist";
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## Supported validation events
## UI options
# shadcn-svelte
import { Code, Card, LinkButton, Tabs, TabItem } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf/shadcn4-theme/specs';
import styles from '@sjsf/shadcn4-theme/styles.css?inline';
import shadcnContextCode from 'apps/playground2/src/shadcn-context.ts?raw'
import pkg from '#/shadcn4-theme/package.json'
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro'
import Form from './shadcn4/wrapper.svelte';
import uiOptionsCode from './shadcn4/ui-options?raw';
import { createSchemas, replacer, createExtraImports, validationEvents } from "./_demo-schema";
export const schemas = createSchemas(specs)
## Installation
### Install shadcn-svelte
Installation - shadcn-svelte
### Configuration
Register the theme source path by adding the following line to the `app.css` file:.
```css
@source "../node_modules/@sjsf/shadcn4-theme/dist";
```
## Components
Since `shadcn-svelte` is not a component library you should provide your components via `setThemeContext`.
:::note
`components: ThemeComponents` is an augmentable interface; the set of components
is determined automatically when extra widgets are included/imported.
Below is an example of a function that sets the set of components required
when including all extra widgets.
:::
:::caution
Using this approach will result in duplication of shadcn components in the final bundle.
:::
```typescript
import { setThemeContext } from '@sjsf/shadcn4-theme';
import * as components from '@sjsf/shadcn4-theme/new-york';
setThemeContext({ components })
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## Supported validation events
## UI options
# Skeleton v4
import { Code, Card, TabItem, Tabs, LinkButton } from '@astrojs/starlight/components';
import { specs, extraWidgets } from '@sjsf/skeleton4-theme/specs';
import styles from "@sjsf/skeleton4-theme/styles.css?inline";
import pkg from '#/skeleton4-theme/package.json'
import { themePkg } from '@/shared';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Deps from '@/components/peer-dependencies.astro'
import Form from './skeleton4/form.svelte';
import uiOptionsCode from './skeleton4/ui-options?raw';
import { createSchemas, replacer, createExtraImports, validationEvents } from "./_demo-schema";
export const schemas = createSchemas(specs)
## Installation
### Install Skeleton v4
Install and configure Skeleton for SvelteKit
### Configuration
Register the theme source path by adding the following line to the `app.css` file:.
```css
@source "../node_modules/@sjsf/skeleton4-theme/dist";
```
## Extra widgets
You can connect extra widgets using the following `include` imports:
:::note
For a widget to be applied, it must be explicitly specified in the
[UI schema](/form/ui-schema/#ui-components)
or used in the field as the default widget.
:::
## Widgets demo
## Supported validation events
## UI options
# Ajv
import { LinkButton, Code, Card, TabItem, Tabs } from '@astrojs/starlight/components';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Form from './ajv/form.svelte';
import formCode from './ajv/form.svelte?raw';
import sharedCode from './shared/index.ts?raw';
import PrecompiledForm from './ajv/precompile/form.svelte'
import precompiledFormCode from './ajv/precompile/form.svelte?raw'
import compileSchemaCode from './ajv/precompile/compile-schema-script?raw'
import inputSchemaCode from './ajv/precompile/input-schema.json?raw'
import precompiledSchemaCode from './ajv/precompile/patched-schema?raw'
Form validator implementation based on [Ajv@8](https://github.com/ajv-validator/ajv)
JSON schema validator.
## Installation
## Example
## Plugins
You can connect AJV plugins as follows:
```ts
import type { ValidatorFactoryOptions } from "@sjsf/form";
import { createFormValidator, addFormComponents } from "@sjsf/ajv8-validator";
import addFormats from "ajv-formats"
export const validator = (options: ValidatorFactoryOptions) =>
createFormValidator({
...options,
ajvPlugins: (ajv) => addFormComponents(addFormats(ajv))
})
```
## Async validation
This validator supports async validation.
Asynchronous validation
```typescript
import type { ValidatorFactoryOptions } from "@sjsf/form";
import { createAsyncFormValidator } from "@sjsf/ajv8-validator";
export const validator = (options: ValidatorFactoryOptions) =>
createAsyncFormValidator({
...options,
ajv: yourAjvInstance,
})
```
## Precompiled validation
It is possible to use precompiled validator to avoid issues with `unsafe-eval`
warnings from the browser caused by strict Content Security Policy settings.
### Schema precompilation
The first step in the process is to compile a schema into a set of validate functions.
```shell
node --experimental-strip-types compile-schema-script.ts
```
### Usage
# cfworker/json-schema
import { Code, Card, TabItem, Tabs } from '@astrojs/starlight/components';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Form from './cfworker/form.svelte';
import formCode from './cfworker/form.svelte?raw';
import sharedCode from './shared/index.ts?raw';
Form validator implementation based on [@cfworker/json-schema](https://github.com/cfworker/cfworker/tree/main/packages/json-schema).
## Installation
## Example
## Caveats
- `data-url` and `color` formats are ignored.
- By itself this validator does not support undefined values. But we use the
`valueToJSON` function with the following default implementation to fix it,
consider this overhead.
```typescript
const valueToJSON = (v: FormValue) =>
v === undefined || v === null
? null
: typeof v === "object"
? JSON.parse(JSON.stringify(v))
: v
```
It will also lead to incorrect error text (e.g.
`Instance type “null” is invalid. Expected “string”`) where you would like to
see the text `this field is requred`.
This can be fixed using [error transformation](/form/validator/).
## Async validation
This validator does not support async validation.
# exodus/schemasafe
import { LinkButton, Code, Card, TabItem, Tabs } from '@astrojs/starlight/components';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Form from './schemasafe/form.svelte';
import formCode from './schemasafe/form.svelte?raw';
import sharedCode from './shared/index.ts?raw';
import PrecompiledForm from './schemasafe/precompile/form.svelte'
import precompiledFormCode from './schemasafe/precompile/form.svelte?raw'
import compileSchemaCode from './schemasafe/precompile/compile-schema-script?raw'
import inputSchemaCode from './schemasafe/precompile/input-schema.json?raw'
import precompiledSchemaCode from './schemasafe/precompile/patched-schema?raw'
Form validator implementation based on [@exodus/schemasafe](https://github.com/ExodusMovement/schemasafe)
a code-generating JSON Schema validator.
## Installation
## Example
## Async validation
This validator does not support async validation.
## Precompiled validation
It is possible to use precompiled validator.
### Schema precompilation
The first step in the process is to compile a schema into a set of validate functions.
```shell
node --experimental-strip-types compile-schema-script.ts
```
### Usage
# Standard Schema
import { Code, Card, TabItem, Tabs } from '@astrojs/starlight/components';
import FormCard from '@/components/form-card.astro';
import Form from './standard-schema/form.svelte';
import formCode from './standard-schema/form.svelte?raw';
import sharedCode from './shared/index.ts?raw';
You can use any validator that implements [Standard Schema](https://standardschema.dev/)
spec and can be converted to JSON Schema for the full form validation step.
## Example
## Limitations
You can use `isValid: () => true` as long as the generated form does not contain
the following keywords `oneOf`, `anyOf` and `if,them,else`.
In such cases, you will need a real `Validator` interface implementation
(You can take it from another validator or write it yourself).
## Async validation
This validator supports async validation.
```typescript
import { createAsyncFormValueValidator } from "@sjsf/form/validators/standard-schema";
export const validator = {
...createAsyncFormValueValidator(schema),
isValid: () => true,
};
```
# Valibot
import { Code, Card, TabItem, Tabs } from '@astrojs/starlight/components';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Form from './valibot/form.svelte';
import formCode from './valibot/form.svelte?raw';
import sharedCode from './shared/index.ts?raw';
You may want to use Valibot validator because:
- You can use Valibot schema as a source of truth for the form value type (`v.InferInput`).
- This is an easy way to add custom error messages
:::caution
Your schema must be convertible to a JSON schema.
See the [supported features](https://github.com/fabian-hiller/valibot/blob/main/packages/to-json-schema/README.md#supported-features) for details.
:::
## Installation
## Example
## Async validation
This validator supports async validation.
```typescript
import { adaptAsync } from "@sjsf/valibot-validator";
const { schema, validator } = adaptAsync(valibotSchema);
```
# Zod
import { Code, Card, TabItem, Tabs } from '@astrojs/starlight/components';
import Npm from '@/components/npm.astro';
import FormCard from '@/components/form-card.astro';
import Form from './zod4/form.svelte';
import formCode from './zod4/form.svelte?raw';
import sharedCode from './shared/index.ts?raw';
You may want to use Zod validator because:
- You can use Zod schema as a source of truth for the form value type (`z.infer`).
- This is an easy way to add custom error messages
:::caution
Your schema must be convertible to a JSON schema.
See the [unrepresentable](https://zod.dev/json-schema#unrepresentable) for details.
:::
## Installation
## Example
:::tip
Use `@sjsf/zod4-validator/mini` to work with `zod/mini`
:::
## Async validation
This validator supports async validation.
```typescript
import { adaptAsync } from "@sjsf/zod-validator/classic";
const { schema, validator } = adaptAsync(zodSchema);
```