sv5ui 1.5.1 → 1.6.0
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/dist/Checkbox/Checkbox.svelte +8 -2
- package/dist/CheckboxGroup/CheckboxGroup.svelte +15 -2
- package/dist/Form/Form.svelte +203 -0
- package/dist/Form/Form.svelte.d.ts +26 -0
- package/dist/Form/form.context.svelte.d.ts +64 -0
- package/dist/Form/form.context.svelte.js +478 -0
- package/dist/Form/form.types.d.ts +164 -0
- package/dist/Form/form.types.js +12 -0
- package/dist/Form/form.variants.d.ts +39 -0
- package/dist/Form/form.variants.js +17 -0
- package/dist/Form/index.d.ts +4 -0
- package/dist/Form/index.js +6 -0
- package/dist/Form/validate-schema.d.ts +13 -0
- package/dist/Form/validate-schema.js +113 -0
- package/dist/FormField/FormField.svelte +71 -8
- package/dist/FormField/form-field.types.d.ts +15 -0
- package/dist/Input/Input.svelte +31 -5
- package/dist/Input/Input.svelte.d.ts +25 -4
- package/dist/Input/input.types.d.ts +24 -3
- package/dist/PinInput/PinInput.svelte +9 -2
- package/dist/RadioGroup/RadioGroup.svelte +17 -3
- package/dist/Select/Select.svelte +14 -3
- package/dist/SelectMenu/SelectMenu.svelte +9 -2
- package/dist/Slider/Slider.svelte +4 -1
- package/dist/Switch/Switch.svelte +8 -2
- package/dist/Textarea/Textarea.svelte +27 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useFormField.svelte.d.ts +64 -0
- package/dist/hooks/useFormField.svelte.js +70 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +26 -3
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { checkboxVariants, checkboxDefaults } from './checkbox.variants.js'
|
|
10
10
|
import { getComponentConfig, iconsDefaults } from '../config.js'
|
|
11
11
|
import Icon from '../Icon/Icon.svelte'
|
|
12
|
-
import { useFormField } from '../hooks/useFormField.svelte.js'
|
|
12
|
+
import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
|
|
13
13
|
|
|
14
14
|
const config = getComponentConfig('checkbox', checkboxDefaults)
|
|
15
15
|
const icons = getComponentConfig('icons', iconsDefaults)
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
}: Props = $props()
|
|
44
44
|
|
|
45
45
|
const formFieldContext = useFormField()
|
|
46
|
+
const emit = useFormFieldEmit()
|
|
46
47
|
|
|
47
48
|
const hasError = $derived(
|
|
48
49
|
formFieldContext?.error !== undefined && formFieldContext?.error !== false
|
|
@@ -107,9 +108,14 @@
|
|
|
107
108
|
<div bind:this={containerRef} class={classes.container}>
|
|
108
109
|
<Checkbox.Root
|
|
109
110
|
bind:checked
|
|
110
|
-
{
|
|
111
|
+
onCheckedChange={(val) => {
|
|
112
|
+
emit.onChange()
|
|
113
|
+
onCheckedChange?.(val)
|
|
114
|
+
}}
|
|
111
115
|
bind:indeterminate
|
|
112
116
|
{onIndeterminateChange}
|
|
117
|
+
onblur={() => emit.onBlur()}
|
|
118
|
+
onfocus={() => emit.onFocus()}
|
|
113
119
|
id={resolvedId}
|
|
114
120
|
name={resolvedName}
|
|
115
121
|
{value}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { checkboxGroupVariants, checkboxGroupDefaults } from './checkbox-group.variants.js'
|
|
10
10
|
import { getComponentConfig, iconsDefaults } from '../config.js'
|
|
11
11
|
import Icon from '../Icon/Icon.svelte'
|
|
12
|
-
import { useFormField } from '../hooks/useFormField.svelte.js'
|
|
12
|
+
import { useFormField, useFormFieldEmit } from '../hooks/useFormField.svelte.js'
|
|
13
13
|
import type { CheckboxGroupItem } from './checkbox-group.types.js'
|
|
14
14
|
|
|
15
15
|
const config = getComponentConfig('checkboxGroup', checkboxGroupDefaults)
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
}: Props = $props()
|
|
43
43
|
|
|
44
44
|
const formFieldContext = useFormField()
|
|
45
|
+
const emit = useFormFieldEmit()
|
|
45
46
|
|
|
46
47
|
const hasError = $derived(
|
|
47
48
|
formFieldContext?.error !== undefined && formFieldContext?.error !== false
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
} else {
|
|
98
99
|
value = value.filter((v) => v !== itemValue)
|
|
99
100
|
}
|
|
101
|
+
emit.onChange()
|
|
100
102
|
onValueChange?.(value)
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -169,7 +171,18 @@
|
|
|
169
171
|
{/snippet}
|
|
170
172
|
|
|
171
173
|
<div {...restProps} bind:this={ref} class={layoutClasses.root}>
|
|
172
|
-
<fieldset
|
|
174
|
+
<fieldset
|
|
175
|
+
class={layoutClasses.fieldset}
|
|
176
|
+
aria-describedby={ariaDescribedBy}
|
|
177
|
+
onfocusin={() => emit.onFocus()}
|
|
178
|
+
onfocusout={(e) => {
|
|
179
|
+
// Only emit blur when focus leaves the fieldset entirely, not when
|
|
180
|
+
// moving between checkboxes within the group.
|
|
181
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
|
|
182
|
+
emit.onBlur()
|
|
183
|
+
}
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
173
186
|
{#if legend || legendSlot}
|
|
174
187
|
{#if legendSlot}
|
|
175
188
|
{@render legendSlot({ legend })}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { FormProps, FormSchema } from './form.types.js'
|
|
3
|
+
|
|
4
|
+
export type Props<
|
|
5
|
+
S extends FormSchema | undefined = FormSchema | undefined,
|
|
6
|
+
T extends boolean = true,
|
|
7
|
+
N extends boolean = false
|
|
8
|
+
> = FormProps<S, T, N>
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<script
|
|
12
|
+
lang="ts"
|
|
13
|
+
generics="S extends FormSchema | undefined = FormSchema | undefined, T extends boolean = true, N extends boolean = false"
|
|
14
|
+
>
|
|
15
|
+
import { setContext, onMount, onDestroy, untrack } from 'svelte'
|
|
16
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
17
|
+
import { formVariants, formDefaults } from './form.variants.js'
|
|
18
|
+
import { FormContext, FORM_CONTEXT_KEY, getFormContext } from './form.context.svelte.js'
|
|
19
|
+
import { getComponentConfig } from '../config.js'
|
|
20
|
+
import type { FormApi, FormErrorWithId } from './form.types.js'
|
|
21
|
+
|
|
22
|
+
const config = getComponentConfig('form', formDefaults)
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
ref = $bindable(null),
|
|
26
|
+
api = $bindable<FormApi<unknown> | undefined>(),
|
|
27
|
+
id,
|
|
28
|
+
schema,
|
|
29
|
+
state = $bindable({} as never),
|
|
30
|
+
validate: customValidate,
|
|
31
|
+
validateOn = ['input', 'blur', 'change'],
|
|
32
|
+
validateOnInputDelay = 300,
|
|
33
|
+
disabled = false,
|
|
34
|
+
loadingAuto = true,
|
|
35
|
+
transform = true as T,
|
|
36
|
+
nested = false as N,
|
|
37
|
+
name,
|
|
38
|
+
onsubmit,
|
|
39
|
+
onerror,
|
|
40
|
+
class: className,
|
|
41
|
+
ui,
|
|
42
|
+
children,
|
|
43
|
+
...restProps
|
|
44
|
+
}: Props<S, T, N> = $props()
|
|
45
|
+
|
|
46
|
+
// Generate a stable form id. Both `id` and `nested` are effectively init-only
|
|
47
|
+
// (they should not change over a form instance's lifetime), so `untrack`
|
|
48
|
+
// silences Svelte's state_referenced_locally warning.
|
|
49
|
+
const formId: string | number = untrack(
|
|
50
|
+
() =>
|
|
51
|
+
(id as string | number | undefined) ??
|
|
52
|
+
(typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
53
|
+
? crypto.randomUUID()
|
|
54
|
+
: `sv5ui-form-${Math.random().toString(36).slice(2)}`)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// Parent context captured once at init — `nested` is effectively immutable
|
|
58
|
+
// over a form instance's lifetime. We warn (dev-only) if it's mutated later.
|
|
59
|
+
const parentCtx = untrack(() => (nested ? getFormContext() : undefined))
|
|
60
|
+
const initialNested = untrack(() => nested)
|
|
61
|
+
$effect(() => {
|
|
62
|
+
if (nested !== initialNested) {
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.warn(
|
|
65
|
+
'[sv5ui Form] The `nested` prop was changed after mount. This is ' +
|
|
66
|
+
'not supported — nested/standalone status is fixed at mount time. ' +
|
|
67
|
+
'Unmount and remount the Form to toggle.'
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Create the reactive form context. All config is passed via getter closures
|
|
73
|
+
// so that reactive prop changes are visible inside the class.
|
|
74
|
+
const ctx = new FormContext(
|
|
75
|
+
{
|
|
76
|
+
getState: () => state as unknown,
|
|
77
|
+
getSchema: () => schema as FormSchema | undefined,
|
|
78
|
+
getCustomValidate: () =>
|
|
79
|
+
customValidate as
|
|
80
|
+
| ((s: unknown) => ReturnType<NonNullable<typeof customValidate>>)
|
|
81
|
+
| undefined,
|
|
82
|
+
getValidateOn: () => validateOn,
|
|
83
|
+
getValidateOnInputDelay: () => validateOnInputDelay,
|
|
84
|
+
getDisabled: () => disabled,
|
|
85
|
+
getLoadingAuto: () => loadingAuto,
|
|
86
|
+
getTransform: () => transform,
|
|
87
|
+
getName: () => name as string | undefined,
|
|
88
|
+
getOnSubmit: () =>
|
|
89
|
+
onsubmit as
|
|
90
|
+
| ((
|
|
91
|
+
e: import('./form.types.js').FormSubmitEvent<unknown>
|
|
92
|
+
) => void | Promise<void>)
|
|
93
|
+
| undefined,
|
|
94
|
+
getOnError: () => onerror
|
|
95
|
+
},
|
|
96
|
+
formId,
|
|
97
|
+
parentCtx
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
setContext(FORM_CONTEXT_KEY, ctx)
|
|
101
|
+
|
|
102
|
+
// Build the bindable API object. Getters ensure reactive state reads stay live.
|
|
103
|
+
const apiObject: FormApi<unknown> = {
|
|
104
|
+
validate: (opts) => ctx.validate(opts) as Promise<unknown | false>,
|
|
105
|
+
submit: () => ctx.submit(),
|
|
106
|
+
clear: (name) => ctx.clear(name),
|
|
107
|
+
getErrors: (name) => ctx.getErrors(name),
|
|
108
|
+
setErrors: (errs, name) => ctx.setErrors(errs, name),
|
|
109
|
+
reset: () => ctx.reset(),
|
|
110
|
+
get errors() {
|
|
111
|
+
return ctx.errors
|
|
112
|
+
},
|
|
113
|
+
get loading() {
|
|
114
|
+
return ctx.loading
|
|
115
|
+
},
|
|
116
|
+
get disabled() {
|
|
117
|
+
return ctx.disabled
|
|
118
|
+
},
|
|
119
|
+
get dirty() {
|
|
120
|
+
return ctx.dirty
|
|
121
|
+
},
|
|
122
|
+
get dirtyFields() {
|
|
123
|
+
return ctx.dirtyFields as ReadonlySet<string>
|
|
124
|
+
},
|
|
125
|
+
get touchedFields() {
|
|
126
|
+
return ctx.touchedFields as ReadonlySet<string>
|
|
127
|
+
},
|
|
128
|
+
get blurredFields() {
|
|
129
|
+
return ctx.blurredFields as ReadonlySet<string>
|
|
130
|
+
},
|
|
131
|
+
get submitCount() {
|
|
132
|
+
return ctx.submitCount
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Sync the api object to the bindable prop once at setup time. `apiObject`
|
|
137
|
+
// is a stable reference built from getters, so there's nothing reactive to
|
|
138
|
+
// track — direct assignment is sufficient.
|
|
139
|
+
api = apiObject as typeof api
|
|
140
|
+
|
|
141
|
+
// Nested form attach/detach lifecycle.
|
|
142
|
+
onMount(() => {
|
|
143
|
+
if (parentCtx && nested) {
|
|
144
|
+
parentCtx.attachChild(formId, {
|
|
145
|
+
formId,
|
|
146
|
+
name: name as string | undefined,
|
|
147
|
+
validate: (opts) => ctx.validate(opts) as Promise<unknown | false>,
|
|
148
|
+
clear: (n) => ctx.clear(n),
|
|
149
|
+
reset: () => ctx.reset(),
|
|
150
|
+
setErrors: (errs, n) => ctx.setErrors(errs, n)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
onDestroy(() => {
|
|
156
|
+
if (parentCtx && nested) parentCtx.detachChild(formId)
|
|
157
|
+
ctx.dispose()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
async function handleSubmit(event: SubmitEvent) {
|
|
161
|
+
event.preventDefault()
|
|
162
|
+
// Forward the real SubmitEvent so user handlers can read `submitter`,
|
|
163
|
+
// `target`, etc. When `api.submit()` is called programmatically, ctx.submit
|
|
164
|
+
// synthesizes a fresh event instead.
|
|
165
|
+
await ctx.submit(event)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// `formVariants` has no variants (only a single `root` slot), so its
|
|
169
|
+
// output is stable — compute once at setup rather than via $derived.
|
|
170
|
+
const variantSlots = formVariants()
|
|
171
|
+
const classes = $derived({
|
|
172
|
+
root: variantSlots.root({
|
|
173
|
+
class: [config.slots.root, className, ui?.root]
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Slot props for children
|
|
178
|
+
const slotProps = $derived({
|
|
179
|
+
errors: ctx.errors as FormErrorWithId[],
|
|
180
|
+
loading: ctx.loading
|
|
181
|
+
})
|
|
182
|
+
</script>
|
|
183
|
+
|
|
184
|
+
{#if nested}
|
|
185
|
+
<div
|
|
186
|
+
{...restProps as unknown as HTMLAttributes<HTMLDivElement>}
|
|
187
|
+
bind:this={ref}
|
|
188
|
+
id={typeof id === 'number' ? String(id) : id}
|
|
189
|
+
class={classes.root}
|
|
190
|
+
>
|
|
191
|
+
{@render children?.(slotProps)}
|
|
192
|
+
</div>
|
|
193
|
+
{:else}
|
|
194
|
+
<form
|
|
195
|
+
{...restProps}
|
|
196
|
+
bind:this={ref}
|
|
197
|
+
id={typeof id === 'number' ? String(id) : id}
|
|
198
|
+
class={classes.root}
|
|
199
|
+
onsubmit={handleSubmit}
|
|
200
|
+
>
|
|
201
|
+
{@render children?.(slotProps)}
|
|
202
|
+
</form>
|
|
203
|
+
{/if}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FormProps, FormSchema } from './form.types.js';
|
|
2
|
+
export type Props<S extends FormSchema | undefined = FormSchema | undefined, T extends boolean = true, N extends boolean = false> = FormProps<S, T, N>;
|
|
3
|
+
declare function $$render<S extends FormSchema | undefined = FormSchema | undefined, T extends boolean = true, N extends boolean = false>(): {
|
|
4
|
+
props: Props<S, T, N>;
|
|
5
|
+
exports: {};
|
|
6
|
+
bindings: "ref" | "api" | "state";
|
|
7
|
+
slots: {};
|
|
8
|
+
events: {};
|
|
9
|
+
};
|
|
10
|
+
declare class __sveltets_Render<S extends FormSchema | undefined = FormSchema | undefined, T extends boolean = true, N extends boolean = false> {
|
|
11
|
+
props(): ReturnType<typeof $$render<S, T, N>>['props'];
|
|
12
|
+
events(): ReturnType<typeof $$render<S, T, N>>['events'];
|
|
13
|
+
slots(): ReturnType<typeof $$render<S, T, N>>['slots'];
|
|
14
|
+
bindings(): "ref" | "api" | "state";
|
|
15
|
+
exports(): {};
|
|
16
|
+
}
|
|
17
|
+
interface $$IsomorphicComponent {
|
|
18
|
+
new <S extends FormSchema | undefined = FormSchema | undefined, T extends boolean = true, N extends boolean = false>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<S, T, N>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<S, T, N>['props']>, ReturnType<__sveltets_Render<S, T, N>['events']>, ReturnType<__sveltets_Render<S, T, N>['slots']>> & {
|
|
19
|
+
$$bindings?: ReturnType<__sveltets_Render<S, T, N>['bindings']>;
|
|
20
|
+
} & ReturnType<__sveltets_Render<S, T, N>['exports']>;
|
|
21
|
+
<S extends FormSchema | undefined = FormSchema | undefined, T extends boolean = true, N extends boolean = false>(internal: unknown, props: ReturnType<__sveltets_Render<S, T, N>['props']> & {}): ReturnType<__sveltets_Render<S, T, N>['exports']>;
|
|
22
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any, any, any>['bindings']>;
|
|
23
|
+
}
|
|
24
|
+
declare const Form: $$IsomorphicComponent;
|
|
25
|
+
type Form<S extends FormSchema | undefined = FormSchema | undefined, T extends boolean = true, N extends boolean = false> = InstanceType<typeof Form<S, T, N>>;
|
|
26
|
+
export default Form;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
2
|
+
import { type FormError, type FormErrorWithId, type FormErrorEvent, type FormFieldRegistryEntry, type FormInputEvents, type FormSchema, type FormSubmitEvent, type FormValidateOpts, type NestedFormEntry } from './form.types.js';
|
|
3
|
+
export declare const FORM_CONTEXT_KEY: unique symbol;
|
|
4
|
+
export declare function getFormContext<T = unknown>(): FormContext<T> | undefined;
|
|
5
|
+
export interface FormContextOptions<T> {
|
|
6
|
+
getState: () => T;
|
|
7
|
+
getSchema: () => FormSchema | undefined;
|
|
8
|
+
getCustomValidate: () => ((state: T) => FormError[] | Promise<FormError[]>) | undefined;
|
|
9
|
+
getValidateOn: () => FormInputEvents[];
|
|
10
|
+
getValidateOnInputDelay: () => number;
|
|
11
|
+
getDisabled: () => boolean;
|
|
12
|
+
getLoadingAuto: () => boolean;
|
|
13
|
+
getTransform: () => boolean;
|
|
14
|
+
getName: () => string | undefined;
|
|
15
|
+
getOnSubmit: () => ((event: FormSubmitEvent<unknown>) => void | Promise<void>) | undefined;
|
|
16
|
+
getOnError: () => ((event: FormErrorEvent) => void) | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare class FormContext<T = unknown> {
|
|
19
|
+
#private;
|
|
20
|
+
errors: FormErrorWithId[];
|
|
21
|
+
loading: boolean;
|
|
22
|
+
submitCount: number;
|
|
23
|
+
dirtyFields: SvelteSet<string>;
|
|
24
|
+
touchedFields: SvelteSet<string>;
|
|
25
|
+
blurredFields: SvelteSet<string>;
|
|
26
|
+
formId: string | number;
|
|
27
|
+
constructor(opts: FormContextOptions<T>, formId: string | number, parentCtx?: FormContext);
|
|
28
|
+
get dirty(): boolean;
|
|
29
|
+
get disabled(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Current state. For nested forms, read the sub-tree from parent state by name.
|
|
32
|
+
* Falls back to `{}` if the parent hasn't initialized the sub-object yet —
|
|
33
|
+
* avoids crashing downstream validators when consumers forget to prefill it.
|
|
34
|
+
*/
|
|
35
|
+
get state(): T;
|
|
36
|
+
getErrors(name?: string | RegExp): FormErrorWithId[];
|
|
37
|
+
setErrors(errs: FormError[], name?: string | RegExp): void;
|
|
38
|
+
clear(name?: string | RegExp): void;
|
|
39
|
+
/**
|
|
40
|
+
* Reset all form tracking state: errors, dirty/touched/blurred field sets,
|
|
41
|
+
* and submitCount. Does NOT modify `state` (caller owns that). Cascades to
|
|
42
|
+
* nested forms.
|
|
43
|
+
*/
|
|
44
|
+
reset(): void;
|
|
45
|
+
registerField(name: string, entry: FormFieldRegistryEntry): void;
|
|
46
|
+
unregisterField(name: string): void;
|
|
47
|
+
attachChild(childFormId: string | number, entry: NestedFormEntry): void;
|
|
48
|
+
detachChild(childFormId: string | number): void;
|
|
49
|
+
onFocus(name: string): void;
|
|
50
|
+
onBlur(name: string): void;
|
|
51
|
+
onChange(name: string): void;
|
|
52
|
+
onInput(name: string, eager?: boolean): void;
|
|
53
|
+
validate(opts?: FormValidateOpts): Promise<T | false>;
|
|
54
|
+
/**
|
|
55
|
+
* Run full validation and fire onsubmit / onerror.
|
|
56
|
+
*
|
|
57
|
+
* @param sourceEvent - Real SubmitEvent from the `<form>` submit handler,
|
|
58
|
+
* forwarded to the user's onsubmit / onerror so they can read `submitter`,
|
|
59
|
+
* `preventDefault`, etc. When called programmatically (via the public API),
|
|
60
|
+
* a fresh SubmitEvent is synthesized.
|
|
61
|
+
*/
|
|
62
|
+
submit(sourceEvent?: SubmitEvent): Promise<void>;
|
|
63
|
+
dispose(): void;
|
|
64
|
+
}
|