@pindoba/svelte-tanstack-form 0.0.0-alpha-20260617191010
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/dist/demos/basic.svelte +41 -0
- package/dist/demos/basic.svelte.d.ts +3 -0
- package/dist/demos/generic-field.svelte +52 -0
- package/dist/demos/generic-field.svelte.d.ts +18 -0
- package/dist/demos/index.d.ts +3 -0
- package/dist/demos/index.js +3 -0
- package/dist/demos/mixed-controls.svelte +57 -0
- package/dist/demos/mixed-controls.svelte.d.ts +3 -0
- package/dist/field-errors.d.ts +1 -0
- package/dist/field-errors.js +5 -0
- package/dist/form-checkbox.svelte +45 -0
- package/dist/form-checkbox.svelte.d.ts +16 -0
- package/dist/form-field.svelte +63 -0
- package/dist/form-field.svelte.d.ts +18 -0
- package/dist/form-input.svelte +43 -0
- package/dist/form-input.svelte.d.ts +15 -0
- package/dist/form-select.svelte +54 -0
- package/dist/form-select.svelte.d.ts +18 -0
- package/dist/form-submit.svelte +67 -0
- package/dist/form-submit.svelte.d.ts +23 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +15 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +1 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright 2025 Eduardo Lopes
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
8
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
9
|
+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
10
|
+
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
11
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
12
|
+
DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createForm, FormInput, FormSubmit } from "../index.js";
|
|
4
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
5
|
+
|
|
6
|
+
const schema = z.object({
|
|
7
|
+
name: z.string().min(2, "Use at least 2 characters."),
|
|
8
|
+
email: z.string().email("Enter a valid email address."),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
let submitted = $state<string | null>(null);
|
|
12
|
+
|
|
13
|
+
const form = createForm(() => ({
|
|
14
|
+
defaultValues: { name: "", email: "" },
|
|
15
|
+
validators: { onChange: schema },
|
|
16
|
+
onSubmit: async ({ value }) => {
|
|
17
|
+
submitted = JSON.stringify(value);
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<form
|
|
23
|
+
onsubmit={(event) => {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
form.handleSubmit();
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<div class={stack({ gap: "md", direction: "column" })}>
|
|
29
|
+
<FormInput {form} name="name" label="Name" required />
|
|
30
|
+
<FormInput
|
|
31
|
+
{form}
|
|
32
|
+
name="email"
|
|
33
|
+
label="Email"
|
|
34
|
+
type="email"
|
|
35
|
+
required
|
|
36
|
+
labelTooltip="We email your receipt here."
|
|
37
|
+
/>
|
|
38
|
+
<FormSubmit {form}>Create account</FormSubmit>
|
|
39
|
+
{#if submitted}<p>Submitted: {submitted}</p>{/if}
|
|
40
|
+
</div>
|
|
41
|
+
</form>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createForm, FormField } from "../index.js";
|
|
4
|
+
import { InputRoot, InputField } from "@pindoba/svelte-input";
|
|
5
|
+
import Affix from "@pindoba/svelte-affix";
|
|
6
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
7
|
+
|
|
8
|
+
const schema = z.object({
|
|
9
|
+
handle: z
|
|
10
|
+
.string()
|
|
11
|
+
.regex(/^@?[a-z0-9_]{2,}$/i, "Letters, numbers and underscores only."),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const form = createForm(() => ({
|
|
15
|
+
defaultValues: { handle: "" },
|
|
16
|
+
validators: { onChange: schema },
|
|
17
|
+
onSubmit: async () => {},
|
|
18
|
+
}));
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<!--
|
|
22
|
+
The generic <FormField> hands you the props bag + the raw TanStack field so
|
|
23
|
+
you can wire ANY control — here input parts with a leading "@" Affix prefix.
|
|
24
|
+
-->
|
|
25
|
+
<form
|
|
26
|
+
onsubmit={(event) => {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
form.handleSubmit();
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
<div class={stack({ gap: "md", direction: "column" })}>
|
|
32
|
+
<FormField
|
|
33
|
+
{form}
|
|
34
|
+
name="handle"
|
|
35
|
+
label="Username"
|
|
36
|
+
description="Your public @handle."
|
|
37
|
+
>
|
|
38
|
+
{#snippet control(props, field)}
|
|
39
|
+
{@const { feedback, ...inputProps } = props}
|
|
40
|
+
<InputRoot {feedback}>
|
|
41
|
+
<Affix decorative>@</Affix>
|
|
42
|
+
<InputField
|
|
43
|
+
{...inputProps}
|
|
44
|
+
value={field.state.value}
|
|
45
|
+
oninput={(event) => field.handleChange(event.currentTarget.value)}
|
|
46
|
+
onblur={() => field.handleBlur()}
|
|
47
|
+
/>
|
|
48
|
+
</InputRoot>
|
|
49
|
+
{/snippet}
|
|
50
|
+
</FormField>
|
|
51
|
+
</div>
|
|
52
|
+
</form>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const GenericField: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type GenericField = InstanceType<typeof GenericField>;
|
|
18
|
+
export default GenericField;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
createForm,
|
|
5
|
+
FormInput,
|
|
6
|
+
FormSelect,
|
|
7
|
+
FormCheckbox,
|
|
8
|
+
FormSubmit,
|
|
9
|
+
} from "../index.js";
|
|
10
|
+
import { stack } from "@pindoba/styled-system/patterns";
|
|
11
|
+
|
|
12
|
+
const countries = [
|
|
13
|
+
{ id: "br", label: "Brazil" },
|
|
14
|
+
{ id: "us", label: "United States" },
|
|
15
|
+
{ id: "pt", label: "Portugal" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const schema = z.object({
|
|
19
|
+
fullName: z.string().min(2, "Use at least 2 characters."),
|
|
20
|
+
country: z.string().min(1, "Pick a country."),
|
|
21
|
+
terms: z.literal(true, { message: "You must accept the terms." }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let submitted = $state<string | null>(null);
|
|
25
|
+
|
|
26
|
+
const form = createForm(() => ({
|
|
27
|
+
defaultValues: { fullName: "", country: "", terms: false },
|
|
28
|
+
validators: { onChange: schema },
|
|
29
|
+
onSubmit: async ({ value }) => {
|
|
30
|
+
submitted = JSON.stringify(value);
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<form
|
|
36
|
+
onsubmit={(event) => {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
form.handleSubmit();
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div class={stack({ gap: "md", direction: "column" })}>
|
|
42
|
+
<FormInput {form} name="fullName" label="Full name" required />
|
|
43
|
+
<FormSelect
|
|
44
|
+
{form}
|
|
45
|
+
name="country"
|
|
46
|
+
label="Country"
|
|
47
|
+
items={countries}
|
|
48
|
+
placeholder="Select a country"
|
|
49
|
+
required
|
|
50
|
+
/>
|
|
51
|
+
<FormCheckbox {form} name="terms" label="Agreement" errorMode="submitted">
|
|
52
|
+
I accept the terms of service
|
|
53
|
+
</FormCheckbox>
|
|
54
|
+
<FormSubmit {form}>Continue</FormSubmit>
|
|
55
|
+
{#if submitted}<p>Submitted: {submitted}</p>{/if}
|
|
56
|
+
</div>
|
|
57
|
+
</form>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getVisibleErrors, normalizeErrors } from "@pindoba/core-tanstack-form";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The error-normalization + visibility logic is framework-agnostic and lives in
|
|
2
|
+
// `@pindoba/core-tanstack-form`, shared with the Astro adapter. Re-exported here
|
|
3
|
+
// so the block's internal imports and public API stay stable. TanStack's Svelte
|
|
4
|
+
// `AnyFieldApi` structurally satisfies the core's `FieldErrorSource`.
|
|
5
|
+
export { getVisibleErrors, normalizeErrors } from "@pindoba/core-tanstack-form";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { FieldSize } from "@pindoba/core-field";
|
|
4
|
+
import type { AnyFormApi, FormFieldOwnProps } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export interface FormCheckboxProps extends Omit<FormFieldOwnProps, "size"> {
|
|
7
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
8
|
+
form: AnyFormApi;
|
|
9
|
+
/** Field name (a boolean key of the form's data). */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Inline text/content rendered beside the checkbox. */
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
/** Size variant. Checkbox supports `sm`/`md`/`lg` (no `xs`). */
|
|
14
|
+
size?: Exclude<FieldSize, "xs">;
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import FormField from "./form-field.svelte";
|
|
20
|
+
import Checkbox from "@pindoba/svelte-checkbox";
|
|
21
|
+
|
|
22
|
+
let { form, name, children, size, ...fieldProps }: FormCheckboxProps =
|
|
23
|
+
$props();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<FormField {form} {name} {size} {...fieldProps}>
|
|
27
|
+
{#snippet control(props, field)}
|
|
28
|
+
<!--
|
|
29
|
+
Two-way function binding rather than `onchange`: Checkbox forwards consumer
|
|
30
|
+
rest props onto its root <label>, not the inner <input>, so an `onchange`
|
|
31
|
+
handler would read `currentTarget.checked` off the label (undefined). The
|
|
32
|
+
`bind:checked` getter/setter drives the field value directly.
|
|
33
|
+
-->
|
|
34
|
+
<Checkbox
|
|
35
|
+
{...props}
|
|
36
|
+
{size}
|
|
37
|
+
bind:checked={
|
|
38
|
+
() => Boolean(field.state.value), (value) => field.handleChange(value)
|
|
39
|
+
}
|
|
40
|
+
onfocusout={() => field.handleBlur()}
|
|
41
|
+
>
|
|
42
|
+
{#if children}{@render children()}{/if}
|
|
43
|
+
</Checkbox>
|
|
44
|
+
{/snippet}
|
|
45
|
+
</FormField>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { FieldSize } from "@pindoba/core-field";
|
|
3
|
+
import type { AnyFormApi, FormFieldOwnProps } from "./types.js";
|
|
4
|
+
export interface FormCheckboxProps extends Omit<FormFieldOwnProps, "size"> {
|
|
5
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
6
|
+
form: AnyFormApi;
|
|
7
|
+
/** Field name (a boolean key of the form's data). */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Inline text/content rendered beside the checkbox. */
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
/** Size variant. Checkbox supports `sm`/`md`/`lg` (no `xs`). */
|
|
12
|
+
size?: Exclude<FieldSize, "xs">;
|
|
13
|
+
}
|
|
14
|
+
declare const FormCheckbox: import("svelte").Component<FormCheckboxProps, {}, "">;
|
|
15
|
+
type FormCheckbox = ReturnType<typeof FormCheckbox>;
|
|
16
|
+
export default FormCheckbox;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { AnyFieldApi } from "@tanstack/svelte-form";
|
|
3
|
+
import type { FieldControlProps } from "@pindoba/core-field";
|
|
4
|
+
import type { Snippet } from "svelte";
|
|
5
|
+
import type { AnyFormApi, FormFieldOwnProps } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface FormFieldProps extends FormFieldOwnProps {
|
|
8
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
9
|
+
form: AnyFormApi;
|
|
10
|
+
/** Field name (a key of the form's data). */
|
|
11
|
+
name: string;
|
|
12
|
+
/**
|
|
13
|
+
* Render-prop for the control. Receives the props bag to spread onto the
|
|
14
|
+
* control plus the raw TanStack field API for value/handler binding.
|
|
15
|
+
*/
|
|
16
|
+
control: Snippet<[FieldControlProps, AnyFieldApi]>;
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import Field from "@pindoba/svelte-field";
|
|
22
|
+
import { getVisibleErrors } from "./field-errors.js";
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
form,
|
|
26
|
+
name,
|
|
27
|
+
label,
|
|
28
|
+
description,
|
|
29
|
+
required,
|
|
30
|
+
optionalText,
|
|
31
|
+
disabled,
|
|
32
|
+
size,
|
|
33
|
+
orientation,
|
|
34
|
+
feedback,
|
|
35
|
+
labelTooltip,
|
|
36
|
+
errorMode = "touched",
|
|
37
|
+
passThrough,
|
|
38
|
+
control,
|
|
39
|
+
}: FormFieldProps = $props();
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<form.Field {name}>
|
|
43
|
+
{#snippet children(field)}
|
|
44
|
+
{@const errors = getVisibleErrors(field, errorMode)}
|
|
45
|
+
{#snippet fieldControl(controlProps: FieldControlProps)}
|
|
46
|
+
{@render control(controlProps, field)}
|
|
47
|
+
{/snippet}
|
|
48
|
+
<Field
|
|
49
|
+
{label}
|
|
50
|
+
{description}
|
|
51
|
+
{required}
|
|
52
|
+
{optionalText}
|
|
53
|
+
{disabled}
|
|
54
|
+
{size}
|
|
55
|
+
{orientation}
|
|
56
|
+
{feedback}
|
|
57
|
+
{labelTooltip}
|
|
58
|
+
{passThrough}
|
|
59
|
+
error={errors}
|
|
60
|
+
control={fieldControl}
|
|
61
|
+
/>
|
|
62
|
+
{/snippet}
|
|
63
|
+
</form.Field>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AnyFieldApi } from "@tanstack/svelte-form";
|
|
2
|
+
import type { FieldControlProps } from "@pindoba/core-field";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import type { AnyFormApi, FormFieldOwnProps } from "./types.js";
|
|
5
|
+
export interface FormFieldProps extends FormFieldOwnProps {
|
|
6
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
7
|
+
form: AnyFormApi;
|
|
8
|
+
/** Field name (a key of the form's data). */
|
|
9
|
+
name: string;
|
|
10
|
+
/**
|
|
11
|
+
* Render-prop for the control. Receives the props bag to spread onto the
|
|
12
|
+
* control plus the raw TanStack field API for value/handler binding.
|
|
13
|
+
*/
|
|
14
|
+
control: Snippet<[FieldControlProps, AnyFieldApi]>;
|
|
15
|
+
}
|
|
16
|
+
declare const FormField: import("svelte").Component<FormFieldProps, {}, "">;
|
|
17
|
+
type FormField = ReturnType<typeof FormField>;
|
|
18
|
+
export default FormField;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { InputProps } from "@pindoba/svelte-input";
|
|
3
|
+
import type { AnyFormApi, FormFieldOwnProps } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export interface FormInputProps extends FormFieldOwnProps {
|
|
6
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
7
|
+
form: AnyFormApi;
|
|
8
|
+
/** Field name (a key of the form's data). */
|
|
9
|
+
name: string;
|
|
10
|
+
/** HTML input type forwarded to the underlying `<Input>`. */
|
|
11
|
+
type?: InputProps["type"];
|
|
12
|
+
/** Placeholder forwarded to the underlying `<Input>`. */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
import FormField from "./form-field.svelte";
|
|
19
|
+
import Input from "@pindoba/svelte-input";
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
form,
|
|
23
|
+
name,
|
|
24
|
+
type = "text",
|
|
25
|
+
placeholder,
|
|
26
|
+
size,
|
|
27
|
+
...fieldProps
|
|
28
|
+
}: FormInputProps = $props();
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<FormField {form} {name} {size} {...fieldProps}>
|
|
32
|
+
{#snippet control(props, field)}
|
|
33
|
+
<Input
|
|
34
|
+
{...props}
|
|
35
|
+
{size}
|
|
36
|
+
{type}
|
|
37
|
+
{placeholder}
|
|
38
|
+
value={field.state.value}
|
|
39
|
+
oninput={(event) => field.handleChange(event.currentTarget.value)}
|
|
40
|
+
onblur={() => field.handleBlur()}
|
|
41
|
+
/>
|
|
42
|
+
{/snippet}
|
|
43
|
+
</FormField>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { InputProps } from "@pindoba/svelte-input";
|
|
2
|
+
import type { AnyFormApi, FormFieldOwnProps } from "./types.js";
|
|
3
|
+
export interface FormInputProps extends FormFieldOwnProps {
|
|
4
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
5
|
+
form: AnyFormApi;
|
|
6
|
+
/** Field name (a key of the form's data). */
|
|
7
|
+
name: string;
|
|
8
|
+
/** HTML input type forwarded to the underlying `<Input>`. */
|
|
9
|
+
type?: InputProps["type"];
|
|
10
|
+
/** Placeholder forwarded to the underlying `<Input>`. */
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const FormInput: import("svelte").Component<FormInputProps, {}, "">;
|
|
14
|
+
type FormInput = ReturnType<typeof FormInput>;
|
|
15
|
+
export default FormInput;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { ListBoxNodeInput } from "@pindoba/core-listbox";
|
|
3
|
+
import type { FieldSize } from "@pindoba/core-field";
|
|
4
|
+
import type { AnyFormApi, FormFieldOwnProps } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export interface FormSelectProps extends Omit<FormFieldOwnProps, "size"> {
|
|
7
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
8
|
+
form: AnyFormApi;
|
|
9
|
+
/** Field name (a key of the form's data holding the selected option key). */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Options to choose from. */
|
|
12
|
+
items: ListBoxNodeInput[];
|
|
13
|
+
/** Placeholder shown when nothing is selected. */
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
/** Size variant. Select supports `sm`/`md`/`lg` (no `xs`). */
|
|
16
|
+
size?: Exclude<FieldSize, "xs">;
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import FormField from "./form-field.svelte";
|
|
22
|
+
import Select from "@pindoba/svelte-select";
|
|
23
|
+
|
|
24
|
+
let { form, name, items, placeholder, size, ...fieldProps }: FormSelectProps =
|
|
25
|
+
$props();
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<FormField {form} {name} {size} {...fieldProps}>
|
|
29
|
+
{#snippet control(props, field)}
|
|
30
|
+
<!--
|
|
31
|
+
Controlled `value`: the field is the single source of truth. Select's
|
|
32
|
+
`value` is properly controlled (it syncs the prop into its reducer), so a
|
|
33
|
+
parent re-render no longer drops the selection. `onValueChange` writes the
|
|
34
|
+
scalar key back to the form (single-select), and `onOpenChange` marks the
|
|
35
|
+
field touched on dismiss.
|
|
36
|
+
-->
|
|
37
|
+
<Select
|
|
38
|
+
{...props}
|
|
39
|
+
{size}
|
|
40
|
+
{items}
|
|
41
|
+
{placeholder}
|
|
42
|
+
value={field.state.value || null}
|
|
43
|
+
onValueChange={(next) => field.handleChange(next ?? "")}
|
|
44
|
+
onOpenChange={(open) => {
|
|
45
|
+
// Mark touched when the dropdown closes — whether or not a choice was
|
|
46
|
+
// made — so `errorMode="touched"` surfaces a required error after the
|
|
47
|
+
// user opens and dismisses without picking. Select keeps focus on its
|
|
48
|
+
// trigger (aria-activedescendant), so this fires only on a real close,
|
|
49
|
+
// not while navigating options.
|
|
50
|
+
if (!open) field.handleBlur();
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
{/snippet}
|
|
54
|
+
</FormField>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ListBoxNodeInput } from "@pindoba/core-listbox";
|
|
2
|
+
import type { FieldSize } from "@pindoba/core-field";
|
|
3
|
+
import type { AnyFormApi, FormFieldOwnProps } from "./types.js";
|
|
4
|
+
export interface FormSelectProps extends Omit<FormFieldOwnProps, "size"> {
|
|
5
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
6
|
+
form: AnyFormApi;
|
|
7
|
+
/** Field name (a key of the form's data holding the selected option key). */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Options to choose from. */
|
|
10
|
+
items: ListBoxNodeInput[];
|
|
11
|
+
/** Placeholder shown when nothing is selected. */
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
/** Size variant. Select supports `sm`/`md`/`lg` (no `xs`). */
|
|
14
|
+
size?: Exclude<FieldSize, "xs">;
|
|
15
|
+
}
|
|
16
|
+
declare const FormSelect: import("svelte").Component<FormSelectProps, {}, "">;
|
|
17
|
+
type FormSelect = ReturnType<typeof FormSelect>;
|
|
18
|
+
export default FormSelect;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { ButtonProps } from "@pindoba/svelte-button";
|
|
4
|
+
import type { AnyFormApi } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export interface FormSubmitProps {
|
|
7
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
8
|
+
form: AnyFormApi;
|
|
9
|
+
/**
|
|
10
|
+
* Also disable while the form is invalid (not just while submitting). Off by
|
|
11
|
+
* default — the button stays clickable so submitting an invalid form surfaces
|
|
12
|
+
* its errors instead of leaving the user stuck on a disabled control.
|
|
13
|
+
* @default false
|
|
14
|
+
*/
|
|
15
|
+
disableWhenInvalid?: boolean;
|
|
16
|
+
/** Emphasis forwarded to the underlying `<Button>`. */
|
|
17
|
+
emphasis?: ButtonProps["emphasis"];
|
|
18
|
+
/** Size forwarded to the underlying `<Button>`. */
|
|
19
|
+
size?: ButtonProps["size"];
|
|
20
|
+
/** Button label content. */
|
|
21
|
+
children?: Snippet;
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import { untrack } from "svelte";
|
|
27
|
+
import Button from "@pindoba/svelte-button";
|
|
28
|
+
import Loading from "@pindoba/svelte-loading";
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
form,
|
|
32
|
+
disableWhenInvalid = false,
|
|
33
|
+
emphasis = "primary",
|
|
34
|
+
size,
|
|
35
|
+
children,
|
|
36
|
+
}: FormSubmitProps = $props();
|
|
37
|
+
|
|
38
|
+
// The form's submit-readiness lives on a derived store that isn't mounted
|
|
39
|
+
// during SSR, so `state` can be undefined server-side — the selector defaults
|
|
40
|
+
// to an enabled, not-submitting button until the client hydrates. The form
|
|
41
|
+
// instance is stable for the component's lifetime, so capture it once
|
|
42
|
+
// (`untrack`) when wiring the reactive selector subscription.
|
|
43
|
+
const submitState = untrack(() =>
|
|
44
|
+
form.useStore((state) => ({
|
|
45
|
+
canSubmit: state?.canSubmit ?? true,
|
|
46
|
+
isSubmitting: state?.isSubmitting ?? false,
|
|
47
|
+
})),
|
|
48
|
+
);
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<!--
|
|
52
|
+
Wrap the button in Loading's `busy` treatment while the onSubmit promise is in
|
|
53
|
+
flight: it blurs the label and shows a corner spinner (`indicator`). Loading's
|
|
54
|
+
root is `display:contents`, so it doesn't disturb the button's layout.
|
|
55
|
+
-->
|
|
56
|
+
<Loading loading={submitState.current.isSubmitting} variant="busy" indicator>
|
|
57
|
+
<Button
|
|
58
|
+
type="submit"
|
|
59
|
+
{emphasis}
|
|
60
|
+
{size}
|
|
61
|
+
disabled={submitState.current.isSubmitting ||
|
|
62
|
+
(disableWhenInvalid && !submitState.current.canSubmit)}
|
|
63
|
+
aria-busy={submitState.current.isSubmitting ? "true" : undefined}
|
|
64
|
+
>
|
|
65
|
+
{#if children}{@render children()}{:else}Submit{/if}
|
|
66
|
+
</Button>
|
|
67
|
+
</Loading>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ButtonProps } from "@pindoba/svelte-button";
|
|
3
|
+
import type { AnyFormApi } from "./types.js";
|
|
4
|
+
export interface FormSubmitProps {
|
|
5
|
+
/** The TanStack form instance returned by `createForm(...)`. */
|
|
6
|
+
form: AnyFormApi;
|
|
7
|
+
/**
|
|
8
|
+
* Also disable while the form is invalid (not just while submitting). Off by
|
|
9
|
+
* default — the button stays clickable so submitting an invalid form surfaces
|
|
10
|
+
* its errors instead of leaving the user stuck on a disabled control.
|
|
11
|
+
* @default false
|
|
12
|
+
*/
|
|
13
|
+
disableWhenInvalid?: boolean;
|
|
14
|
+
/** Emphasis forwarded to the underlying `<Button>`. */
|
|
15
|
+
emphasis?: ButtonProps["emphasis"];
|
|
16
|
+
/** Size forwarded to the underlying `<Button>`. */
|
|
17
|
+
size?: ButtonProps["size"];
|
|
18
|
+
/** Button label content. */
|
|
19
|
+
children?: Snippet;
|
|
20
|
+
}
|
|
21
|
+
declare const FormSubmit: import("svelte").Component<FormSubmitProps, {}, "">;
|
|
22
|
+
type FormSubmit = ReturnType<typeof FormSubmit>;
|
|
23
|
+
export default FormSubmit;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { createForm, useStore } from "@tanstack/svelte-form";
|
|
2
|
+
export type { AnyFieldApi } from "@tanstack/svelte-form";
|
|
3
|
+
export { default as FormField } from "./form-field.svelte";
|
|
4
|
+
export * from "./form-field.svelte";
|
|
5
|
+
export { default as FormInput } from "./form-input.svelte";
|
|
6
|
+
export * from "./form-input.svelte";
|
|
7
|
+
export { default as FormSelect } from "./form-select.svelte";
|
|
8
|
+
export * from "./form-select.svelte";
|
|
9
|
+
export { default as FormCheckbox } from "./form-checkbox.svelte";
|
|
10
|
+
export * from "./form-checkbox.svelte";
|
|
11
|
+
export { default as FormSubmit } from "./form-submit.svelte";
|
|
12
|
+
export * from "./form-submit.svelte";
|
|
13
|
+
export { getVisibleErrors, normalizeErrors } from "./field-errors.js";
|
|
14
|
+
export type { AnyFormApi, FormFieldErrorMode, FormFieldOwnProps, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Re-export the TanStack Svelte Form runtime so consumers import everything
|
|
2
|
+
// from one place. `createForm` accepts Standard Schema validators natively
|
|
3
|
+
// (Zod 3.24+, Valibot, ArkType): `validators: { onChange: mySchema }`.
|
|
4
|
+
export { createForm, useStore } from "@tanstack/svelte-form";
|
|
5
|
+
export { default as FormField } from "./form-field.svelte";
|
|
6
|
+
export * from "./form-field.svelte";
|
|
7
|
+
export { default as FormInput } from "./form-input.svelte";
|
|
8
|
+
export * from "./form-input.svelte";
|
|
9
|
+
export { default as FormSelect } from "./form-select.svelte";
|
|
10
|
+
export * from "./form-select.svelte";
|
|
11
|
+
export { default as FormCheckbox } from "./form-checkbox.svelte";
|
|
12
|
+
export * from "./form-checkbox.svelte";
|
|
13
|
+
export { default as FormSubmit } from "./form-submit.svelte";
|
|
14
|
+
export * from "./form-submit.svelte";
|
|
15
|
+
export { getVisibleErrors, normalizeErrors } from "./field-errors.js";
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FieldFeedback, FieldOrientation, FieldSize, FieldPassThrough } from "@pindoba/core-field";
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { SvelteFormApi } from "@tanstack/svelte-form";
|
|
4
|
+
import type { FormFieldErrorMode } from "@pindoba/core-tanstack-form";
|
|
5
|
+
export type { FormFieldErrorMode } from "@pindoba/core-tanstack-form";
|
|
6
|
+
/**
|
|
7
|
+
* A TanStack Svelte form instance with its generics erased. The adapter only
|
|
8
|
+
* touches `form.Field` and `form.state`, so the concrete data/validator
|
|
9
|
+
* generics are irrelevant here — any `createForm(...)` result is assignable.
|
|
10
|
+
*/
|
|
11
|
+
export type AnyFormApi = SvelteFormApi<any, any, any, any, any, any, any, any, any, any, any, any>;
|
|
12
|
+
/**
|
|
13
|
+
* Field-level presentation props shared by `<FormField>` and every convenience
|
|
14
|
+
* wrapper (`<FormInput>`, `<FormSelect>`, `<FormCheckbox>`). These mirror the
|
|
15
|
+
* presentational `<Field>` props; the adapter supplies `error` from TanStack.
|
|
16
|
+
*/
|
|
17
|
+
export interface FormFieldOwnProps {
|
|
18
|
+
/** Label text shown above (or beside) the control. */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Help text rendered under the control. */
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Renders a required `*` after the label. */
|
|
23
|
+
required?: boolean;
|
|
24
|
+
/** Optional-hint text after the label when not required. */
|
|
25
|
+
optionalText?: string;
|
|
26
|
+
/** Disables the field and forwards `disabled` to the control. */
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/** Size variant, kept in sync with the control. */
|
|
29
|
+
size?: FieldSize;
|
|
30
|
+
/** Label/control layout direction. */
|
|
31
|
+
orientation?: FieldOrientation;
|
|
32
|
+
/** Explicit feedback override; defaults to error-derived danger / neutral. */
|
|
33
|
+
feedback?: FieldFeedback;
|
|
34
|
+
/** Tooltip attached to an info trigger next to the label. */
|
|
35
|
+
labelTooltip?: string | Snippet;
|
|
36
|
+
/** When the field's errors become visible. @default "touched" */
|
|
37
|
+
errorMode?: FormFieldErrorMode;
|
|
38
|
+
/** Per-slot escape hatch for styling and attributes. */
|
|
39
|
+
passThrough?: FieldPassThrough;
|
|
40
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pindoba/svelte-tanstack-form",
|
|
3
|
+
"version": "0.0.0-alpha-20260617191010",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"svelte": "./dist/index.js",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./demos": {
|
|
13
|
+
"types": "./dist/demos/index.d.ts",
|
|
14
|
+
"svelte": "./dist/demos/index.js",
|
|
15
|
+
"default": "./dist/demos/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./demos/*": {
|
|
18
|
+
"types": "./dist/demos/*.svelte.d.ts",
|
|
19
|
+
"svelte": "./dist/demos/*.svelte",
|
|
20
|
+
"default": "./dist/demos/*.svelte"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"!dist/**/*.test.*",
|
|
26
|
+
"!dist/**/*.spec.*"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@tanstack/svelte-form": "^1.33.0",
|
|
30
|
+
"@pindoba/core-field": "0.0.0-alpha-20260617191010",
|
|
31
|
+
"@pindoba/core-listbox": "0.0.0-alpha-20260617191010",
|
|
32
|
+
"@pindoba/core-tanstack-form": "0.0.0-alpha-20260617191010",
|
|
33
|
+
"@pindoba/styled-system": "0.0.0-alpha-20260617191010",
|
|
34
|
+
"@pindoba/svelte-affix": "0.0.0-alpha-20260617191010",
|
|
35
|
+
"@pindoba/svelte-button": "0.0.0-alpha-20260617191010",
|
|
36
|
+
"@pindoba/svelte-checkbox": "0.0.0-alpha-20260617191010",
|
|
37
|
+
"@pindoba/svelte-input": "0.0.0-alpha-20260617191010",
|
|
38
|
+
"@pindoba/svelte-field": "0.0.0-alpha-20260617191010",
|
|
39
|
+
"@pindoba/svelte-loading": "0.0.0-alpha-20260617191010",
|
|
40
|
+
"@pindoba/svelte-select": "0.0.0-alpha-20260617191010"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"svelte": "^5.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@sveltejs/kit": "^2.65.1",
|
|
47
|
+
"@sveltejs/package": "^2.5.8",
|
|
48
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
49
|
+
"publint": "^0.3.21",
|
|
50
|
+
"svelte": "^5.56.3",
|
|
51
|
+
"svelte-check": "^4.6.0",
|
|
52
|
+
"typescript": "^6.0.3",
|
|
53
|
+
"vite": "^8.0.16",
|
|
54
|
+
"zod": "^4.4.3"
|
|
55
|
+
},
|
|
56
|
+
"default": "./dist/index.js",
|
|
57
|
+
"svelte": "./dist/index.js",
|
|
58
|
+
"types": "./dist/index.d.ts",
|
|
59
|
+
"type": "module",
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "vite build && npm run package",
|
|
62
|
+
"dev:serve": "vite dev",
|
|
63
|
+
"preview": "vite preview",
|
|
64
|
+
"package": "svelte-kit sync && svelte-package && publint",
|
|
65
|
+
"dev": "svelte-kit sync && svelte-package --watch",
|
|
66
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
67
|
+
"lint": "TIMING=1 eslint . --max-warnings 0",
|
|
68
|
+
"clean": "rm -rf dist && rm -rf .svelte-kit && rm -rf node_modules && rm -rf .turbo",
|
|
69
|
+
"format": "prettier --check . --ignore-path \"$PRETTIER_IGNORE_PATH\"",
|
|
70
|
+
"format:fix": "prettier --write . --ignore-path \"$PRETTIER_IGNORE_PATH\""
|
|
71
|
+
}
|
|
72
|
+
}
|