Tabbed layout
<script lang="ts"> import { BasicForm, type UiSchemaRoot } from "@sjsf/form";
import { createMyForm } from "@/components/my-form";
import { Layout, makeTabbedFocusOnFirstError, schema, setTabsContext, type TabsContext, } from "./tabs";
const uiSchema = { "ui:components": { layout: Layout, }, items: { "ui:components": { layout: Layout, }, }, } satisfies UiSchemaRoot;
const tabsCtx: TabsContext = new Map(); setTabsContext(tabsCtx);
const form = createMyForm({ schema, uiSchema, onSubmit: console.log, onSubmitError: makeTabbedFocusOnFirstError(tabsCtx), });</script>
<BasicForm {form} novalidate />
<script lang="ts"> import { overrideByRecord } from "@sjsf/form/lib/resolver"; import { BasicForm } from "@sjsf/form";
import { theme } from "@/components/form-defaults"; import { createMyForm } from "@/components/my-form";
import { Layout, makeTabbedFocusOnFirstError, schema, setTabsContext, type TabsContext, } from "./tabs";
const tabsCtx: TabsContext = new Map(); setTabsContext(tabsCtx);
const form = createMyForm({ schema, theme: overrideByRecord(theme, { layout: Layout, }), onSubmit: console.log, onSubmitError: makeTabbedFocusOnFirstError(tabsCtx), });</script>
<BasicForm {form} novalidate />
export * from './context.svelte'export * from './focus'export * from './schema'export { default as Layout } from './layout.svelte'
import type { Schema } from '@sjsf/form';
export const schema = { title: "Multi page form", type: "array", items: [ { title: "Page 1", type: "array", items: [ { title: "Page 1.1", type: "object", properties: { label: { type: "string", title: "Label", }, }, required: ["label"], }, { title: "Page 1.2", type: "object", properties: { otherField: { type: "string", title: "Other Label", minLength: 3, }, }, required: ["otherField"], }, { title: "Page 1.3", type: "object", properties: { number: { type: "number", title: "Some number", minimum: 5, maximum: 150, }, }, required: ["number"], }, ], }, { title: "Page 2", type: "array", items: [ { title: "Page 2.1", type: "object", properties: { label: { type: "string", title: "Label", }, }, required: ["label"], }, { title: "Page 2.2", type: "object", properties: { otherField: { type: "string", title: "Other Label", minLength: 3, }, }, required: ["otherField"], }, { title: "Page 2.3", type: "object", properties: { number: { type: "number", title: "Some number", minimum: 5, maximum: 150, }, }, required: ["number"], }, ], }, ],} as const satisfies Schema;
import type { Id } from '@sjsf/form'import { getContext, setContext, type Snippet } from 'svelte'
export interface TabsNode { readonly children: TabsContext readonly tabs: Snippet[] selectedTab: number}
export type TabsContext = Map<Id, TabsNode>
const TABS_CONTEXT = Symbol()
export function getTabsContext(): TabsContext { return getContext(TABS_CONTEXT)}
export function setTabsContext(ctx: TabsContext) { setContext(TABS_CONTEXT, ctx)}
const TABS_NODE_CONTEXT = Symbol()
export function getTabsNodeContext(): TabsNode { return getContext(TABS_NODE_CONTEXT)}
export function setTabsNodeContext(ctx: TabsNode) { setContext(TABS_NODE_CONTEXT, ctx)}
export function createTabsNode(initialTab: number): TabsNode { let selectedTab = $state(initialTab) return { children: new Map(), tabs: [], get selectedTab() { return selectedTab }, set selectedTab(v) { selectedTab = v } }}
<script lang="ts"> import { isFixedItems } from '@sjsf/form/core' import type { ComponentProps } from "@sjsf/form"; import { getArrayContext } from "@sjsf/form/fields/array/context.svelte"; import Layout from "@sjsf/basic-theme/components/layout.svelte";
import Tabs from "./tabs.svelte"; import Tab from "./tab.svelte";
const props: ComponentProps["layout"] = $props();
const arrCtx = getArrayContext();
const isTuple = $derived(isFixedItems(arrCtx.config.schema));</script>
{#if props.type === "array-items" && isTuple} <Tabs {...props} />{:else if props.type === "array-item" && isTuple} <Tab {...props} />{:else if !(props.type === "array-field-meta" && isTuple)} <Layout {...props} />{/if}
<script lang="ts"> import type { ComponentProps } from '@sjsf/form';
import { getTabsNodeContext } from './context.svelte';
const { children }: ComponentProps["layout"] = $props(); const node = getTabsNodeContext()
node.tabs.push(children)</script>
<script lang="ts"> import { isSchema } from "@sjsf/form/core"; import type { ComponentProps } from "@sjsf/form";
import { createTabsNode, getTabsContext, setTabsContext, setTabsNodeContext, } from "./context.svelte";
const { config, children }: ComponentProps["layout"] = $props();
const tabsCtx = getTabsContext(); const node = createTabsNode(0); tabsCtx.set(config.id, node); setTabsContext(node.children); setTabsNodeContext(node);
function getTabTitle(i: number): string { // TODO: handle `config.uiOptions` const { items } = config.schema; const fallback = `Tab ${i + 1}`; if (!Array.isArray(items)) { return fallback; } const item = items[i]; if (isSchema(item)) { return item.title ?? fallback; } return fallback; }</script>
{@render children()}
<div style="display: flex; gap: 1rem;"> {#each node.tabs as _, i} <button style="width: 100%;" onclick={(e) => { e.preventDefault(); node.selectedTab = i; }} > {getTabTitle(i)} {#if node.selectedTab === i} (selected) {/if} </button> {/each}</div>
{#each node.tabs as tab, i} <div style:display={node.selectedTab === i ? "unset" : "none"}> {@render tab()} </div>{/each}
<!-- Or render only selected tab -->
<!-- {#if tabs.length > 0} {@render tabs[selectedTab]()}{/if} -->
import { DEFAULT_ID_SEPARATOR, pathToId, type FieldErrorsMap, type PathToIdOptions,} from "@sjsf/form";import { focusOnFirstError } from "@sjsf/form/focus-on-first-error";
import type { TabsContext } from "./context.svelte";
export function makeTabbedFocusOnFirstError<E>( ctx: TabsContext, options: PathToIdOptions = {}) { return (errors: FieldErrorsMap<E>, e: SubmitEvent) => { if (errors.size === 0) { return; } // NOTE: For simplicity, we will switch to the tab with the first error, // although it would be nice to take into account the current tab selection const [key] = errors.entries().next().value!; const path = key .split(options.idSeparator ?? DEFAULT_ID_SEPARATOR) .slice(1); let children = ctx; for (let i = 0; i < path.length && children.size; i++) { const id = pathToId(path.slice(0, i), options); const node = children.get(id); if (node === undefined) { continue; } node.selectedTab = Number(path[i]); children = node.children; } return focusOnFirstError(errors, e); };}