diff --git a/.changeset/mean-bugs-end.md b/.changeset/mean-bugs-end.md new file mode 100644 index 00000000..4906dc77 --- /dev/null +++ b/.changeset/mean-bugs-end.md @@ -0,0 +1,5 @@ +--- +"@sjsf/form": minor +--- + +Implement `useForm` API, Add `FormContent` and `SubmitButton` components diff --git a/.github/workflows/deploy-pages.yaml b/.github/workflows/deploy-pages.yaml new file mode 100644 index 00000000..89070db2 --- /dev/null +++ b/.github/workflows/deploy-pages.yaml @@ -0,0 +1,54 @@ +name: Deploy to GitHub Pages +on: + release: + types: [published] + +permissions: + id-token: write + contents: write + pages: write + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout code repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install + + - name: Cache turbo build setup + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + + - name: Build + run: pnpm run build --filter="./apps/*" + + - name: Merge apps build outputs + run: mv apps/playground/dist apps/docs/dist/playground + + - name: Upload Pages Artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "apps/docs/dist/" + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/version-and-publish.yaml similarity index 65% rename from .github/workflows/release.yml rename to .github/workflows/version-and-publish.yaml index db603e7d..1fcf83de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/version-and-publish.yaml @@ -1,4 +1,4 @@ -name: Release +name: Version and Publish on: push: branches: @@ -10,7 +10,6 @@ permissions: id-token: write contents: write pull-requests: write - pages: write jobs: version: @@ -41,9 +40,9 @@ jobs: ${{ runner.os }}-turbo- - name: Build - run: pnpm run ci:build + run: pnpm run ci:build --filter="@sjsf/*" - - name: create and publish versions + - name: Create and publish versions uses: changesets/action@v1 with: version: pnpm ci:version @@ -53,23 +52,3 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Merge apps build outputs - run: mv apps/playground/dist apps/docs/dist/playground - - - name: Upload Pages Artifact - uses: actions/upload-pages-artifact@v3 - with: - path: "apps/docs/dist/" - - deploy: - needs: version - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 - diff --git a/README.md b/README.md index fb7d239f..0ec1e285 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ npm install @sjsf/form @sjsf/ajv8-validator ajv@8 ```svelte -
+ required: ["name"] + } + + const form = useForm({ + ...theme, + schema, + validator, + translation, + onSubmit: console.log, + }) + + + ``` ## License diff --git a/apps/docs/src/theme.svelte.ts b/apps/docs/src/astro.svelte.ts similarity index 97% rename from apps/docs/src/theme.svelte.ts rename to apps/docs/src/astro.svelte.ts index c4fc21cf..1456238c 100644 --- a/apps/docs/src/theme.svelte.ts +++ b/apps/docs/src/astro.svelte.ts @@ -14,7 +14,7 @@ const loadTheme = (): Theme => const getPreferredColorScheme = (): DarkOrLight => matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"; -export function astroTheme() { +export function useAstro() { let theme = $state(loadTheme()); $effect(() => { diff --git a/apps/docs/src/components/custom-form.svelte b/apps/docs/src/components/custom-form.svelte deleted file mode 100644 index 922a3d85..00000000 --- a/apps/docs/src/components/custom-form.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/apps/docs/src/components/custom-form.ts b/apps/docs/src/components/custom-form.ts new file mode 100644 index 00000000..e6b90628 --- /dev/null +++ b/apps/docs/src/components/custom-form.ts @@ -0,0 +1,27 @@ +import Ajv, { type ErrorObject } from "ajv"; +import { useForm, type UseFormOptions } from "@sjsf/form"; +import { translation } from "@sjsf/form/translations/en"; +import { theme } from "@sjsf/form/basic-theme"; +import { + AjvValidator, + addFormComponents, + DEFAULT_AJV_CONFIG, +} from "@sjsf/ajv8-validator"; + +type Defaults = "widgets" | "components" | "validator" | "translation"; + +export type CustomOptions = Omit, Defaults> & + Partial, Defaults>>; + +export function useCustomForm(options: CustomOptions) { + const validator = new AjvValidator( + addFormComponents(new Ajv(DEFAULT_AJV_CONFIG)) + ); + return useForm( + Object.setPrototypeOf(options, { + ...theme, + validator, + translation, + }) + ); +} diff --git a/apps/docs/src/content/docs/_schema.ts b/apps/docs/src/content/docs/_schema.ts index 73bac299..182ae9b5 100644 --- a/apps/docs/src/content/docs/_schema.ts +++ b/apps/docs/src/content/docs/_schema.ts @@ -87,7 +87,7 @@ export const uiSchema: UiSchemaRoot = { }, }; -export const initialData = { +export const initialValue = { lastName: "Norris", age: 75, bio: "Roundhouse kicking asses since 1940", diff --git a/apps/docs/src/content/docs/_with-basic.svelte b/apps/docs/src/content/docs/_with-basic.svelte index 713b0dd4..b8a1ab12 100644 --- a/apps/docs/src/content/docs/_with-basic.svelte +++ b/apps/docs/src/content/docs/_with-basic.svelte @@ -1,22 +1,21 @@ - + -
{JSON.stringify(value, null, 2)}
+
{JSON.stringify(form.value, null, 2)}
diff --git a/apps/docs/src/content/docs/_with-daisyui.svelte b/apps/docs/src/content/docs/_with-daisyui.svelte index e3900c86..c07d0a4f 100644 --- a/apps/docs/src/content/docs/_with-daisyui.svelte +++ b/apps/docs/src/content/docs/_with-daisyui.svelte @@ -1,28 +1,30 @@ - -
{JSON.stringify(value, null, 2)}
+
{JSON.stringify(form.value, null, 2)}
diff --git a/apps/docs/src/content/docs/_with-flowbite.svelte b/apps/docs/src/content/docs/_with-flowbite.svelte index 038a9d3e..95d3b64f 100644 --- a/apps/docs/src/content/docs/_with-flowbite.svelte +++ b/apps/docs/src/content/docs/_with-flowbite.svelte @@ -1,27 +1,25 @@ - + -
{JSON.stringify(value, null, 2)}
+
{JSON.stringify(form.value, null, 2)}
diff --git a/apps/docs/src/content/docs/_with-shadcn.svelte b/apps/docs/src/content/docs/_with-shadcn.svelte index 7684fb87..f3a4dc3b 100644 --- a/apps/docs/src/content/docs/_with-shadcn.svelte +++ b/apps/docs/src/content/docs/_with-shadcn.svelte @@ -1,30 +1,28 @@ - + -
{JSON.stringify(value, null, 2)}
+
{JSON.stringify(form.value, null, 2)}
diff --git a/apps/docs/src/content/docs/_with-skeleton.svelte b/apps/docs/src/content/docs/_with-skeleton.svelte index d421fc83..46b313ab 100644 --- a/apps/docs/src/content/docs/_with-skeleton.svelte +++ b/apps/docs/src/content/docs/_with-skeleton.svelte @@ -1,29 +1,30 @@ - -
{JSON.stringify(value, null, 2)}
+
{JSON.stringify(form.value, null, 2)}
diff --git a/apps/docs/src/content/docs/guides/custom-form.mdx b/apps/docs/src/content/docs/advanced/custom-form.mdx similarity index 51% rename from apps/docs/src/content/docs/guides/custom-form.mdx rename to apps/docs/src/content/docs/advanced/custom-form.mdx index 2f9d706d..7119729e 100644 --- a/apps/docs/src/content/docs/guides/custom-form.mdx +++ b/apps/docs/src/content/docs/advanced/custom-form.mdx @@ -1,14 +1,14 @@ --- title: Custom form sidebar: - order: 4 + order: 0 --- import { Code } from '@astrojs/starlight/components'; -import customFormCode from '@/components/custom-form.svelte?raw'; +import customFormCode from '@/components/custom-form.ts?raw'; Even with a simple setup, resulting code is very verbose. -Therefore, after choosing a theme, validator and translation, it is convenient to create a custom component. +Therefore, after choosing a theme, validator and translation, it is convenient to create a wrapper around the `useForm`. - + diff --git a/apps/docs/src/content/docs/advanced/custom-validation.mdx b/apps/docs/src/content/docs/advanced/custom-validation.mdx index 1dc428c6..3353524e 100644 --- a/apps/docs/src/content/docs/advanced/custom-validation.mdx +++ b/apps/docs/src/content/docs/advanced/custom-validation.mdx @@ -1,7 +1,7 @@ --- title: Custom validation sidebar: - order: 2 + order: 1 --- ## Errors transformation and custom validation diff --git a/apps/docs/src/content/docs/advanced/custom-widgets.mdx b/apps/docs/src/content/docs/advanced/custom-widgets.mdx index 21a922b9..f614450e 100644 --- a/apps/docs/src/content/docs/advanced/custom-widgets.mdx +++ b/apps/docs/src/content/docs/advanced/custom-widgets.mdx @@ -1,7 +1,7 @@ --- title: Custom widgets sidebar: - order: 3 + order: 2 --- ## Example diff --git a/apps/docs/src/content/docs/advanced/form-base.mdx b/apps/docs/src/content/docs/advanced/form-base.mdx deleted file mode 100644 index 1850f8cc..00000000 --- a/apps/docs/src/content/docs/advanced/form-base.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Form base -sidebar: - order: 0 ---- - -By default `Form` will calculate default value for provided `schema` and current `value` on each `schema` change. - -```typescript -$effect(() => { - schema; - value = untrack(() => getDefaultFormState( - validator, - schema, - value - )); -}); -``` - -While this behavior is convenient it has some drawbacks: - -- Empty form on initial render when `value` is `undefined` -- Additional render after defaults calculation -- This behavior may not be desirable in some cases - -To prevent this behavior, you can use `FormBase` component, -which provides the same interface as `Form`. - -:::note - -You should calculate initial value by yourself. - -::: - -```svelte - - - -``` diff --git a/apps/docs/src/content/docs/advanced/state-transformation.mdx b/apps/docs/src/content/docs/advanced/state-transformation.mdx index 66ccc6bd..db70c7ef 100644 --- a/apps/docs/src/content/docs/advanced/state-transformation.mdx +++ b/apps/docs/src/content/docs/advanced/state-transformation.mdx @@ -1,7 +1,7 @@ --- title: State transformation sidebar: - order: 4 + order: 3 --- In some cases it may be necessary to transform the form state before it is passed to the validator. @@ -26,32 +26,32 @@ It will be replaced with `@sjsf/form/omit-extra-data` import in the major releas ```svelte - - omitExtraData2(validator, defaultMerger, schema, $state.snapshot(value))} - onSubmit={(value) => { - console.log("transformed", value); - }} - onSubmitError={(errors, _, value) => { - console.log("errors", errors); - console.log("transformed", value); - }} -/> -``` \ No newline at end of file +``` diff --git a/apps/docs/src/content/docs/api-reference/handlers.mdx b/apps/docs/src/content/docs/api-reference/handlers.mdx index 80ca28e5..f849394a 100644 --- a/apps/docs/src/content/docs/api-reference/handlers.mdx +++ b/apps/docs/src/content/docs/api-reference/handlers.mdx @@ -15,9 +15,9 @@ interface FormProps { * The function to get the form data snapshot * * The snapshot is used to validate the form and passed to - * `onSubmit` and `onSubmitError` handlers. + * `onSubmit` and `onSubmitError` handlers. * - * @default () => $state.snapshot(value) + * @default () => $state.snapshot(formValue) */ getSnapshot?: () => SchemaValue | undefined /** @@ -41,7 +41,7 @@ interface FormProps { * * By default it will clear the errors and set `isSubmitted` state to `false`. * - * @default () => { isSubmitted = false; errors.clear() } + * @default () => { isSubmitted = false; errors.clear() } */ onReset?: (e: Event) => void } diff --git a/apps/docs/src/content/docs/api-reference/icons.mdx b/apps/docs/src/content/docs/api-reference/icons.mdx index 23c09e45..c6f0bff8 100644 --- a/apps/docs/src/content/docs/api-reference/icons.mdx +++ b/apps/docs/src/content/docs/api-reference/icons.mdx @@ -4,6 +4,12 @@ sidebar: order: 12 --- +:::note + +See [Translation](../translation/) for `Label` and `Labels` types. + +::: + ```typescript import type { Snippet } from "svelte"; diff --git a/apps/docs/src/content/docs/api-reference/overview.mdx b/apps/docs/src/content/docs/api-reference/overview.mdx deleted file mode 100644 index a9756708..00000000 --- a/apps/docs/src/content/docs/api-reference/overview.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Overview -sidebar: - order: 0 ---- - -import { LinkCard, CardGrid } from '@astrojs/starlight/components'; - -The form component consists of several parts that allow you to divide responsibility for different aspects of the form. - - - - - - - - - diff --git a/apps/docs/src/content/docs/api-reference/translation.mdx b/apps/docs/src/content/docs/api-reference/translation.mdx index 560b08f0..02cf30c7 100644 --- a/apps/docs/src/content/docs/api-reference/translation.mdx +++ b/apps/docs/src/content/docs/api-reference/translation.mdx @@ -23,4 +23,6 @@ interface Labels { "move-array-item-down": []; "add-array-item": []; } -``` \ No newline at end of file + +type Label = keyof Labels; +``` diff --git a/apps/docs/src/content/docs/guides/_bind-form.svelte b/apps/docs/src/content/docs/guides/_bind-form.svelte index 5899b7d6..a401ce87 100644 --- a/apps/docs/src/content/docs/guides/_bind-form.svelte +++ b/apps/docs/src/content/docs/guides/_bind-form.svelte @@ -1,27 +1,22 @@ - - window.alert(v)}> - {null} - + const form = useCustomForm({ + schema, + onSubmit: (v) => window.alert(v), + }); - + let formElement: HTMLFormElement; + - + + + + + diff --git a/apps/docs/src/content/docs/guides/_errors-list.svelte b/apps/docs/src/content/docs/guides/_errors-list.svelte index ddd2bee4..50eaa4cc 100644 --- a/apps/docs/src/content/docs/guides/_errors-list.svelte +++ b/apps/docs/src/content/docs/guides/_errors-list.svelte @@ -1,26 +1,27 @@ - -{#if errors.size > 0} + +{#if form.errors.size > 0}
Errors - {#each errors as [field, fieldErrors] (field)} + {#each form.errors as [field, fieldErrors] (field)} {#each fieldErrors as err}
  • "{err.propertyTitle}" {err.message}
  • {/each} diff --git a/apps/docs/src/content/docs/guides/_focus-on-first-error.svelte b/apps/docs/src/content/docs/guides/_focus-on-first-error.svelte index ca09a1f5..5dee733b 100644 --- a/apps/docs/src/content/docs/guides/_focus-on-first-error.svelte +++ b/apps/docs/src/content/docs/guides/_focus-on-first-error.svelte @@ -1,13 +1,20 @@ - diff --git a/apps/docs/src/content/docs/guides/_form-data.svelte b/apps/docs/src/content/docs/guides/_form-data.svelte index 37ae1966..dcd9e794 100644 --- a/apps/docs/src/content/docs/guides/_form-data.svelte +++ b/apps/docs/src/content/docs/guides/_form-data.svelte @@ -1,27 +1,24 @@ - + -
    -
    {JSON.stringify(
    -      { value, errors: Object.fromEntries(errors) },
    -      null,
    -      2
    -    )}
    -
    +
    {JSON.stringify(
    +    { value: form.value, errors: Object.fromEntries(form.errors) },
    +    null,
    +    2
    +  )}
    diff --git a/apps/docs/src/content/docs/guides/_icons.svelte b/apps/docs/src/content/docs/guides/_icons.svelte index 199c8a35..cba656b2 100644 --- a/apps/docs/src/content/docs/guides/_icons.svelte +++ b/apps/docs/src/content/docs/guides/_icons.svelte @@ -1,20 +1,21 @@ - + diff --git a/apps/docs/src/content/docs/guides/_inputs-validation.svelte b/apps/docs/src/content/docs/guides/_inputs-validation.svelte index 72f641ea..0ac02aa6 100644 --- a/apps/docs/src/content/docs/guides/_inputs-validation.svelte +++ b/apps/docs/src/content/docs/guides/_inputs-validation.svelte @@ -1,14 +1,19 @@ - diff --git a/apps/docs/src/content/docs/guides/_live-validation.svelte b/apps/docs/src/content/docs/guides/_live-validation.svelte index f37e008c..6a13d5b5 100644 --- a/apps/docs/src/content/docs/guides/_live-validation.svelte +++ b/apps/docs/src/content/docs/guides/_live-validation.svelte @@ -1,7 +1,6 @@ -
    + diff --git a/apps/docs/src/content/docs/guides/_minimal-setup.svelte b/apps/docs/src/content/docs/guides/_minimal-setup.svelte index 31d3618e..06ac5e55 100644 --- a/apps/docs/src/content/docs/guides/_minimal-setup.svelte +++ b/apps/docs/src/content/docs/guides/_minimal-setup.svelte @@ -1,6 +1,6 @@ - e.preventDefault()} -/> + diff --git a/apps/docs/src/content/docs/guides/_ui-schema.svelte b/apps/docs/src/content/docs/guides/_ui-schema.svelte index 40afb6b8..623c6c42 100644 --- a/apps/docs/src/content/docs/guides/_ui-schema.svelte +++ b/apps/docs/src/content/docs/guides/_ui-schema.svelte @@ -1,11 +1,11 @@ - + diff --git a/apps/docs/src/content/docs/guides/labels-and-icons.mdx b/apps/docs/src/content/docs/guides/labels-and-icons.mdx index 51bd2457..d94b0860 100644 --- a/apps/docs/src/content/docs/guides/labels-and-icons.mdx +++ b/apps/docs/src/content/docs/guides/labels-and-icons.mdx @@ -47,11 +47,13 @@ Usage: ```svelte - + const form = useForm({ + icons + }) + ``` - import { Form } from '@sjsf/form'; + import { useForm } from '@sjsf/form'; import { icons } from '@sjsf/flowbite-icons'; - - + const form = useForm({ + icons + }) + ``` - -## Methods - -Also you can bind to `this` to get access to exported methods: - -### Validate - -Perform validation on the form without form state modifications. - -```typescript -import type { Errors } from '@sjsf/form'; - -function validate(): Errors -``` diff --git a/apps/docs/src/content/docs/guides/quickstart.mdx b/apps/docs/src/content/docs/guides/quickstart.mdx index 90c9b219..6d9ca597 100644 --- a/apps/docs/src/content/docs/guides/quickstart.mdx +++ b/apps/docs/src/content/docs/guides/quickstart.mdx @@ -8,10 +8,12 @@ import { Code, Card, LinkCard } from '@astrojs/starlight/components'; import MinimalSetup from './_minimal-setup.astro'; -import UiSchema from './_ui-schema.astro'; -import FormData from './_form-data.astro'; import minimalSetupCode from './_minimal-setup.svelte?raw'; + +import UiSchema from './_ui-schema.astro'; import uiSchemaCode from './_ui-schema.svelte?raw'; + +import FormData from './_form-data.astro'; import formDataCode from './_form-data.svelte?raw'; ## Minimal setup @@ -24,17 +26,13 @@ Let's explore the most basic setup: -With this setting, the form will be built based on the JSON Schema definition and -will behave like a normal HTML form, performing only HTML5 validation. - - - -But by providing additional properties we can customize the form behavior. +Quite verbose and here's why: -## Handlers +- **Explicit Configuration**: The library favors explicit configuration over "magic" defaults. +- **Tree-Shakeable Architecture**: Each feature is separated into modules 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 customize every piece of from. -By providing `onSubmit` handler, you can achieve the form data validation and get a snapshot of the current form data -(if valid, otherwise the `onSubmitError` handler will be triggered with the errors). + @@ -42,6 +40,12 @@ By providing `onSubmit` handler, you can achieve the form data validation and ge You can also customize the form UI with the `uiSchema` property. +:::note + +We'll talk about `useCustomForm` in [Custom form](../../advanced/custom-form/) guide. + +::: + @@ -53,16 +57,10 @@ You can also customize the form UI with the `uiSchema` property. ## Form state -You can use bindable properties like `value` and `errors` to work with form state. +You can use properties like `value` and `errors` to work with form state. - -:::tip - -To make the entire form read-only use the `inert` attribute. - -::: diff --git a/apps/docs/src/content/docs/guides/validation.mdx b/apps/docs/src/content/docs/guides/validation.mdx index 41c24043..02df20ec 100644 --- a/apps/docs/src/content/docs/guides/validation.mdx +++ b/apps/docs/src/content/docs/guides/validation.mdx @@ -7,16 +7,18 @@ sidebar: import { Code, Card, LinkCard } from '@astrojs/starlight/components'; import LiveValidation from './_live-validation.astro' -import InputsValidation from './_inputs-validation.astro' -import FocusOnFirsError from './_focus-on-first-error.astro' -import ErrorsList from './_errors-list.astro' - import liveValidationCode from './_live-validation.svelte?raw' + +import InputsValidation from './_inputs-validation.astro' import inputsValidationCode from './_inputs-validation.svelte?raw' + +import FocusOnFirsError from './_focus-on-first-error.astro' import focusOnFirstErrorCode from './_focus-on-first-error.svelte?raw' + +import ErrorsList from './_errors-list.astro' import errorsListCode from './_errors-list.svelte?raw' -By default form data will be validated by HTML5 validation and provided validator only on submission. +By default, form data will only be validated by HTML5 validation and the provided validator when submitted. :::tip diff --git a/apps/docs/src/content/docs/guides/value-vs-form-value.mdx b/apps/docs/src/content/docs/guides/value-vs-form-value.mdx new file mode 100644 index 00000000..43bf84ff --- /dev/null +++ b/apps/docs/src/content/docs/guides/value-vs-form-value.mdx @@ -0,0 +1,20 @@ +--- +title: "`value` vs `formValue`" +sidebar: + order: 4 +--- + +Once a form is created, you have access to its state through the `value` and `formValue` properties. +Let's take a look at their areas of application: + +`value: T | undefined` + +- Intended for use by the user +- Maintains the consistency of the form state + - Returns a snapshot of the form state + - The update takes into account the default values from the JSON Schema + +`formValue: SchemaValue | undefined` + +- Intended for use by the library +- Direct access to the form state diff --git a/apps/docs/src/content/docs/themes/_basic-theme.svelte b/apps/docs/src/content/docs/themes/_basic-theme.svelte index c7e93c9b..a75e2afe 100644 --- a/apps/docs/src/content/docs/themes/_basic-theme.svelte +++ b/apps/docs/src/content/docs/themes/_basic-theme.svelte @@ -1,20 +1,22 @@ - -
    {JSON.stringify(value, null, 2)}
    +
    {JSON.stringify(form.value, null, 2)}
    diff --git a/apps/docs/src/content/docs/themes/_daisyui-theme.svelte b/apps/docs/src/content/docs/themes/_daisyui-theme.svelte index cd8dc887..0f61bad6 100644 --- a/apps/docs/src/content/docs/themes/_daisyui-theme.svelte +++ b/apps/docs/src/content/docs/themes/_daisyui-theme.svelte @@ -1,25 +1,26 @@ - -
    {JSON.stringify(value, null, 2)}
    +
    {JSON.stringify(form.value, null, 2)}
    diff --git a/apps/docs/src/content/docs/themes/_flowbite-theme.svelte b/apps/docs/src/content/docs/themes/_flowbite-theme.svelte index 728b9f85..c3711dbf 100644 --- a/apps/docs/src/content/docs/themes/_flowbite-theme.svelte +++ b/apps/docs/src/content/docs/themes/_flowbite-theme.svelte @@ -1,24 +1,25 @@ - -
    {JSON.stringify(value, null, 2)}
    +
    {JSON.stringify(form.value, null, 2)}
    diff --git a/apps/docs/src/content/docs/themes/_shadcn-theme.svelte b/apps/docs/src/content/docs/themes/_shadcn-theme.svelte index c40b4e70..2f6bd5da 100644 --- a/apps/docs/src/content/docs/themes/_shadcn-theme.svelte +++ b/apps/docs/src/content/docs/themes/_shadcn-theme.svelte @@ -1,27 +1,24 @@ - + -
    {JSON.stringify(value, null, 2)}
    +
    {JSON.stringify(form.value, null, 2)}
    diff --git a/apps/docs/src/content/docs/themes/_skeleton-theme.svelte b/apps/docs/src/content/docs/themes/_skeleton-theme.svelte index 4d7985a5..62e53300 100644 --- a/apps/docs/src/content/docs/themes/_skeleton-theme.svelte +++ b/apps/docs/src/content/docs/themes/_skeleton-theme.svelte @@ -1,26 +1,27 @@ - -
    {JSON.stringify(value, null, 2)}
    +
    {JSON.stringify(form.value, null, 2)}
    diff --git a/apps/playground/src/app.svelte b/apps/playground/src/app.svelte index fd457cd5..c548feae 100644 --- a/apps/playground/src/app.svelte +++ b/apps/playground/src/app.svelte @@ -1,12 +1,13 @@
    {name} @@ -186,36 +223,17 @@
    - +
    - {#key themeName} - { - console.log("submit", value); - }} - onSubmitError={(errors, e) => { - if (doFocusOnFirstError) { - focusOnFirstError(errors, e); - } - console.log("errors", errors); - }} - /> - {/key} +
    diff --git a/apps/playground/src/form.svelte b/apps/playground/src/form.svelte deleted file mode 100644 index a9305fb6..00000000 --- a/apps/playground/src/form.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/packages/daisyui-theme/README.md b/packages/daisyui-theme/README.md index 6140a605..5a101c02 100644 --- a/packages/daisyui-theme/README.md +++ b/packages/daisyui-theme/README.md @@ -2,7 +2,7 @@ The [daisyui](https://github.com/saadeghi/daisyui) based theme for [svelte-jsonschema-form](https://github.com/x0k/svelte-jsonschema-form). -- [Documentation](https://x0k.github.io/svelte-jsonschema-form/) +- [Documentation](https://x0k.github.io/svelte-jsonschema-form/themes/daisyui/) - [Playground](https://x0k.github.io/svelte-jsonschema-form/playground/) ## Installation @@ -52,11 +52,13 @@ import themeStyles from "@sjsf/daisyui-theme/styles.css?inline"; ```svelte - + const form = useForm({ + ...theme + }) + ``` ## License diff --git a/packages/flowbite-icons/README.md b/packages/flowbite-icons/README.md index 98b811f7..47a1fb89 100644 --- a/packages/flowbite-icons/README.md +++ b/packages/flowbite-icons/README.md @@ -2,7 +2,7 @@ The [flowbite-svelte-icons](https://github.com/themesberg/flowbite-svelte-icons) based icons set for [svelte-jsonschema-form](https://github.com/x0k/svelte-jsonschema-form). -- [Documentation](https://x0k.github.io/svelte-jsonschema-form/) +- [Documentation](https://x0k.github.io/svelte-jsonschema-form/guides/labels-and-icons/#flowbite-icons) - [Playground](https://x0k.github.io/svelte-jsonschema-form/playground/) ## Installation @@ -50,11 +50,13 @@ import flowbiteIconsStyles from "@sjsf/flowbite-icons/styles.css?inline"; ```svelte - + const form = useForm({ + icons + }) + ``` ## License diff --git a/packages/flowbite-theme/README.md b/packages/flowbite-theme/README.md index f1f48e74..4613b895 100644 --- a/packages/flowbite-theme/README.md +++ b/packages/flowbite-theme/README.md @@ -2,7 +2,7 @@ The [flowbite-svelte](https://github.com/themesberg/flowbite-svelte) based theme for [svelte-jsonschema-form](https://github.com/x0k/svelte-jsonschema-form). -- [Documentation](https://x0k.github.io/svelte-jsonschema-form/) +- [Documentation](https://x0k.github.io/svelte-jsonschema-form/themes/flowbite/) - [Playground](https://x0k.github.io/svelte-jsonschema-form/playground/) ## Installation @@ -52,11 +52,13 @@ import themeStyles from "@sjsf/flowbite-theme/styles.css?inline"; ```svelte - + const form = useForm({ + ...theme, + }) + ``` ## License diff --git a/packages/form/README.md b/packages/form/README.md index 746d7aa1..5234e6db 100644 --- a/packages/form/README.md +++ b/packages/form/README.md @@ -19,7 +19,7 @@ npm install @sjsf/form @sjsf/ajv8-validator ajv@8 ```svelte - + } + + const form = useForm({ + ...theme, + schema, + validator, + translation, + onSubmit: console.log + }) + + + ``` ## License diff --git a/packages/form/src/form/fields/object/object-field.svelte b/packages/form/src/form/fields/object/object-field.svelte index ccbbeeb2..f0e1268d 100644 --- a/packages/form/src/form/fields/object/object-field.svelte +++ b/packages/form/src/form/fields/object/object-field.svelte @@ -64,7 +64,9 @@ // will populate their `defaults`. $effect.pre(() => { schemaProperties; - value = untrack(() => getDefaultFieldState(ctx, retrievedSchema, value) as SchemaObjectValue); + untrack(() => { + value = getDefaultFieldState(ctx, retrievedSchema, value) as SchemaObjectValue + }); }) const schemaPropertiesOrder = $derived( diff --git a/packages/form/src/form/form-base.svelte b/packages/form/src/form/form-base.svelte index bea44c6e..01e6a51d 100644 --- a/packages/form/src/form/form-base.svelte +++ b/packages/form/src/form/form-base.svelte @@ -1,3 +1,7 @@ + + + diff --git a/packages/form/src/form/form.svelte b/packages/form/src/form/form.svelte index 43f38b6b..2413d79e 100644 --- a/packages/form/src/form/form.svelte +++ b/packages/form/src/form/form.svelte @@ -1,3 +1,7 @@ + + +{#if icon} + {@render icon(data)} +{:else} + {ctx.translation.apply(null, data as never)} +{/if} diff --git a/packages/form/src/form/index.ts b/packages/form/src/form/index.ts index 5fce76fe..4ff4a297 100644 --- a/packages/form/src/form/index.ts +++ b/packages/form/src/form/index.ts @@ -45,3 +45,8 @@ export { type Props as FormProps, } from "./form-base.svelte"; export { default as Form } from "./form.svelte"; + +export * from './use-form.svelte.js' +export { default as FormContent } from "./form-content.svelte"; +export { default as SubmitButton } from "./submit-button.svelte"; +export { default as SimpleForm } from "./simple-form.svelte"; diff --git a/packages/form/src/form/simple-form.svelte b/packages/form/src/form/simple-form.svelte new file mode 100644 index 00000000..125dfdab --- /dev/null +++ b/packages/form/src/form/simple-form.svelte @@ -0,0 +1,19 @@ + + + + + + + diff --git a/packages/form/src/form/use-form.svelte.ts b/packages/form/src/form/use-form.svelte.ts new file mode 100644 index 00000000..41ab4b1e --- /dev/null +++ b/packages/form/src/form/use-form.svelte.ts @@ -0,0 +1,273 @@ +import type { ComponentInternals, Snippet } from "svelte"; +import type { Action } from "svelte/action"; +import { SvelteMap } from "svelte/reactivity"; + +import type { SchedulerYield } from "@/lib/scheduler.js"; +import type { Schema, SchemaValue } from "@/core/schema.js"; + +import type { FormValidator } from "./validator.js"; +import type { Components } from "./component.js"; +import type { Widgets } from "./widgets.js"; +import type { Label, Labels, Translation } from "./translation.js"; +import type { UiSchemaRoot } from "./ui-schema.js"; +import type { Fields } from "./fields/index.js"; +import type { Templates } from "./templates/index.js"; +import type { Icons } from "./icons.js"; +import type { InputsValidationMode } from "./validation.js"; +import type { Errors } from "./errors.js"; +import { setFromContext } from "./context/index.js"; +import { DefaultFormMerger, type FormMerger } from "./merger.js"; +import { fields as defaultFields } from "./fields/index.js"; +import { templates as defaultTemplates } from "./templates/index.js"; +import { DEFAULT_ID_PREFIX, DEFAULT_ID_SEPARATOR } from "./id-schema.js"; +import IconOrTranslation from "./icon-or-translation.svelte"; + +export interface UseFormOptions { + validator: FormValidator; + schema: Schema; + components: Components; + translation: Translation; + widgets: Widgets; + uiSchema?: UiSchemaRoot; + merger?: FormMerger; + fields?: Fields; + templates?: Templates; + icons?: Icons; + inputsValidationMode?: InputsValidationMode; + disabled?: boolean; + idPrefix?: string; + idSeparator?: string; + // + initialValue?: T; + initialErrors?: Errors; + /** + * The function to get the form data snapshot + * + * The snapshot is used to validate the form and passed to + * `onSubmit` and `onSubmitError` handlers. + * + * @default () => $state.snapshot(formValue) + */ + getSnapshot?: () => SchemaValue | undefined; + /** + * Submit handler + * + * Will be called when the form is submitted and form data + * snapshot is valid + */ + onSubmit?: (value: T | undefined, e: SubmitEvent) => void; + /** + * Submit error handler + * + * Will be called when the form is submitted and form data + * snapshot is not valid + */ + onSubmitError?: ( + errors: Errors, + e: SubmitEvent, + snapshot: SchemaValue | undefined + ) => void; + /** + * Reset handler + * + * Will be called on form reset. + * + * By default it will clear the errors and set `isSubmitted` state to `false`. + * + * @default () => { isSubmitted = false; errors.clear() } + */ + onReset?: (e: Event) => void; + schedulerYield?: SchedulerYield; +} + +export interface FormState { + value: T | undefined; + formValue: SchemaValue | undefined; + errors: Errors; + isSubmitted: boolean; + validate: () => Errors; +} + +export interface FormAPI extends FormState { + enhance: Action; +} + +type Value = SchemaValue | undefined; + +export function useForm(options: UseFormOptions): FormAPI { + const merger = $derived( + options.merger ?? new DefaultFormMerger(options.validator, options.schema) + ); + + let value = $state( + merger.mergeFormDataAndSchemaDefaults( + options.initialValue as Value, + options.schema + ) + ); + let errors: Errors = $state(options.initialErrors ?? new SvelteMap()); + let isSubmitted = $state(false); + + const getSnapshot = $derived( + options.getSnapshot ?? (() => $state.snapshot(value)) + ); + + function validateSnapshot(snapshot: SchemaValue | undefined) { + const list = options.validator.validateFormData(options.schema, snapshot); + return new SvelteMap(SvelteMap.groupBy(list, (error) => error.instanceId)); + } + + const submitHandler = (e: SubmitEvent) => { + e.preventDefault(); + isSubmitted = true; + const snapshot = getSnapshot(); + errors = validateSnapshot(snapshot); + if (errors.size === 0) { + options.onSubmit?.(snapshot as T | undefined, e); + return; + } + options.onSubmitError?.(errors, e, snapshot); + }; + + const resetHandler = $derived( + options.onReset ?? + (() => { + isSubmitted = false; + errors.clear(); + }) + ); + + const inputsValidationMode = $derived(options.inputsValidationMode ?? 0); + const uiSchema = $derived(options.uiSchema ?? {}); + const disabled = $derived(options.disabled ?? false); + const idPrefix = $derived(options.idPrefix ?? DEFAULT_ID_PREFIX); + const idSeparator = $derived(options.idSeparator ?? DEFAULT_ID_SEPARATOR); + const fields = $derived(options.fields ?? defaultFields); + const templates = $derived(options.templates ?? defaultTemplates); + const icons = $derived(options.icons ?? {}); + const schedulerYield: SchedulerYield = $derived( + (options.schedulerYield ?? + (typeof scheduler !== "undefined" && "yield" in scheduler)) + ? scheduler.yield.bind(scheduler) + : ({ signal }: Parameters[0]) => + new Promise((resolve, reject) => { + setTimeout(() => { + if (signal.aborted) { + reject(signal.reason); + } else { + resolve(); + } + }, 0); + }) + ); + + setFromContext({ + get inputsValidationMode() { + return inputsValidationMode; + }, + get isSubmitted() { + return isSubmitted; + }, + get errors() { + return errors; + }, + get schema() { + return options.schema; + }, + get uiSchema() { + return uiSchema; + }, + get disabled() { + return disabled; + }, + get idPrefix() { + return idPrefix; + }, + get idSeparator() { + return idSeparator; + }, + get validator() { + return options.validator; + }, + get merger() { + return merger; + }, + get fields() { + return fields; + }, + get templates() { + return templates; + }, + get components() { + return options.components; + }, + get widgets() { + return options.widgets; + }, + get translation() { + return options.translation; + }, + get icons() { + return icons; + }, + get schedulerYield() { + return schedulerYield; + }, + iconOrTranslation: (( + internals: ComponentInternals, + data: () => [L, ...Labels[L]] + ) => { + IconOrTranslation(internals, { + get data() { + return data(); + }, + }); + }) as unknown as Snippet< + [ + { + [L in Label]: [L, ...Labels[L]]; + }[Label], + ] + >, + }); + + return { + get value() { + return getSnapshot() as T | undefined; + }, + set value(v) { + value = merger.mergeFormDataAndSchemaDefaults(v as Value, options.schema); + }, + get formValue() { + return value; + }, + set formValue(v) { + value = v; + }, + get errors() { + return errors; + }, + set errors(v) { + errors = v; + }, + get isSubmitted() { + return isSubmitted; + }, + set isSubmitted(v) { + isSubmitted = v; + }, + validate() { + return validateSnapshot(getSnapshot()); + }, + enhance(node) { + $effect(() => { + node.addEventListener("submit", submitHandler); + node.addEventListener("reset", resetHandler); + return () => { + node.removeEventListener("submit", submitHandler); + node.removeEventListener("reset", resetHandler); + }; + }); + }, + }; +} diff --git a/packages/lucide-icons/README.md b/packages/lucide-icons/README.md index 8cdae72e..9ee09556 100644 --- a/packages/lucide-icons/README.md +++ b/packages/lucide-icons/README.md @@ -2,7 +2,7 @@ The [lucide](https://github.com/lucide-icons/lucide) based icons set for [svelte-jsonschema-form](https://github.com/x0k/svelte-jsonschema-form). -- [Documentation](https://x0k.github.io/svelte-jsonschema-form/) +- [Documentation](https://x0k.github.io/svelte-jsonschema-form/guides/labels-and-icons/#lucide-icons) - [Playground](https://x0k.github.io/svelte-jsonschema-form/playground/) ## Installation @@ -15,11 +15,13 @@ npm install @sjsf/form @sjsf/lucide-icons ```svelte -
    + const form = useForm({ + icons + }) + ``` ## License diff --git a/packages/shadcn-theme/README.md b/packages/shadcn-theme/README.md index 8e89b6ab..cf486409 100644 --- a/packages/shadcn-theme/README.md +++ b/packages/shadcn-theme/README.md @@ -2,7 +2,7 @@ The [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) based theme for [svelte-jsonschema-form](https://github.com/x0k/svelte-jsonschema-form). -- [Documentation](https://x0k.github.io/svelte-jsonschema-form/) +- [Documentation](https://x0k.github.io/svelte-jsonschema-form/themes/shadcn/) - [Playground](https://x0k.github.io/svelte-jsonschema-form/playground/) ## Installation @@ -50,14 +50,16 @@ import themeStyles from "@sjsf/shadcn-theme/styles.css?inline"; ```svelte - - ``` ## License diff --git a/packages/skeleton-theme/README.md b/packages/skeleton-theme/README.md index dadb36bd..d77ef46c 100644 --- a/packages/skeleton-theme/README.md +++ b/packages/skeleton-theme/README.md @@ -2,7 +2,7 @@ The [skeleton](https://github.com/skeletonlabs/skeleton) based theme for [svelte-jsonschema-form](https://github.com/x0k/svelte-jsonschema-form). -- [Documentation](https://x0k.github.io/svelte-jsonschema-form/) +- [Documentation](https://x0k.github.io/svelte-jsonschema-form/themes/skeleton/) - [Playground](https://x0k.github.io/svelte-jsonschema-form/playground/) ## Installation @@ -66,11 +66,13 @@ Bundled themes: ```svelte - + const form = useForm({ + ...theme, + }) + ``` ## License