@privaty/ui-forms 0.1.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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +107 -0
- package/dist/components/form-error.svelte +49 -0
- package/dist/components/form-error.svelte.d.ts +15 -0
- package/dist/components/reset.svelte +49 -0
- package/dist/components/reset.svelte.d.ts +19 -0
- package/dist/components/submit.svelte +82 -0
- package/dist/components/submit.svelte.d.ts +27 -0
- package/dist/context.d.ts +23 -0
- package/dist/context.js +21 -0
- package/dist/form-state.svelte.d.ts +89 -0
- package/dist/form-state.svelte.js +139 -0
- package/dist/form.svelte +219 -0
- package/dist/form.svelte.d.ts +62 -0
- package/dist/inputs/checkbox-input.svelte +119 -0
- package/dist/inputs/checkbox-input.svelte.d.ts +45 -0
- package/dist/inputs/date-input.svelte +132 -0
- package/dist/inputs/date-input.svelte.d.ts +61 -0
- package/dist/inputs/number-input.svelte +141 -0
- package/dist/inputs/number-input.svelte.d.ts +63 -0
- package/dist/inputs/select-input.svelte +156 -0
- package/dist/inputs/select-input.svelte.d.ts +57 -0
- package/dist/inputs/text-input.svelte +126 -0
- package/dist/inputs/text-input.svelte.d.ts +59 -0
- package/dist/inputs/textarea-input.svelte +135 -0
- package/dist/inputs/textarea-input.svelte.d.ts +57 -0
- package/dist/inputs/wire-field.d.ts +44 -0
- package/dist/inputs/wire-field.js +45 -0
- package/dist/testing/fakes.svelte.d.ts +205 -0
- package/dist/testing/fakes.svelte.js +346 -0
- package/dist/types/field.d.ts +156 -0
- package/dist/types/field.js +1 -0
- package/dist/types/form.d.ts +27 -0
- package/dist/types/form.js +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Text-family input (text, email, password, search, url, tel) wired to a
|
|
3
|
+
SvelteKit remote form string field — must render inside a <Form>, whose
|
|
4
|
+
context it registers with. Shows resolver-translated validation errors once
|
|
5
|
+
the form state reveals them, plus a majority-aware required/optional marker,
|
|
6
|
+
and turns readonly while the form submits.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { cn } from "@privaty/ui/cn.js";
|
|
10
|
+
import Input from "@privaty/ui/components/input.svelte";
|
|
11
|
+
import type { LabelStyle } from "@privaty/ui/components/types.js";
|
|
12
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
13
|
+
import type { TextField, TextFieldType } from "../types/field";
|
|
14
|
+
import { wireField } from "./wire-field";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
18
|
+
* satisfying the TextField slice works, e.g. a fake from the public
|
|
19
|
+
* testing subpath. */
|
|
20
|
+
field: TextField;
|
|
21
|
+
/** Visible label text. In the floating label style it doubles as the
|
|
22
|
+
* placeholder. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Native input type within the text family. Defaults to "text". */
|
|
25
|
+
type?: TextFieldType;
|
|
26
|
+
|
|
27
|
+
/** Label placement: "top" (default), "left", "floating", or "hidden"
|
|
28
|
+
* (visually hidden, still read by screen readers). */
|
|
29
|
+
labelStyle?: LabelStyle;
|
|
30
|
+
|
|
31
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
32
|
+
* marker (required markers styled red) — forms where at least half the
|
|
33
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
34
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
35
|
+
* as a native `required` attribute. */
|
|
36
|
+
required?: boolean;
|
|
37
|
+
/** Disables the control. Disabled controls are excluded from FormData —
|
|
38
|
+
* never disable to lock the form during submit; the input already turns
|
|
39
|
+
* readonly while submitting. */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/** Renders the input readonly. Also forced on while the form is
|
|
42
|
+
* submitting, independent of this prop. */
|
|
43
|
+
readonly?: boolean;
|
|
44
|
+
|
|
45
|
+
/** Seed handed to Kit's `as()` (defaults to "") — the value native form
|
|
46
|
+
* reset restores and the baseline for dirty tracking. Assumed stable for
|
|
47
|
+
* the component's lifetime. */
|
|
48
|
+
initialValue?: string;
|
|
49
|
+
|
|
50
|
+
/** Placeholder text — ignored in the floating label style, where the
|
|
51
|
+
* label plays that role. */
|
|
52
|
+
placeholder?: string;
|
|
53
|
+
/** Forwarded to the native input. */
|
|
54
|
+
autocomplete?: HTMLInputAttributes["autocomplete"];
|
|
55
|
+
|
|
56
|
+
/** Extra classes for the outer field wrapper. */
|
|
57
|
+
class?: string;
|
|
58
|
+
/** Extra classes for the <label> element. */
|
|
59
|
+
labelClass?: string;
|
|
60
|
+
/** Extra classes for the <input> element. */
|
|
61
|
+
inputClass?: string;
|
|
62
|
+
/** Extra classes for the required/optional marker. */
|
|
63
|
+
markerClass?: string;
|
|
64
|
+
/** Extra classes for the error <ul>. */
|
|
65
|
+
errorClass?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const {
|
|
69
|
+
field,
|
|
70
|
+
label,
|
|
71
|
+
type = "text",
|
|
72
|
+
|
|
73
|
+
labelStyle,
|
|
74
|
+
|
|
75
|
+
required = false,
|
|
76
|
+
disabled = false,
|
|
77
|
+
readonly = false,
|
|
78
|
+
|
|
79
|
+
initialValue = "",
|
|
80
|
+
|
|
81
|
+
placeholder,
|
|
82
|
+
autocomplete,
|
|
83
|
+
|
|
84
|
+
class: classes,
|
|
85
|
+
labelClass,
|
|
86
|
+
inputClass,
|
|
87
|
+
markerClass,
|
|
88
|
+
errorClass,
|
|
89
|
+
}: Props = $props();
|
|
90
|
+
|
|
91
|
+
const attributes = $derived(field.as(type, initialValue));
|
|
92
|
+
|
|
93
|
+
// The field, type, and initialValue are stable for the component's lifetime,
|
|
94
|
+
// so capturing the initial name and registration is intentional.
|
|
95
|
+
// svelte-ignore state_referenced_locally
|
|
96
|
+
const name = attributes.name;
|
|
97
|
+
|
|
98
|
+
// svelte-ignore state_referenced_locally
|
|
99
|
+
const wired = wireField({
|
|
100
|
+
name,
|
|
101
|
+
initialValue,
|
|
102
|
+
required,
|
|
103
|
+
issues: () => field.issues(),
|
|
104
|
+
getValue: () => field.value(),
|
|
105
|
+
setValue: (value) => field.set(value as string),
|
|
106
|
+
normalize: (value) => (value == null ? "" : String(value)),
|
|
107
|
+
});
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<Input
|
|
111
|
+
{...attributes}
|
|
112
|
+
{label}
|
|
113
|
+
{labelStyle}
|
|
114
|
+
errors={wired.errors}
|
|
115
|
+
marker={wired.marker}
|
|
116
|
+
aria-invalid={wired.errors.length > 0 ? true : undefined}
|
|
117
|
+
{disabled}
|
|
118
|
+
readonly={readonly || wired.state.isSubmitting}
|
|
119
|
+
{placeholder}
|
|
120
|
+
{autocomplete}
|
|
121
|
+
class={classes}
|
|
122
|
+
{labelClass}
|
|
123
|
+
{inputClass}
|
|
124
|
+
markerClass={cn(required && "text-red-700 dark:text-red-500", markerClass)}
|
|
125
|
+
{errorClass}
|
|
126
|
+
/>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { LabelStyle } from "@privaty/ui/components/types.js";
|
|
2
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
3
|
+
import type { TextField, TextFieldType } from "../types/field";
|
|
4
|
+
interface Props {
|
|
5
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
6
|
+
* satisfying the TextField slice works, e.g. a fake from the public
|
|
7
|
+
* testing subpath. */
|
|
8
|
+
field: TextField;
|
|
9
|
+
/** Visible label text. In the floating label style it doubles as the
|
|
10
|
+
* placeholder. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Native input type within the text family. Defaults to "text". */
|
|
13
|
+
type?: TextFieldType;
|
|
14
|
+
/** Label placement: "top" (default), "left", "floating", or "hidden"
|
|
15
|
+
* (visually hidden, still read by screen readers). */
|
|
16
|
+
labelStyle?: LabelStyle;
|
|
17
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
18
|
+
* marker (required markers styled red) — forms where at least half the
|
|
19
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
20
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
21
|
+
* as a native `required` attribute. */
|
|
22
|
+
required?: boolean;
|
|
23
|
+
/** Disables the control. Disabled controls are excluded from FormData —
|
|
24
|
+
* never disable to lock the form during submit; the input already turns
|
|
25
|
+
* readonly while submitting. */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Renders the input readonly. Also forced on while the form is
|
|
28
|
+
* submitting, independent of this prop. */
|
|
29
|
+
readonly?: boolean;
|
|
30
|
+
/** Seed handed to Kit's `as()` (defaults to "") — the value native form
|
|
31
|
+
* reset restores and the baseline for dirty tracking. Assumed stable for
|
|
32
|
+
* the component's lifetime. */
|
|
33
|
+
initialValue?: string;
|
|
34
|
+
/** Placeholder text — ignored in the floating label style, where the
|
|
35
|
+
* label plays that role. */
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/** Forwarded to the native input. */
|
|
38
|
+
autocomplete?: HTMLInputAttributes["autocomplete"];
|
|
39
|
+
/** Extra classes for the outer field wrapper. */
|
|
40
|
+
class?: string;
|
|
41
|
+
/** Extra classes for the <label> element. */
|
|
42
|
+
labelClass?: string;
|
|
43
|
+
/** Extra classes for the <input> element. */
|
|
44
|
+
inputClass?: string;
|
|
45
|
+
/** Extra classes for the required/optional marker. */
|
|
46
|
+
markerClass?: string;
|
|
47
|
+
/** Extra classes for the error <ul>. */
|
|
48
|
+
errorClass?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Text-family input (text, email, password, search, url, tel) wired to a
|
|
52
|
+
* SvelteKit remote form string field — must render inside a <Form>, whose
|
|
53
|
+
* context it registers with. Shows resolver-translated validation errors once
|
|
54
|
+
* the form state reveals them, plus a majority-aware required/optional marker,
|
|
55
|
+
* and turns readonly while the form submits.
|
|
56
|
+
*/
|
|
57
|
+
declare const TextInput: import("svelte").Component<Props, {}, "">;
|
|
58
|
+
type TextInput = ReturnType<typeof TextInput>;
|
|
59
|
+
export default TextInput;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Multi-line text input wired to a SvelteKit remote form string field, rendered
|
|
3
|
+
as a textarea — must render inside a <Form>, whose context it registers with.
|
|
4
|
+
Shows resolver-translated validation errors once the form state reveals them,
|
|
5
|
+
plus a majority-aware required/optional marker, and turns readonly while the
|
|
6
|
+
form submits. No floating label style — that is tuned to single-line inputs.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { cn } from "@privaty/ui/cn.js";
|
|
10
|
+
import Textarea from "@privaty/ui/components/textarea.svelte";
|
|
11
|
+
import type { LabelStyle } from "@privaty/ui/components/types.js";
|
|
12
|
+
import type { HTMLTextareaAttributes } from "svelte/elements";
|
|
13
|
+
import type { TextField } from "../types/field";
|
|
14
|
+
import { wireField } from "./wire-field";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
18
|
+
* satisfying the TextField slice works, e.g. a fake from the public
|
|
19
|
+
* testing subpath. */
|
|
20
|
+
field: TextField;
|
|
21
|
+
/** Visible label text for the control. */
|
|
22
|
+
label: string;
|
|
23
|
+
|
|
24
|
+
/** Label placement: "top" (default), "left", or "hidden" (visually
|
|
25
|
+
* hidden, still read by screen readers). */
|
|
26
|
+
labelStyle?: Exclude<LabelStyle, "floating">;
|
|
27
|
+
|
|
28
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
29
|
+
* marker (required markers styled red) — forms where at least half the
|
|
30
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
31
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
32
|
+
* as a native `required` attribute. */
|
|
33
|
+
required?: boolean;
|
|
34
|
+
/** Disables the control. Disabled controls are excluded from FormData —
|
|
35
|
+
* never disable to lock the form during submit; the textarea already
|
|
36
|
+
* turns readonly while submitting. */
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Renders the textarea readonly. Also forced on while the form is
|
|
39
|
+
* submitting, independent of this prop. */
|
|
40
|
+
readonly?: boolean;
|
|
41
|
+
|
|
42
|
+
/** Seed handed to Kit's `as()` (defaults to "") — the value native form
|
|
43
|
+
* reset restores and the baseline for dirty tracking. Assumed stable for
|
|
44
|
+
* the component's lifetime. */
|
|
45
|
+
initialValue?: string;
|
|
46
|
+
|
|
47
|
+
/** Forwarded to the native textarea. */
|
|
48
|
+
placeholder?: string;
|
|
49
|
+
/** Forwarded to the native textarea. */
|
|
50
|
+
autocomplete?: HTMLTextareaAttributes["autocomplete"];
|
|
51
|
+
/** Visible line count (native `rows`). Defaults to 3. */
|
|
52
|
+
rows?: number;
|
|
53
|
+
|
|
54
|
+
/** Extra classes for the outer field wrapper. */
|
|
55
|
+
class?: string;
|
|
56
|
+
/** Extra classes for the <label> element. */
|
|
57
|
+
labelClass?: string;
|
|
58
|
+
/** Extra classes for the <textarea> element. */
|
|
59
|
+
textareaClass?: string;
|
|
60
|
+
/** Extra classes for the required/optional marker. */
|
|
61
|
+
markerClass?: string;
|
|
62
|
+
/** Extra classes for the error <ul>. */
|
|
63
|
+
errorClass?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
field,
|
|
68
|
+
label,
|
|
69
|
+
|
|
70
|
+
labelStyle,
|
|
71
|
+
|
|
72
|
+
required = false,
|
|
73
|
+
disabled = false,
|
|
74
|
+
readonly = false,
|
|
75
|
+
|
|
76
|
+
initialValue = "",
|
|
77
|
+
|
|
78
|
+
placeholder,
|
|
79
|
+
autocomplete,
|
|
80
|
+
rows = 3,
|
|
81
|
+
|
|
82
|
+
class: classes,
|
|
83
|
+
labelClass,
|
|
84
|
+
textareaClass,
|
|
85
|
+
markerClass,
|
|
86
|
+
errorClass,
|
|
87
|
+
}: Props = $props();
|
|
88
|
+
|
|
89
|
+
// Kit has no textarea `as()` type — but `as("text")` returns exactly what
|
|
90
|
+
// a textarea needs (name, aria-invalid, the value accessors) and its text
|
|
91
|
+
// branch carries no `type` attribute to clash with the element.
|
|
92
|
+
const attributes = $derived(field.as("text", initialValue));
|
|
93
|
+
|
|
94
|
+
// The slice TYPES the return broadly as input attributes, whose event-
|
|
95
|
+
// handler element types clash with a textarea — at runtime the object only
|
|
96
|
+
// holds the textarea-compatible members above, so the spread is cast.
|
|
97
|
+
const textareaAttributes = $derived(
|
|
98
|
+
attributes as unknown as Omit<HTMLTextareaAttributes, "class">,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// The field and initialValue are stable for the component's lifetime, so
|
|
102
|
+
// capturing the initial name and registration is intentional.
|
|
103
|
+
// svelte-ignore state_referenced_locally
|
|
104
|
+
const name = attributes.name;
|
|
105
|
+
|
|
106
|
+
// svelte-ignore state_referenced_locally
|
|
107
|
+
const wired = wireField({
|
|
108
|
+
name,
|
|
109
|
+
initialValue,
|
|
110
|
+
required,
|
|
111
|
+
issues: () => field.issues(),
|
|
112
|
+
getValue: () => field.value(),
|
|
113
|
+
setValue: (value) => field.set(value as string),
|
|
114
|
+
normalize: (value) => (value == null ? "" : String(value)),
|
|
115
|
+
});
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<Textarea
|
|
119
|
+
{...textareaAttributes}
|
|
120
|
+
{label}
|
|
121
|
+
{labelStyle}
|
|
122
|
+
errors={wired.errors}
|
|
123
|
+
marker={wired.marker}
|
|
124
|
+
aria-invalid={wired.errors.length > 0 ? true : undefined}
|
|
125
|
+
{disabled}
|
|
126
|
+
readonly={readonly || wired.state.isSubmitting}
|
|
127
|
+
{placeholder}
|
|
128
|
+
{autocomplete}
|
|
129
|
+
{rows}
|
|
130
|
+
class={classes}
|
|
131
|
+
{labelClass}
|
|
132
|
+
{textareaClass}
|
|
133
|
+
markerClass={cn(required && "text-red-700 dark:text-red-500", markerClass)}
|
|
134
|
+
{errorClass}
|
|
135
|
+
/>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { LabelStyle } from "@privaty/ui/components/types.js";
|
|
2
|
+
import type { HTMLTextareaAttributes } from "svelte/elements";
|
|
3
|
+
import type { TextField } from "../types/field";
|
|
4
|
+
interface Props {
|
|
5
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
6
|
+
* satisfying the TextField slice works, e.g. a fake from the public
|
|
7
|
+
* testing subpath. */
|
|
8
|
+
field: TextField;
|
|
9
|
+
/** Visible label text for the control. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Label placement: "top" (default), "left", or "hidden" (visually
|
|
12
|
+
* hidden, still read by screen readers). */
|
|
13
|
+
labelStyle?: Exclude<LabelStyle, "floating">;
|
|
14
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
15
|
+
* marker (required markers styled red) — forms where at least half the
|
|
16
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
17
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
18
|
+
* as a native `required` attribute. */
|
|
19
|
+
required?: boolean;
|
|
20
|
+
/** Disables the control. Disabled controls are excluded from FormData —
|
|
21
|
+
* never disable to lock the form during submit; the textarea already
|
|
22
|
+
* turns readonly while submitting. */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Renders the textarea readonly. Also forced on while the form is
|
|
25
|
+
* submitting, independent of this prop. */
|
|
26
|
+
readonly?: boolean;
|
|
27
|
+
/** Seed handed to Kit's `as()` (defaults to "") — the value native form
|
|
28
|
+
* reset restores and the baseline for dirty tracking. Assumed stable for
|
|
29
|
+
* the component's lifetime. */
|
|
30
|
+
initialValue?: string;
|
|
31
|
+
/** Forwarded to the native textarea. */
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
/** Forwarded to the native textarea. */
|
|
34
|
+
autocomplete?: HTMLTextareaAttributes["autocomplete"];
|
|
35
|
+
/** Visible line count (native `rows`). Defaults to 3. */
|
|
36
|
+
rows?: number;
|
|
37
|
+
/** Extra classes for the outer field wrapper. */
|
|
38
|
+
class?: string;
|
|
39
|
+
/** Extra classes for the <label> element. */
|
|
40
|
+
labelClass?: string;
|
|
41
|
+
/** Extra classes for the <textarea> element. */
|
|
42
|
+
textareaClass?: string;
|
|
43
|
+
/** Extra classes for the required/optional marker. */
|
|
44
|
+
markerClass?: string;
|
|
45
|
+
/** Extra classes for the error <ul>. */
|
|
46
|
+
errorClass?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Multi-line text input wired to a SvelteKit remote form string field, rendered
|
|
50
|
+
* as a textarea — must render inside a <Form>, whose context it registers with.
|
|
51
|
+
* Shows resolver-translated validation errors once the form state reveals them,
|
|
52
|
+
* plus a majority-aware required/optional marker, and turns readonly while the
|
|
53
|
+
* form submits. No floating label style — that is tuned to single-line inputs.
|
|
54
|
+
*/
|
|
55
|
+
declare const TextareaInput: import("svelte").Component<Props, {}, "">;
|
|
56
|
+
type TextareaInput = ReturnType<typeof TextareaInput>;
|
|
57
|
+
export default TextareaInput;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { FormState } from "../form-state.svelte";
|
|
3
|
+
interface WireFieldOptions {
|
|
4
|
+
/** The field's form name (from the attributes Kit's `as(...)` returned).
|
|
5
|
+
* Must be unique within the form. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** The typed seed value — the baseline for dirty comparison and what
|
|
8
|
+
* reset restores. */
|
|
9
|
+
initialValue: unknown;
|
|
10
|
+
/** Whether the field is required — feeds the majority-aware marker. */
|
|
11
|
+
required: boolean;
|
|
12
|
+
/** Returns the field's current issues (the field's `issues()`). */
|
|
13
|
+
issues: () => readonly StandardSchemaV1.Issue[] | undefined;
|
|
14
|
+
/** Reads the field's current value — raw DOM strings can appear mid-edit. */
|
|
15
|
+
getValue: () => unknown;
|
|
16
|
+
/** Writes a value back to the field — used by the form-level reset. */
|
|
17
|
+
setValue: (value: unknown) => void;
|
|
18
|
+
/** Maps raw field values onto the initialValue's domain before dirty
|
|
19
|
+
* comparison (e.g. "5" vs 5, "on" vs true). */
|
|
20
|
+
normalize: (value: unknown) => unknown;
|
|
21
|
+
}
|
|
22
|
+
interface WiredField {
|
|
23
|
+
/** The owning form's FormState — inputs read e.g. isSubmitting from it. */
|
|
24
|
+
state: FormState;
|
|
25
|
+
/** Resolver-translated issue messages, or empty while the field's issues
|
|
26
|
+
* are gated (not yet touched and no submit attempted). Recomputed on
|
|
27
|
+
* access, so template reads stay reactive. */
|
|
28
|
+
readonly errors: string[];
|
|
29
|
+
/** The required/optional label text for the field's marker, or undefined —
|
|
30
|
+
* only the form's minority kind is marked, and nothing renders until every
|
|
31
|
+
* field has registered. */
|
|
32
|
+
readonly marker: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The wiring every form input shares: registers the field with the FormState
|
|
36
|
+
* (unregistering on destroy), and exposes the gated, resolver-translated
|
|
37
|
+
* errors plus the majority-aware required/optional marker.
|
|
38
|
+
*
|
|
39
|
+
* Must be called during component init (it uses context and onDestroy). The
|
|
40
|
+
* getters recompute on access, so reads from a template stay reactive.
|
|
41
|
+
*/
|
|
42
|
+
declare function wireField(options: WireFieldOptions): WiredField;
|
|
43
|
+
export { wireField };
|
|
44
|
+
export type { WiredField, WireFieldOptions };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getUiConfig } from "@privaty/ui/config/context.js";
|
|
2
|
+
import { onDestroy } from "svelte";
|
|
3
|
+
import { getFormContext } from "../context";
|
|
4
|
+
/**
|
|
5
|
+
* The wiring every form input shares: registers the field with the FormState
|
|
6
|
+
* (unregistering on destroy), and exposes the gated, resolver-translated
|
|
7
|
+
* errors plus the majority-aware required/optional marker.
|
|
8
|
+
*
|
|
9
|
+
* Must be called during component init (it uses context and onDestroy). The
|
|
10
|
+
* getters recompute on access, so reads from a template stay reactive.
|
|
11
|
+
*/
|
|
12
|
+
function wireField(options) {
|
|
13
|
+
const { state } = getFormContext();
|
|
14
|
+
const config = getUiConfig();
|
|
15
|
+
onDestroy(state.register({
|
|
16
|
+
name: options.name,
|
|
17
|
+
initialValue: options.initialValue,
|
|
18
|
+
required: options.required,
|
|
19
|
+
getValue: options.getValue,
|
|
20
|
+
setValue: options.setValue,
|
|
21
|
+
normalize: options.normalize,
|
|
22
|
+
}));
|
|
23
|
+
return {
|
|
24
|
+
state,
|
|
25
|
+
get errors() {
|
|
26
|
+
return state.shouldShowIssues(options.name)
|
|
27
|
+
? (options.issues() ?? []).map((issue) => config.resolveMessage(issue))
|
|
28
|
+
: [];
|
|
29
|
+
},
|
|
30
|
+
get marker() {
|
|
31
|
+
// Markers wait until every field has registered — a partial majority
|
|
32
|
+
// would render the wrong marker first (visible as an SSR/load flash).
|
|
33
|
+
if (!state.settled)
|
|
34
|
+
return undefined;
|
|
35
|
+
return state.majorityRequired
|
|
36
|
+
? options.required
|
|
37
|
+
? undefined
|
|
38
|
+
: config.labels.form.optional
|
|
39
|
+
: options.required
|
|
40
|
+
? config.labels.form.required
|
|
41
|
+
: undefined;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export { wireField };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { CheckboxField, DateField, NumberField, SelectField, TextField } from "../types/field";
|
|
2
|
+
import type { ValidatableForm } from "../types/form";
|
|
3
|
+
/**
|
|
4
|
+
* Test doubles for the structural form interfaces. For use in specs and
|
|
5
|
+
* consumers' own tests — never imported by library runtime code.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The minimal issue shape the library reads back from a form — a Standard
|
|
9
|
+
* Schema issue narrowed to what tests need to construct.
|
|
10
|
+
*/
|
|
11
|
+
interface FakeIssue {
|
|
12
|
+
/** Human-readable validation message. */
|
|
13
|
+
message: string;
|
|
14
|
+
/** Path segments locating the field the issue belongs to (Standard Schema
|
|
15
|
+
* shape). Omit for form-level issues. */
|
|
16
|
+
path?: readonly (string | number)[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Minimal `ValidatableForm` fake for FormState-level tests. `computeIssues`
|
|
20
|
+
* runs on every `validate()` call and its result becomes the issue set that
|
|
21
|
+
* `fields.allIssues()` exposes (default: always valid). State is reactive —
|
|
22
|
+
* effects re-run on `setIssues`/`setPending`.
|
|
23
|
+
*/
|
|
24
|
+
declare function fakeForm(computeIssues?: () => readonly FakeIssue[] | undefined): {
|
|
25
|
+
/** The fake form — pass it where a `ValidatableForm` is expected. */
|
|
26
|
+
form: ValidatableForm;
|
|
27
|
+
/** Arguments of every `validate()` call, in order. */
|
|
28
|
+
validateCalls: unknown[];
|
|
29
|
+
/** Replaces the issue set directly, bypassing `validate()`. */
|
|
30
|
+
setIssues: (next: readonly FakeIssue[] | undefined) => void;
|
|
31
|
+
/** Sets the form's `pending` submission count. */
|
|
32
|
+
setPending: (next: number) => void;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Structural `TextField` fake for TextInput/TextareaInput tests. `as()`
|
|
36
|
+
* returns plain spreadable attributes (no attachment); value and issues are
|
|
37
|
+
* reactive state. Pass `options.issues` to seed the field with issues.
|
|
38
|
+
*/
|
|
39
|
+
declare function fakeTextField(name: string, options?: {
|
|
40
|
+
issues?: readonly {
|
|
41
|
+
message: string;
|
|
42
|
+
}[];
|
|
43
|
+
}): {
|
|
44
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
45
|
+
field: TextField;
|
|
46
|
+
/** Simulates USER typing (string fields store the same value `set()`
|
|
47
|
+
* would — no raw/typed split here). */
|
|
48
|
+
edit: (next: string) => void;
|
|
49
|
+
/** Replaces the field's issue set. */
|
|
50
|
+
setIssues: (next: readonly {
|
|
51
|
+
message: string;
|
|
52
|
+
}[] | undefined) => void;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Structural `DateField` fake for DateInput tests. The whole date family
|
|
56
|
+
* (date, month, week, time, datetime-local) carries ISO-style string values,
|
|
57
|
+
* so one fake covers all five. Same shape and reactivity as `fakeTextField`.
|
|
58
|
+
*/
|
|
59
|
+
declare function fakeDateField(name: string, options?: {
|
|
60
|
+
issues?: readonly {
|
|
61
|
+
message: string;
|
|
62
|
+
}[];
|
|
63
|
+
}): {
|
|
64
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
65
|
+
field: DateField;
|
|
66
|
+
/** Simulates USER input — an ISO-style string, same as `set()` would
|
|
67
|
+
* store. */
|
|
68
|
+
edit: (next: string) => void;
|
|
69
|
+
/** Replaces the field's issue set. */
|
|
70
|
+
setIssues: (next: readonly {
|
|
71
|
+
message: string;
|
|
72
|
+
}[] | undefined) => void;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Structural `NumberField` fake for NumberInput tests. Mirrors Kit's mid-edit
|
|
76
|
+
* behavior: `edit()` stores the raw DOM string a user's typing would produce,
|
|
77
|
+
* while `set()` stores the typed number — the distinction is what makes
|
|
78
|
+
* dirty-tracking tests meaningful.
|
|
79
|
+
*/
|
|
80
|
+
declare function fakeNumberField(name: string, options?: {
|
|
81
|
+
issues?: readonly {
|
|
82
|
+
message: string;
|
|
83
|
+
}[];
|
|
84
|
+
}): {
|
|
85
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
86
|
+
field: NumberField;
|
|
87
|
+
/** Simulates USER typing: stores the raw DOM string, like Kit does. */
|
|
88
|
+
edit: (next: number | undefined) => void;
|
|
89
|
+
/** Replaces the field's issue set. */
|
|
90
|
+
setIssues: (next: readonly {
|
|
91
|
+
message: string;
|
|
92
|
+
}[] | undefined) => void;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Structural `SelectField` fake for SelectInput tests. Same shape and
|
|
96
|
+
* reactivity as `fakeTextField` — select values are plain strings on both
|
|
97
|
+
* the DOM and typed sides.
|
|
98
|
+
*/
|
|
99
|
+
declare function fakeSelectField(name: string, options?: {
|
|
100
|
+
issues?: readonly {
|
|
101
|
+
message: string;
|
|
102
|
+
}[];
|
|
103
|
+
}): {
|
|
104
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
105
|
+
field: SelectField;
|
|
106
|
+
/** Simulates the USER choosing an option: stores its string value. */
|
|
107
|
+
edit: (next: string) => void;
|
|
108
|
+
/** Replaces the field's issue set. */
|
|
109
|
+
setIssues: (next: readonly {
|
|
110
|
+
message: string;
|
|
111
|
+
}[] | undefined) => void;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Structural `CheckboxField` fake for CheckboxInput tests. Like Kit's
|
|
115
|
+
* `as("checkbox", seed)`, the returned attributes expose `checked` and
|
|
116
|
+
* `defaultChecked` getters (the latter is what makes native reset restore
|
|
117
|
+
* the seed). `edit()` stores the raw DOM value ("on"/null) while `set()`
|
|
118
|
+
* stores the typed boolean — mirroring Kit's mid-edit behavior.
|
|
119
|
+
*/
|
|
120
|
+
declare function fakeCheckboxField(name: string, options?: {
|
|
121
|
+
issues?: readonly {
|
|
122
|
+
message: string;
|
|
123
|
+
}[];
|
|
124
|
+
}): {
|
|
125
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
126
|
+
field: CheckboxField;
|
|
127
|
+
/** Simulates a USER toggle: raw DOM value, like Kit's input listener. */
|
|
128
|
+
edit: (next: boolean) => void;
|
|
129
|
+
/** Replaces the field's issue set. */
|
|
130
|
+
setIssues: (next: readonly {
|
|
131
|
+
message: string;
|
|
132
|
+
}[] | undefined) => void;
|
|
133
|
+
};
|
|
134
|
+
/** The options object Kit's `validate()` accepts, as the fakes model it. */
|
|
135
|
+
interface FakeValidateOptions {
|
|
136
|
+
/** Kit's flag to also surface issues on fields not yet edited and blurred
|
|
137
|
+
* (ignored after first submission). The fake attaches no behavior to it —
|
|
138
|
+
* it is only recorded in `validateCalls` and forwarded to `onValidate`. */
|
|
139
|
+
all?: boolean;
|
|
140
|
+
/** True for client-schema-only validation with no server round-trip. In
|
|
141
|
+
* the fake it decides issue origin (preflight issues are client-flagged,
|
|
142
|
+
* full validations server-flagged) and the merge-vs-replace semantics of
|
|
143
|
+
* the resulting issue set. */
|
|
144
|
+
preflightOnly?: boolean;
|
|
145
|
+
}
|
|
146
|
+
/** Behavior knobs for `fakeRemoteForm`. */
|
|
147
|
+
interface FakeRemoteFormOptions {
|
|
148
|
+
/** Issues the next validation resolves with (default: none — valid). */
|
|
149
|
+
onValidate?: (options: FakeValidateOptions | undefined) => readonly FakeIssue[] | undefined;
|
|
150
|
+
/** Submission outcome: return false for "rejected by validation", throw for
|
|
151
|
+
* a failed request (default: true — success). May return a promise. */
|
|
152
|
+
onSubmit?: () => boolean | Promise<boolean>;
|
|
153
|
+
/** Issues installed — server-flagged, replacing the whole set — when
|
|
154
|
+
* onSubmit rejects, like Kit's rejected submissions do. Left unset, a
|
|
155
|
+
* rejection leaves the issue set untouched. */
|
|
156
|
+
serverIssues?: readonly FakeIssue[];
|
|
157
|
+
/** Hold every validate() unresolved until releaseValidate() is called. */
|
|
158
|
+
gateValidate?: boolean;
|
|
159
|
+
/** The value exposed as `form.result` after submission. */
|
|
160
|
+
result?: unknown;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A full-surface fake of a SvelteKit remote form for testing the Form
|
|
164
|
+
* component: `enhance` returns spreadable attributes whose attachment
|
|
165
|
+
* intercepts native submits (like Kit's does), so tests drive real buttons.
|
|
166
|
+
*/
|
|
167
|
+
declare function fakeRemoteForm(options?: FakeRemoteFormOptions): {
|
|
168
|
+
/** The fake remote form — pass it as the Form component's form. */
|
|
169
|
+
form: {
|
|
170
|
+
method: "POST";
|
|
171
|
+
action: string;
|
|
172
|
+
preflight: (schema: unknown) => /*elided*/ any;
|
|
173
|
+
enhance: (callback: (instance: ReturnType<(node: HTMLFormElement) => {
|
|
174
|
+
element: HTMLFormElement;
|
|
175
|
+
submit: () => Promise<boolean>;
|
|
176
|
+
}>) => unknown) => {
|
|
177
|
+
[x: symbol]: (node: HTMLFormElement) => () => void;
|
|
178
|
+
method: "POST";
|
|
179
|
+
action: string;
|
|
180
|
+
};
|
|
181
|
+
validate: (validateOptions?: FakeValidateOptions) => Promise<void>;
|
|
182
|
+
readonly result: unknown;
|
|
183
|
+
readonly pending: number;
|
|
184
|
+
fields: {
|
|
185
|
+
allIssues: () => {
|
|
186
|
+
message: string;
|
|
187
|
+
path: readonly (string | number)[] | undefined;
|
|
188
|
+
}[] | undefined;
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
/** Arguments of every `validate()` call, in order. */
|
|
192
|
+
validateCalls: (FakeValidateOptions | undefined)[];
|
|
193
|
+
/** Schemas passed to `preflight()`, in order. */
|
|
194
|
+
preflightCalls: unknown[];
|
|
195
|
+
/** Number of enhance submissions started so far. */
|
|
196
|
+
submitCount: () => number;
|
|
197
|
+
/** Resolves the oldest still-gated `validate()` — pairs with the
|
|
198
|
+
* `gateValidate` option. */
|
|
199
|
+
releaseValidate: () => void | undefined;
|
|
200
|
+
/** Installs server-flagged issues directly — models the SSR-restored
|
|
201
|
+
* issue set of a rejected no-JS submission. */
|
|
202
|
+
setServerIssues: (next: readonly FakeIssue[]) => void;
|
|
203
|
+
};
|
|
204
|
+
export { fakeCheckboxField, fakeDateField, fakeForm, fakeNumberField, fakeRemoteForm, fakeSelectField, fakeTextField, };
|
|
205
|
+
export type { FakeIssue, FakeRemoteFormOptions, FakeValidateOptions };
|