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 ``` ## 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 ``` 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