@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,346 @@
|
|
|
1
|
+
import { createAttachmentKey } from "svelte/attachments";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal `ValidatableForm` fake for FormState-level tests. `computeIssues`
|
|
4
|
+
* runs on every `validate()` call and its result becomes the issue set that
|
|
5
|
+
* `fields.allIssues()` exposes (default: always valid). State is reactive —
|
|
6
|
+
* effects re-run on `setIssues`/`setPending`.
|
|
7
|
+
*/
|
|
8
|
+
function fakeForm(computeIssues = () => undefined) {
|
|
9
|
+
let issues = $state(undefined);
|
|
10
|
+
let pending = $state(0);
|
|
11
|
+
const validateCalls = [];
|
|
12
|
+
const form = {
|
|
13
|
+
validate: (validateOptions) => {
|
|
14
|
+
validateCalls.push(validateOptions);
|
|
15
|
+
issues = computeIssues();
|
|
16
|
+
},
|
|
17
|
+
fields: { allIssues: () => issues },
|
|
18
|
+
get pending() {
|
|
19
|
+
return pending;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
/** The fake form — pass it where a `ValidatableForm` is expected. */
|
|
24
|
+
form,
|
|
25
|
+
/** Arguments of every `validate()` call, in order. */
|
|
26
|
+
validateCalls,
|
|
27
|
+
/** Replaces the issue set directly, bypassing `validate()`. */
|
|
28
|
+
setIssues: (next) => {
|
|
29
|
+
issues = next;
|
|
30
|
+
},
|
|
31
|
+
/** Sets the form's `pending` submission count. */
|
|
32
|
+
setPending: (next) => {
|
|
33
|
+
pending = next;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Structural `TextField` fake for TextInput/TextareaInput tests. `as()`
|
|
39
|
+
* returns plain spreadable attributes (no attachment); value and issues are
|
|
40
|
+
* reactive state. Pass `options.issues` to seed the field with issues.
|
|
41
|
+
*/
|
|
42
|
+
function fakeTextField(name, options = {}) {
|
|
43
|
+
let value = $state(undefined);
|
|
44
|
+
let issues = $state(options.issues);
|
|
45
|
+
const field = {
|
|
46
|
+
as: (type, initialValue) => ({
|
|
47
|
+
name,
|
|
48
|
+
type,
|
|
49
|
+
value: initialValue,
|
|
50
|
+
}),
|
|
51
|
+
issues: () => issues,
|
|
52
|
+
value: () => value,
|
|
53
|
+
set: (next) => {
|
|
54
|
+
value = next;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
59
|
+
field,
|
|
60
|
+
/** Simulates USER typing (string fields store the same value `set()`
|
|
61
|
+
* would — no raw/typed split here). */
|
|
62
|
+
edit: (next) => {
|
|
63
|
+
value = next;
|
|
64
|
+
},
|
|
65
|
+
/** Replaces the field's issue set. */
|
|
66
|
+
setIssues: (next) => {
|
|
67
|
+
issues = next;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Structural `DateField` fake for DateInput tests. The whole date family
|
|
73
|
+
* (date, month, week, time, datetime-local) carries ISO-style string values,
|
|
74
|
+
* so one fake covers all five. Same shape and reactivity as `fakeTextField`.
|
|
75
|
+
*/
|
|
76
|
+
function fakeDateField(name, options = {}) {
|
|
77
|
+
let value = $state(undefined);
|
|
78
|
+
let issues = $state(options.issues);
|
|
79
|
+
const field = {
|
|
80
|
+
as: (type, initialValue) => ({
|
|
81
|
+
name,
|
|
82
|
+
type,
|
|
83
|
+
value: initialValue,
|
|
84
|
+
}),
|
|
85
|
+
issues: () => issues,
|
|
86
|
+
value: () => value,
|
|
87
|
+
set: (next) => {
|
|
88
|
+
value = next;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
93
|
+
field,
|
|
94
|
+
/** Simulates USER input — an ISO-style string, same as `set()` would
|
|
95
|
+
* store. */
|
|
96
|
+
edit: (next) => {
|
|
97
|
+
value = next;
|
|
98
|
+
},
|
|
99
|
+
/** Replaces the field's issue set. */
|
|
100
|
+
setIssues: (next) => {
|
|
101
|
+
issues = next;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Structural `NumberField` fake for NumberInput tests. Mirrors Kit's mid-edit
|
|
107
|
+
* behavior: `edit()` stores the raw DOM string a user's typing would produce,
|
|
108
|
+
* while `set()` stores the typed number — the distinction is what makes
|
|
109
|
+
* dirty-tracking tests meaningful.
|
|
110
|
+
*/
|
|
111
|
+
function fakeNumberField(name, options = {}) {
|
|
112
|
+
// Kit stores raw DOM strings mid-edit; set() stores typed values.
|
|
113
|
+
let value = $state(undefined);
|
|
114
|
+
let issues = $state(options.issues);
|
|
115
|
+
const field = {
|
|
116
|
+
as: (type, initialValue) => ({
|
|
117
|
+
name,
|
|
118
|
+
type,
|
|
119
|
+
value: initialValue,
|
|
120
|
+
}),
|
|
121
|
+
issues: () => issues,
|
|
122
|
+
value: () => value,
|
|
123
|
+
set: (next) => {
|
|
124
|
+
value = next;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
129
|
+
field,
|
|
130
|
+
/** Simulates USER typing: stores the raw DOM string, like Kit does. */
|
|
131
|
+
edit: (next) => {
|
|
132
|
+
value = next === undefined ? "" : String(next);
|
|
133
|
+
},
|
|
134
|
+
/** Replaces the field's issue set. */
|
|
135
|
+
setIssues: (next) => {
|
|
136
|
+
issues = next;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Structural `SelectField` fake for SelectInput tests. Same shape and
|
|
142
|
+
* reactivity as `fakeTextField` — select values are plain strings on both
|
|
143
|
+
* the DOM and typed sides.
|
|
144
|
+
*/
|
|
145
|
+
function fakeSelectField(name, options = {}) {
|
|
146
|
+
let value = $state(undefined);
|
|
147
|
+
let issues = $state(options.issues);
|
|
148
|
+
const field = {
|
|
149
|
+
as: (type, initialValue) => ({
|
|
150
|
+
name,
|
|
151
|
+
value: initialValue,
|
|
152
|
+
}),
|
|
153
|
+
issues: () => issues,
|
|
154
|
+
value: () => value,
|
|
155
|
+
set: (next) => {
|
|
156
|
+
value = next;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
161
|
+
field,
|
|
162
|
+
/** Simulates the USER choosing an option: stores its string value. */
|
|
163
|
+
edit: (next) => {
|
|
164
|
+
value = next;
|
|
165
|
+
},
|
|
166
|
+
/** Replaces the field's issue set. */
|
|
167
|
+
setIssues: (next) => {
|
|
168
|
+
issues = next;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Structural `CheckboxField` fake for CheckboxInput tests. Like Kit's
|
|
174
|
+
* `as("checkbox", seed)`, the returned attributes expose `checked` and
|
|
175
|
+
* `defaultChecked` getters (the latter is what makes native reset restore
|
|
176
|
+
* the seed). `edit()` stores the raw DOM value ("on"/null) while `set()`
|
|
177
|
+
* stores the typed boolean — mirroring Kit's mid-edit behavior.
|
|
178
|
+
*/
|
|
179
|
+
function fakeCheckboxField(name, options = {}) {
|
|
180
|
+
// Kit stores the raw DOM value mid-edit: "on" checked, null unchecked.
|
|
181
|
+
let value = $state(undefined);
|
|
182
|
+
let issues = $state(options.issues);
|
|
183
|
+
const field = {
|
|
184
|
+
as: (type, initialValue) => ({
|
|
185
|
+
name,
|
|
186
|
+
type,
|
|
187
|
+
get checked() {
|
|
188
|
+
if (value === undefined)
|
|
189
|
+
return initialValue ?? false;
|
|
190
|
+
return value === true || value === "on";
|
|
191
|
+
},
|
|
192
|
+
get defaultChecked() {
|
|
193
|
+
return initialValue;
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
issues: () => issues,
|
|
197
|
+
value: () => value,
|
|
198
|
+
set: (next) => {
|
|
199
|
+
value = next;
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
/** The fake field — pass it as the input component's `field` prop. */
|
|
204
|
+
field,
|
|
205
|
+
/** Simulates a USER toggle: raw DOM value, like Kit's input listener. */
|
|
206
|
+
edit: (next) => {
|
|
207
|
+
value = next ? "on" : null;
|
|
208
|
+
},
|
|
209
|
+
/** Replaces the field's issue set. */
|
|
210
|
+
setIssues: (next) => {
|
|
211
|
+
issues = next;
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* A full-surface fake of a SvelteKit remote form for testing the Form
|
|
217
|
+
* component: `enhance` returns spreadable attributes whose attachment
|
|
218
|
+
* intercepts native submits (like Kit's does), so tests drive real buttons.
|
|
219
|
+
*/
|
|
220
|
+
function fakeRemoteForm(options = {}) {
|
|
221
|
+
let issues = $state(undefined);
|
|
222
|
+
let pending = $state(0);
|
|
223
|
+
const validateCalls = [];
|
|
224
|
+
const preflightCalls = [];
|
|
225
|
+
const validateGates = [];
|
|
226
|
+
let submitCount = 0;
|
|
227
|
+
const pathName = (issue) => (issue.path ?? []).join(".");
|
|
228
|
+
async function validate(validateOptions) {
|
|
229
|
+
validateCalls.push(validateOptions);
|
|
230
|
+
if (options.gateValidate) {
|
|
231
|
+
await new Promise((resolve) => validateGates.push(resolve));
|
|
232
|
+
}
|
|
233
|
+
const computed = (options.onValidate?.(validateOptions) ?? []).map((issue) => ({
|
|
234
|
+
...issue,
|
|
235
|
+
server: validateOptions?.preflightOnly !== true,
|
|
236
|
+
}));
|
|
237
|
+
if (validateOptions?.preflightOnly) {
|
|
238
|
+
// Kit's merge: server issues persist through client-side validation
|
|
239
|
+
// unless a client issue lands on the same path — nothing else can
|
|
240
|
+
// refresh them (merge_with_server_issues, next.25).
|
|
241
|
+
const clientNames = computed.map(pathName);
|
|
242
|
+
issues = [
|
|
243
|
+
...(issues ?? []).filter((issue) => issue.server && !clientNames.includes(pathName(issue))),
|
|
244
|
+
...computed,
|
|
245
|
+
];
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// Full validation ends in a server round-trip whose result replaces
|
|
249
|
+
// the entire issue set. (Kit short-circuits on client-schema failures
|
|
250
|
+
// first — the fake folds both stages into onValidate.)
|
|
251
|
+
issues = computed;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function makeEnhanceInstance(node) {
|
|
255
|
+
return {
|
|
256
|
+
element: node,
|
|
257
|
+
submit: async () => {
|
|
258
|
+
submitCount += 1;
|
|
259
|
+
pending += 1;
|
|
260
|
+
try {
|
|
261
|
+
const succeeded = await (options.onSubmit
|
|
262
|
+
? options.onSubmit()
|
|
263
|
+
: true);
|
|
264
|
+
// Kit derives success from the response's issue set: a success
|
|
265
|
+
// clears it, a rejection replaces it with the server's issues.
|
|
266
|
+
if (succeeded) {
|
|
267
|
+
issues = [];
|
|
268
|
+
}
|
|
269
|
+
else if (options.serverIssues) {
|
|
270
|
+
issues = options.serverIssues.map((issue) => ({
|
|
271
|
+
...issue,
|
|
272
|
+
server: true,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
return succeeded;
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
pending -= 1;
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const form = {
|
|
284
|
+
method: "POST",
|
|
285
|
+
action: "?/fake",
|
|
286
|
+
preflight: (schema) => {
|
|
287
|
+
preflightCalls.push(schema);
|
|
288
|
+
return form;
|
|
289
|
+
},
|
|
290
|
+
enhance: (callback) => ({
|
|
291
|
+
method: "POST",
|
|
292
|
+
action: "?/fake",
|
|
293
|
+
[createAttachmentKey()]: (node) => {
|
|
294
|
+
const onSubmit = (event) => {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
void (async () => {
|
|
297
|
+
// Kit runs preflight BEFORE the enhance callback and swallows
|
|
298
|
+
// the submit entirely when the schema rejects — mirror that
|
|
299
|
+
// whenever a schema was registered via preflight(). Only CLIENT
|
|
300
|
+
// issues block: persisted server issues pass through, because
|
|
301
|
+
// the submission itself is what re-judges them.
|
|
302
|
+
if (preflightCalls.length > 0) {
|
|
303
|
+
await validate({ all: true, preflightOnly: true });
|
|
304
|
+
if ((issues ?? []).some((issue) => !issue.server))
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
await callback(makeEnhanceInstance(node));
|
|
308
|
+
})();
|
|
309
|
+
};
|
|
310
|
+
node.addEventListener("submit", onSubmit);
|
|
311
|
+
return () => node.removeEventListener("submit", onSubmit);
|
|
312
|
+
},
|
|
313
|
+
}),
|
|
314
|
+
validate,
|
|
315
|
+
get result() {
|
|
316
|
+
return options.result;
|
|
317
|
+
},
|
|
318
|
+
get pending() {
|
|
319
|
+
return pending;
|
|
320
|
+
},
|
|
321
|
+
fields: {
|
|
322
|
+
// Kit's allIssues() strips issues down to { path, message } — the
|
|
323
|
+
// server flag never reaches the library.
|
|
324
|
+
allIssues: () => issues?.map(({ message, path }) => ({ message, path })),
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
/** The fake remote form — pass it as the Form component's form. */
|
|
329
|
+
form,
|
|
330
|
+
/** Arguments of every `validate()` call, in order. */
|
|
331
|
+
validateCalls,
|
|
332
|
+
/** Schemas passed to `preflight()`, in order. */
|
|
333
|
+
preflightCalls,
|
|
334
|
+
/** Number of enhance submissions started so far. */
|
|
335
|
+
submitCount: () => submitCount,
|
|
336
|
+
/** Resolves the oldest still-gated `validate()` — pairs with the
|
|
337
|
+
* `gateValidate` option. */
|
|
338
|
+
releaseValidate: () => validateGates.shift()?.(),
|
|
339
|
+
/** Installs server-flagged issues directly — models the SSR-restored
|
|
340
|
+
* issue set of a rejected no-JS submission. */
|
|
341
|
+
setServerIssues: (next) => {
|
|
342
|
+
issues = next.map((issue) => ({ ...issue, server: true }));
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
export { fakeCheckboxField, fakeDateField, fakeForm, fakeNumberField, fakeRemoteForm, fakeSelectField, fakeTextField, };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { InputType } from "@privaty/ui/components/types.js";
|
|
2
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
|
+
import type { HTMLInputAttributes, HTMLSelectAttributes } from "svelte/elements";
|
|
4
|
+
/**
|
|
5
|
+
* What an input hands to FormState.register, so the form can compute
|
|
6
|
+
* dirtiness, pick the majority-aware required/optional marker, and restore
|
|
7
|
+
* values on reset.
|
|
8
|
+
*/
|
|
9
|
+
interface FieldRegistration {
|
|
10
|
+
/** The field's form name — must be unique within the form (register throws
|
|
11
|
+
* on a duplicate). */
|
|
12
|
+
name: string;
|
|
13
|
+
/** The typed seed the field started from — the reference value for the
|
|
14
|
+
* dirty comparison and the value reset restores. */
|
|
15
|
+
initialValue: unknown;
|
|
16
|
+
/** Whether the field is required — feeds the majority-aware
|
|
17
|
+
* required/optional markers. */
|
|
18
|
+
required: boolean;
|
|
19
|
+
/** Reads the field's current value from the remote form. `undefined` means
|
|
20
|
+
* Kit tracks no value yet, in which case initialValue stands in. */
|
|
21
|
+
getValue: () => unknown;
|
|
22
|
+
/** Writes a value back into the remote form field — reset uses it to
|
|
23
|
+
* restore initialValue. */
|
|
24
|
+
setValue: (value: unknown) => void;
|
|
25
|
+
/** Maps raw field values onto the initialValue's domain before dirty
|
|
26
|
+
* comparison — Kit stores raw DOM strings mid-edit ("5", "on") while
|
|
27
|
+
* registrations hold typed seeds (5, true). */
|
|
28
|
+
normalize: (value: unknown) => unknown;
|
|
29
|
+
}
|
|
30
|
+
/** The `type` values a text-family field can render as. */
|
|
31
|
+
type TextFieldType = Extract<InputType, "text" | "email" | "password" | "search" | "url" | "tel">;
|
|
32
|
+
/** The attribute bag TextField.as() returns — spread onto the <input>. */
|
|
33
|
+
type TextFieldAttributes = Omit<HTMLInputAttributes, "type"> & {
|
|
34
|
+
name: string;
|
|
35
|
+
type?: TextFieldType;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The slice of a SvelteKit remote form string field that TextInput needs.
|
|
39
|
+
* Structural on purpose: tests can pass a fake, and a client-only adapter can
|
|
40
|
+
* satisfy it later. Declared with method syntax — methods are checked
|
|
41
|
+
* bivariantly, which lets Kit's generic `as(...)` (a union of narrow tuples)
|
|
42
|
+
* satisfy this widened signature.
|
|
43
|
+
*/
|
|
44
|
+
interface TextField {
|
|
45
|
+
/**
|
|
46
|
+
* Returns the spreadable attribute bag for the <input>, optionally seeding
|
|
47
|
+
* an initial value. A single signature over a tuple union mirrors Kit's
|
|
48
|
+
* exact-arity AsArgs: seeded and unseeded are distinct call shapes (never
|
|
49
|
+
* an optional parameter), while a single signature keeps Kit's generic
|
|
50
|
+
* return narrowed by inference (overloads would compare against every
|
|
51
|
+
* field-type branch).
|
|
52
|
+
*/
|
|
53
|
+
as(...args: [type: TextFieldType] | [type: TextFieldType, initialValue: string]): TextFieldAttributes;
|
|
54
|
+
/** Validation issues belonging to this field, if any. */
|
|
55
|
+
issues(): readonly StandardSchemaV1.Issue[] | undefined;
|
|
56
|
+
/** The field's current value, or undefined when Kit tracks none. */
|
|
57
|
+
value(): string | undefined;
|
|
58
|
+
/** Writes a value into the field — reset uses it to restore the seed. */
|
|
59
|
+
set(value: string): void;
|
|
60
|
+
}
|
|
61
|
+
/** The `type` values a date-family field can render as. */
|
|
62
|
+
type DateFieldType = Extract<InputType, "date" | "datetime-local" | "month" | "time" | "week">;
|
|
63
|
+
/** The attribute bag DateField.as() returns — spread onto the <input>. */
|
|
64
|
+
type DateFieldAttributes = Omit<HTMLInputAttributes, "type"> & {
|
|
65
|
+
name: string;
|
|
66
|
+
type?: DateFieldType;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The slice of a SvelteKit remote form date-family field that DateInput
|
|
70
|
+
* needs. The whole family — date, month, week, time, datetime-local —
|
|
71
|
+
* carries ISO-style string values, so one slice covers all five. Same
|
|
72
|
+
* structural/method-syntax reasoning as TextField.
|
|
73
|
+
*/
|
|
74
|
+
interface DateField {
|
|
75
|
+
/** Returns the spreadable attribute bag for the <input>, optionally seeding
|
|
76
|
+
* an initial value — same tuple-union reasoning as TextField.as. */
|
|
77
|
+
as(...args: [type: DateFieldType] | [type: DateFieldType, initialValue: string]): DateFieldAttributes;
|
|
78
|
+
/** Validation issues belonging to this field, if any. */
|
|
79
|
+
issues(): readonly StandardSchemaV1.Issue[] | undefined;
|
|
80
|
+
/** The field's current ISO-style value, or undefined when Kit tracks none. */
|
|
81
|
+
value(): string | undefined;
|
|
82
|
+
/** Writes a value into the field — reset uses it to restore the seed. */
|
|
83
|
+
set(value: string): void;
|
|
84
|
+
}
|
|
85
|
+
/** The attribute bag NumberField.as() returns — spread onto the <input>. */
|
|
86
|
+
type NumberFieldAttributes = Omit<HTMLInputAttributes, "type"> & {
|
|
87
|
+
name: string;
|
|
88
|
+
type?: "number";
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The slice of a SvelteKit remote form number field that NumberInput needs.
|
|
92
|
+
* Same structural/method-syntax reasoning as TextField.
|
|
93
|
+
*/
|
|
94
|
+
interface NumberField {
|
|
95
|
+
/** Returns the spreadable attribute bag for the <input>, optionally seeding
|
|
96
|
+
* an initial value — same tuple-union reasoning as TextField.as. */
|
|
97
|
+
as(...args: [type: "number"] | [type: "number", initialValue: number]): NumberFieldAttributes;
|
|
98
|
+
/** Validation issues belonging to this field, if any. */
|
|
99
|
+
issues(): readonly StandardSchemaV1.Issue[] | undefined;
|
|
100
|
+
/** The field's current value, or undefined when Kit tracks none. Raw DOM
|
|
101
|
+
* strings appear mid-edit — Kit only coerces at submit/reset. */
|
|
102
|
+
value(): number | string | undefined;
|
|
103
|
+
/** Writes a number into the field — reset uses it to restore the seed. */
|
|
104
|
+
set(value: number): void;
|
|
105
|
+
}
|
|
106
|
+
/** The attribute bag SelectField.as() returns — spread onto the <select>. */
|
|
107
|
+
type SelectFieldAttributes = Omit<HTMLSelectAttributes, "class" | "multiple"> & {
|
|
108
|
+
name: string;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The slice of a SvelteKit remote form select field that SelectInput needs.
|
|
112
|
+
* Same structural/method-syntax reasoning as TextField.
|
|
113
|
+
*/
|
|
114
|
+
interface SelectField {
|
|
115
|
+
/** Returns the spreadable attribute bag for the <select>, optionally
|
|
116
|
+
* seeding an initial value — same tuple-union reasoning as TextField.as. */
|
|
117
|
+
as(...args: [type: "select"] | [type: "select", initialValue: string]): SelectFieldAttributes;
|
|
118
|
+
/** Validation issues belonging to this field, if any. */
|
|
119
|
+
issues(): readonly StandardSchemaV1.Issue[] | undefined;
|
|
120
|
+
/** The field's current value, or undefined when Kit tracks none. */
|
|
121
|
+
value(): string | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Typed `never` on purpose: picklist schemas make Kit type `set()` over the
|
|
124
|
+
* field's literal union, which this slice cannot know. `never` is assignable
|
|
125
|
+
* to every union parameter (contravariance), and the library only ever
|
|
126
|
+
* passes back values the field itself produced.
|
|
127
|
+
*/
|
|
128
|
+
set: (value: never) => void;
|
|
129
|
+
}
|
|
130
|
+
/** The attribute bag CheckboxField.as() returns — spread onto the <input>. */
|
|
131
|
+
type CheckboxFieldAttributes = Omit<HTMLInputAttributes, "type"> & {
|
|
132
|
+
name: string;
|
|
133
|
+
type?: "checkbox";
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* The slice of a SvelteKit remote form boolean field that CheckboxInput
|
|
137
|
+
* needs. Same structural/method-syntax reasoning as TextField. The seeded
|
|
138
|
+
* call shape matters: Kit's as("checkbox", seed) provides `checked` AND a
|
|
139
|
+
* `defaultChecked` getter, which is what makes native reset restore the
|
|
140
|
+
* seed — never add a separate defaultChecked attribute on top (Kit defines
|
|
141
|
+
* its getter non-configurably; SSR crashes redefining it).
|
|
142
|
+
*/
|
|
143
|
+
interface CheckboxField {
|
|
144
|
+
/** Returns the spreadable attribute bag for the <input>, optionally seeding
|
|
145
|
+
* an initial value — same tuple-union reasoning as TextField.as. */
|
|
146
|
+
as(...args: [type: "checkbox"] | [type: "checkbox", initialValue: boolean]): CheckboxFieldAttributes;
|
|
147
|
+
/** Validation issues belonging to this field, if any. */
|
|
148
|
+
issues(): readonly StandardSchemaV1.Issue[] | undefined;
|
|
149
|
+
/** The field's current value, or undefined when Kit tracks none. Mid-edit,
|
|
150
|
+
* Kit stores the raw DOM value ("on") or null for unchecked — coercion to
|
|
151
|
+
* boolean only happens at submit/reset. */
|
|
152
|
+
value(): boolean | string | null | undefined;
|
|
153
|
+
/** Writes a boolean into the field — reset uses it to restore the seed. */
|
|
154
|
+
set: (value: boolean) => void;
|
|
155
|
+
}
|
|
156
|
+
export type { CheckboxField, CheckboxFieldAttributes, DateField, DateFieldAttributes, DateFieldType, FieldRegistration, NumberField, NumberFieldAttributes, SelectField, SelectFieldAttributes, TextField, TextFieldAttributes, TextFieldType, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
/**
|
|
3
|
+
* The slice of a SvelteKit remote form that FormState needs. Structural on
|
|
4
|
+
* purpose: tests can pass a fake, and a client-only adapter can satisfy it later.
|
|
5
|
+
*/
|
|
6
|
+
interface ValidatableForm {
|
|
7
|
+
/**
|
|
8
|
+
* Kit's programmatic validation. `all: true` also surfaces issues on fields
|
|
9
|
+
* not yet edited and blurred; `preflightOnly: true` runs only the
|
|
10
|
+
* client-side preflight schema (no server round-trip). Typed `unknown`
|
|
11
|
+
* rather than `Promise<void>` so a fake may return synchronously — callers
|
|
12
|
+
* wrap the result in Promise.resolve.
|
|
13
|
+
*/
|
|
14
|
+
validate: (options?: {
|
|
15
|
+
all?: boolean;
|
|
16
|
+
preflightOnly?: boolean;
|
|
17
|
+
}) => unknown;
|
|
18
|
+
/** Root issue accessor: validation issues for the whole form — form-level
|
|
19
|
+
* (path-less) and field-level alike — if any. */
|
|
20
|
+
fields: {
|
|
21
|
+
allIssues: () => readonly StandardSchemaV1.Issue[] | undefined;
|
|
22
|
+
};
|
|
23
|
+
/** The number of pending submissions — isSubmitting derives from it being
|
|
24
|
+
* greater than zero. */
|
|
25
|
+
readonly pending: number;
|
|
26
|
+
}
|
|
27
|
+
export type { ValidatableForm };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@privaty/ui-forms",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/privaty-io/ui.git",
|
|
8
|
+
"directory": "packages/ui-forms"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"NOTICE"
|
|
14
|
+
],
|
|
15
|
+
"sideEffects": [
|
|
16
|
+
"**/*.css"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
"./package.json": "./package.json",
|
|
20
|
+
"./*": {
|
|
21
|
+
"svelte": "./dist/*",
|
|
22
|
+
"default": "./dist/*"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@sveltejs/kit": "^3.0.0-next.0",
|
|
30
|
+
"svelte": "^5.0.0",
|
|
31
|
+
"@privaty/ui": "^0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@standard-schema/spec": "^1.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@sveltejs/kit": "next",
|
|
38
|
+
"@sveltejs/package": "^2.5.8",
|
|
39
|
+
"eslint": "^10.9.1",
|
|
40
|
+
"svelte": "^5.56.10",
|
|
41
|
+
"svelte-check": "^4.7.6",
|
|
42
|
+
"typescript": "^6.0.3",
|
|
43
|
+
"@config/typescript": "0.0.0",
|
|
44
|
+
"@privaty/ui": "0.1.0",
|
|
45
|
+
"@config/eslint": "0.0.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
49
|
+
"build": "svelte-package && find dist \\( -name '*.test.*' -o -name '*.fixture.*' \\) -delete",
|
|
50
|
+
"lint": "eslint ."
|
|
51
|
+
}
|
|
52
|
+
}
|