@samuel-charpentier/sform 0.0.12 → 1.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/README.md +697 -621
- package/dist/Sform/SIssues.svelte +1 -1
- package/dist/Sform/Sfield.svelte +226 -59
- package/dist/Sform/Sform.svelte +79 -11
- package/dist/Sform/Sform.svelte.d.ts +6 -1
- package/dist/Sform/context.svelte.d.ts +1 -1
- package/dist/Sform/context.svelte.js +41 -1
- package/dist/Sform/index.d.ts +1 -1
- package/dist/Sform/inputs/ButtonInput.svelte +25 -36
- package/dist/Sform/inputs/ButtonInput.svelte.d.ts +2 -25
- package/dist/Sform/inputs/CheckboxGroupInput.svelte +3 -31
- package/dist/Sform/inputs/CheckboxInput.svelte +5 -1
- package/dist/Sform/inputs/HiddenInput.svelte +10 -10
- package/dist/Sform/inputs/MaskedInput.svelte +3 -4
- package/dist/Sform/inputs/NumberInput.svelte +2 -3
- package/dist/Sform/inputs/PasswordInput.svelte +2 -3
- package/dist/Sform/inputs/RadioCheckboxLabel.svelte +50 -0
- package/dist/Sform/inputs/RadioCheckboxLabel.svelte.d.ts +39 -0
- package/dist/Sform/inputs/RadioInput.svelte +3 -42
- package/dist/Sform/inputs/RangeInput.svelte +2 -3
- package/dist/Sform/inputs/SelectInput.svelte +2 -3
- package/dist/Sform/inputs/TextInput.svelte +2 -3
- package/dist/Sform/inputs/TextareaInput.svelte +2 -3
- package/dist/Sform/inputs/ToggleInput.svelte +5 -1
- package/dist/Sform/inputs/TopLabel.svelte +14 -0
- package/dist/Sform/inputs/TopLabel.svelte.d.ts +4 -0
- package/dist/Sform/sform.css +0 -41
- package/dist/Sform/types.d.ts +68 -44
- package/dist/Sform/utils/Fieldset.svelte +8 -3
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/Sform/inputs/ToggleOptionsInput.svelte +0 -118
- package/dist/Sform/inputs/ToggleOptionsInput.svelte.d.ts +0 -4
package/README.md
CHANGED
|
@@ -1,621 +1,697 @@
|
|
|
1
|
-
# Sform
|
|
2
|
-
|
|
3
|
-
A type-safe form library for **Svelte 5** with **SvelteKit remote functions**.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [Features](#features)
|
|
8
|
-
- [Requirements](#requirements)
|
|
9
|
-
- [Installation](#installation)
|
|
10
|
-
- [Quick Start](#quick-start)
|
|
11
|
-
- [Create a Remote Form](#1-create-a-remote-form)
|
|
12
|
-
- [Create Your Form Component](#2-create-your-form-component)
|
|
13
|
-
- [Components](#components)
|
|
14
|
-
- [`<Sform>`](#sform)
|
|
15
|
-
- [`<Sfield>`](#sfield)
|
|
16
|
-
- [Common Props (all types)](#common-props-all-types)
|
|
17
|
-
- [Text Inputs](#text-inputs)
|
|
18
|
-
- [Password Input](#password-input)
|
|
19
|
-
- [Number Input](#number-input)
|
|
20
|
-
- [Textarea](#textarea)
|
|
21
|
-
- [Select](#select)
|
|
22
|
-
- [Checkbox](#checkbox)
|
|
23
|
-
- [Radio](#radio)
|
|
24
|
-
- [Range](#range)
|
|
25
|
-
- [Toggle](#toggle)
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
|
|
29
|
-
- [`<
|
|
30
|
-
- [`<
|
|
31
|
-
|
|
32
|
-
- [
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- ✅ **
|
|
41
|
-
- ✅ **
|
|
42
|
-
- ✅ **
|
|
43
|
-
- ✅ **
|
|
44
|
-
- ✅ **
|
|
45
|
-
- ✅ **
|
|
46
|
-
- ✅ **
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- `
|
|
139
|
-
- `
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{#snippet
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
|
248
|
-
|
|
|
249
|
-
| `
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
|
273
|
-
|
|
|
274
|
-
| `
|
|
275
|
-
|
|
276
|
-
####
|
|
277
|
-
|
|
278
|
-
```svelte
|
|
279
|
-
<Sfield field={fields.
|
|
280
|
-
<Sfield
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
|
292
|
-
|
|
|
293
|
-
| `
|
|
294
|
-
| `
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
|
369
|
-
|
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
|
388
|
-
|
|
|
389
|
-
| `
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
```svelte
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
<
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
<
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1
|
+
# Sform
|
|
2
|
+
|
|
3
|
+
A type-safe form library for **Svelte 5** with **SvelteKit remote functions**.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#features)
|
|
8
|
+
- [Requirements](#requirements)
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Quick Start](#quick-start)
|
|
11
|
+
- [Create a Remote Form](#1-create-a-remote-form)
|
|
12
|
+
- [Create Your Form Component](#2-create-your-form-component)
|
|
13
|
+
- [Components](#components)
|
|
14
|
+
- [`<Sform>`](#sform)
|
|
15
|
+
- [`<Sfield>`](#sfield)
|
|
16
|
+
- [Common Props (all types)](#common-props-all-types)
|
|
17
|
+
- [Text Inputs](#text-inputs)
|
|
18
|
+
- [Password Input](#password-input)
|
|
19
|
+
- [Number Input](#number-input)
|
|
20
|
+
- [Textarea](#textarea)
|
|
21
|
+
- [Select](#select)
|
|
22
|
+
- [Checkbox](#checkbox)
|
|
23
|
+
- [Radio](#radio)
|
|
24
|
+
- [Range](#range)
|
|
25
|
+
- [Toggle](#toggle)
|
|
26
|
+
- [Masked Input](#masked-input)
|
|
27
|
+
- [Hidden Input](#hidden-input)
|
|
28
|
+
- [`<Sbutton>`](#sbutton)
|
|
29
|
+
- [`<SIssues>`](#sissues)
|
|
30
|
+
- [`<SResult>`](#sresult)
|
|
31
|
+
- [Styling](#styling)
|
|
32
|
+
- [Validation](#validation)
|
|
33
|
+
- [Type Safety](#type-safety)
|
|
34
|
+
- [Development](#development)
|
|
35
|
+
- [License](#license)
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- ✅ **Type-safe** - Discriminated union types for each input type
|
|
40
|
+
- ✅ **Preflight validation** - All errors shown on submit, not one at a time
|
|
41
|
+
- ✅ **Validate modes** - `blur`, `change`, or `submit`
|
|
42
|
+
- ✅ **Password toggle** - Eye icon to show/hide password
|
|
43
|
+
- ✅ **Masked inputs** - Phone, credit card, SSN formatting
|
|
44
|
+
- ✅ **Range slider** - With optional value display
|
|
45
|
+
- ✅ **Toggle switch** - Modern on/off control
|
|
46
|
+
- ✅ **Stateful button** - Shows pending state during submission
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
- Svelte 5
|
|
51
|
+
- SvelteKit with `remoteFunctions: true` in config
|
|
52
|
+
- Valibot for schema validation
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install @samuel-charpentier/sform
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Enable remote functions in `svelte.config.js`:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
export default {
|
|
64
|
+
kit: {
|
|
65
|
+
experimental: {
|
|
66
|
+
remoteFunctions: true
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
### 1. Create a Remote Form
|
|
75
|
+
|
|
76
|
+
Create a `.remote.ts` file with your form schema and handler:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// src/routes/auth.remote.ts
|
|
80
|
+
import * as v from 'valibot';
|
|
81
|
+
import { form } from '@sveltejs/kit/remote';
|
|
82
|
+
|
|
83
|
+
const loginSchema = v.object({
|
|
84
|
+
username: v.pipe(v.string(), v.minLength(3, 'Username must be at least 3 characters')),
|
|
85
|
+
_password: v.pipe(v.string(), v.minLength(8, 'Password must be at least 8 characters'))
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const login = form(loginSchema, async ({ username, _password }) => {
|
|
89
|
+
// Your authentication logic here
|
|
90
|
+
return { success: true, message: 'Welcome!' };
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 2. Create Your Form Component
|
|
95
|
+
|
|
96
|
+
```svelte
|
|
97
|
+
<script lang="ts">
|
|
98
|
+
import { Sform, Sfield, Sbutton } from '@samuel-charpentier/sform';
|
|
99
|
+
import { login } from './auth.remote.ts';
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<Sform form={login} validateOn="blur">
|
|
103
|
+
{#snippet children(fields)}
|
|
104
|
+
<Sfield field={fields.username} type="text" label="Username" />
|
|
105
|
+
<Sfield field={fields._password} type="password" label="Password" />
|
|
106
|
+
|
|
107
|
+
<Sbutton form={login} label="Login" />
|
|
108
|
+
{/snippet}
|
|
109
|
+
</Sform>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Components
|
|
113
|
+
|
|
114
|
+
### `<Sform>`
|
|
115
|
+
|
|
116
|
+
Wrapper component that provides form context to all child fields.
|
|
117
|
+
|
|
118
|
+
```svelte
|
|
119
|
+
<Sform form={remoteForm} validateOn="blur" class="my-form">
|
|
120
|
+
{#snippet children(fields)}
|
|
121
|
+
<!-- Sfield components here -->
|
|
122
|
+
{/snippet}
|
|
123
|
+
</Sform>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| Prop | Type | Default | Description |
|
|
127
|
+
| ---------------- | -------------------------------- | ----------- | -------------------------------------------------------------------- |
|
|
128
|
+
| `form` | `RemoteForm` | required | Remote form object from `form()` API |
|
|
129
|
+
| `validateOn` | `'blur' \| 'change' \| 'submit'` | `'blur'` | When to validate and show errors |
|
|
130
|
+
| `class` | `string` | `undefined` | CSS class for form element |
|
|
131
|
+
| `preflightOnly` | `boolean` | `false` | If true, client side validation is preflight only |
|
|
132
|
+
| `resetOnSuccess` | `boolean` | `true` | If false, keep touched/dirty/submitted state after successful submit |
|
|
133
|
+
| `lifecycle` | `SformLifecycleHooks` | `undefined` | Register lifecycle hooks for submit/validate flow |
|
|
134
|
+
| `disabled` | `boolean` | `false` | Disable the entire form: no validation, no submission, all fields disabled |
|
|
135
|
+
|
|
136
|
+
**Validate Modes:**
|
|
137
|
+
|
|
138
|
+
- `blur` - Validate and show errors after leaving field (default)
|
|
139
|
+
- `change` - Validate and show errors as soon as value changes
|
|
140
|
+
- `submit` - Validate and show all errors only after submit attempt
|
|
141
|
+
|
|
142
|
+
**Disabled Forms:**
|
|
143
|
+
|
|
144
|
+
Setting `disabled` on `<Sform>` freezes the whole form:
|
|
145
|
+
|
|
146
|
+
- All local (preflight) and remote validation is suppressed — no `form.validate()` calls, and the `beforeValidate`/`afterValidateCalled`/`afterValidateSettled` lifecycle hooks don't run.
|
|
147
|
+
- Submission is blocked, even from a custom `<button type="submit">` inside the form; `Sbutton` is automatically disabled.
|
|
148
|
+
- Every child `Sfield` (including `type="hidden"` and the hidden value input backing `type="masked"`) receives the HTML `disabled` attribute and is excluded from the submitted `FormData`. Form-level `disabled` always wins over a field's own `disabled` prop.
|
|
149
|
+
- Validation messages (field-level and `SIssues`) are hidden while disabled. `touched`/`dirty`/`submitted` state is preserved, so previously shown messages reappear unchanged once the form is re-enabled — they are **not** cleared while disabled.
|
|
150
|
+
- The `<form>` element gets `aria-disabled="true"` and `data-disabled` attributes for styling/AT hooks. `inert` is intentionally not applied, since it would also remove links and static content from the accessibility tree.
|
|
151
|
+
|
|
152
|
+
**Lifecycle Hooks:**
|
|
153
|
+
|
|
154
|
+
Use `lifecycle` to run sync/async functions at key points in the form lifecycle:
|
|
155
|
+
|
|
156
|
+
```svelte
|
|
157
|
+
<Sform
|
|
158
|
+
form={login}
|
|
159
|
+
lifecycle={{
|
|
160
|
+
beforeSubmit: async () => {
|
|
161
|
+
// Mutate values right before Sbutton triggers submit
|
|
162
|
+
login.fields.username.set(login.fields.username.value().trim());
|
|
163
|
+
},
|
|
164
|
+
afterSubmitTriggered: () => {
|
|
165
|
+
console.log('submit requested');
|
|
166
|
+
},
|
|
167
|
+
afterSubmitResponse: () => {
|
|
168
|
+
console.log('submit response received');
|
|
169
|
+
},
|
|
170
|
+
beforeValidate: () => {
|
|
171
|
+
console.log('before validate');
|
|
172
|
+
},
|
|
173
|
+
afterValidateCalled: () => {
|
|
174
|
+
console.log('validate called');
|
|
175
|
+
},
|
|
176
|
+
afterValidateSettled: () => {
|
|
177
|
+
console.log('validate settled');
|
|
178
|
+
}
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{#snippet children(fields)}
|
|
182
|
+
<Sfield field={fields.username} type="text" />
|
|
183
|
+
{/snippet}
|
|
184
|
+
</Sform>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Hook event names:
|
|
188
|
+
|
|
189
|
+
- `beforeSubmit` - right before submit is triggered by `<Sbutton>` click
|
|
190
|
+
- `afterSubmitTriggered` - right after submit is triggered
|
|
191
|
+
- `afterSubmitResponse` - once pending resolves back to idle
|
|
192
|
+
- `beforeValidate` - immediately before `form.validate()`
|
|
193
|
+
- `afterValidateCalled` - immediately after `form.validate()` is called (validation may still be in flight)
|
|
194
|
+
- `afterValidateSettled` - when `form.validate()` settles (resolved or rejected)
|
|
195
|
+
|
|
196
|
+
Hooks registered for the same event run in parallel. Execution order is not guaranteed, especially when fields mount dynamically.
|
|
197
|
+
|
|
198
|
+
- Keep same-event hooks independent and idempotent.
|
|
199
|
+
- Do not rely on one same-event hook mutating state before another reads it.
|
|
200
|
+
|
|
201
|
+
Validation lifecycle hooks are intended for UX and instrumentation, not security decisions.
|
|
202
|
+
|
|
203
|
+
- Good uses: loading indicators, tracing/metrics, validation timing analytics.
|
|
204
|
+
- Avoid: relying on client-only validation hooks for authorization, policy enforcement, or bypass patterns.
|
|
205
|
+
- Keep authoritative checks on server submit handlers and schemas.
|
|
206
|
+
|
|
207
|
+
If you need to keep field touched/dirty/submitted state after a successful submit response:
|
|
208
|
+
|
|
209
|
+
```svelte
|
|
210
|
+
<Sform form={login} resetOnSuccess={false}>
|
|
211
|
+
{#snippet children(fields)}
|
|
212
|
+
<Sfield field={fields.username} type="text" />
|
|
213
|
+
{/snippet}
|
|
214
|
+
</Sform>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `<Sfield>`
|
|
218
|
+
|
|
219
|
+
Smart field component with type-safe props based on input type.
|
|
220
|
+
|
|
221
|
+
#### Common Props (all types)
|
|
222
|
+
|
|
223
|
+
| Prop | Type | Default | Description |
|
|
224
|
+
| ------------- | ------------------------- | ----------- | ------------------------------------- |
|
|
225
|
+
| `field` | `RemoteFormField` | required | Field from `fields` snippet parameter |
|
|
226
|
+
| `type` | `InputType` | required | Input type |
|
|
227
|
+
| `label` | `string` | `undefined` | Field label |
|
|
228
|
+
| `placeholder` | `string` | `undefined` | Placeholder text (text/password/etc) |
|
|
229
|
+
| `disabled` | `boolean` | `false` | Disable the field |
|
|
230
|
+
| `readonly` | `boolean` | `false` | Make field readonly |
|
|
231
|
+
| `validateOn` | `ValidateOn` | inherited | Override form validateOn |
|
|
232
|
+
| `class` | `SfieldClasses \| string` | `undefined` | CSS classes |
|
|
233
|
+
| `hint` | `string \| Snippet` | `undefined` | Help text shown below the field |
|
|
234
|
+
| `lifecycle` | `SformLifecycleHooks` | `undefined` | Register field-scoped lifecycle hooks |
|
|
235
|
+
|
|
236
|
+
#### Text Inputs
|
|
237
|
+
|
|
238
|
+
```svelte
|
|
239
|
+
<Sfield field={fields.email} type="email" label="Email" placeholder="you@example.com" />
|
|
240
|
+
<Sfield field={fields.search} type="search" label="Search" />
|
|
241
|
+
<Sfield field={fields.phone} type="tel" label="Phone" />
|
|
242
|
+
<Sfield field={fields.website} type="url" label="Website" prefix="https://" />
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime-local`, `time`, `month`, `week`, `color`, `file`
|
|
246
|
+
|
|
247
|
+
| Prop | Type | Default | Description |
|
|
248
|
+
| -------- | ------------------- | ----------- | -------------------- |
|
|
249
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input |
|
|
250
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input |
|
|
251
|
+
|
|
252
|
+
#### Password Input
|
|
253
|
+
|
|
254
|
+
```svelte
|
|
255
|
+
<Sfield field={fields._password} type="password" label="Password" />
|
|
256
|
+
<Sfield field={fields._password} type="password" label="Password" showToggle={false} />
|
|
257
|
+
<Sfield field={fields._password} type="password" label="Password">
|
|
258
|
+
{#snippet showToggleIcon(passwordShown)}
|
|
259
|
+
{#if passwordShown}
|
|
260
|
+
🙈
|
|
261
|
+
{:else}
|
|
262
|
+
👁️
|
|
263
|
+
{/if}
|
|
264
|
+
{/snippet}
|
|
265
|
+
</Sfield>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
| Prop | Type | Default | Description |
|
|
269
|
+
| ---------------- | ----------------------------------- | ----------- | ---------------------------------- |
|
|
270
|
+
| `showToggle` | `boolean` | `true` | Show eye icon to toggle visibility |
|
|
271
|
+
| `showToggleIcon` | `Snippet<[passwordShown: boolean]>` | `undefined` | Custom toggle icon snippet |
|
|
272
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input |
|
|
273
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input |
|
|
274
|
+
| `autocomplete` | `string` | `undefined` | HTML autocomplete attribute |
|
|
275
|
+
|
|
276
|
+
#### Number Input
|
|
277
|
+
|
|
278
|
+
```svelte
|
|
279
|
+
<Sfield field={fields.age} type="number" label="Age" min={0} max={150} step={1} />
|
|
280
|
+
<Sfield field={fields.price} type="number" label="Price" prefix="$" suffix="USD" align="end" />
|
|
281
|
+
<Sfield field={fields.quantity} type="number" label="Qty" showControls={false} maxDecimals={0} />
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
| Prop | Type | Default | Description |
|
|
285
|
+
| -------------- | ------------------- | ----------- | -------------------------------------- |
|
|
286
|
+
| `min` | `number \| string` | `undefined` | Minimum value |
|
|
287
|
+
| `max` | `number \| string` | `undefined` | Maximum value |
|
|
288
|
+
| `step` | `number \| string` | `undefined` | Step increment |
|
|
289
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input (e.g., "$") |
|
|
290
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input (e.g., "USD") |
|
|
291
|
+
| `showControls` | `boolean` | `true` | Show spinner controls |
|
|
292
|
+
| `align` | `'start' \| 'end'` | `'start'` | Text alignment |
|
|
293
|
+
| `maxDecimals` | `number` | `undefined` | Max decimal places (0 = integers only) |
|
|
294
|
+
| `autocomplete` | `string` | `undefined` | HTML autocomplete attribute |
|
|
295
|
+
|
|
296
|
+
#### Textarea
|
|
297
|
+
|
|
298
|
+
```svelte
|
|
299
|
+
<Sfield field={fields.bio} type="textarea" label="Bio" placeholder="Tell us about yourself" />
|
|
300
|
+
<Sfield field={fields.notes} type="textarea" label="Notes" prefix="📝" suffix="(max 500 chars)" />
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
| Prop | Type | Default | Description |
|
|
304
|
+
| -------- | ------------------- | ----------- | -------------------- |
|
|
305
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input |
|
|
306
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input |
|
|
307
|
+
|
|
308
|
+
#### Select
|
|
309
|
+
|
|
310
|
+
```svelte
|
|
311
|
+
<Sfield
|
|
312
|
+
field={fields.country}
|
|
313
|
+
type="select"
|
|
314
|
+
label="Country"
|
|
315
|
+
options={[
|
|
316
|
+
{ value: 'us', label: 'United States' },
|
|
317
|
+
{ value: 'uk', label: 'United Kingdom' },
|
|
318
|
+
{ value: 'ca', label: 'Canada' }
|
|
319
|
+
]}
|
|
320
|
+
/>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
| Prop | Type | Default | Description |
|
|
324
|
+
| -------------- | ---------------------------- | ----------- | --------------------------- |
|
|
325
|
+
| `options` | `SelectOption[] \| string[]` | required | Select options |
|
|
326
|
+
| `autocomplete` | `string` | `undefined` | HTML autocomplete attribute |
|
|
327
|
+
|
|
328
|
+
#### Checkbox
|
|
329
|
+
|
|
330
|
+
```svelte
|
|
331
|
+
<Sfield field={fields.subscribe} type="checkbox" label="Subscribe to newsletter" />
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### Radio
|
|
335
|
+
|
|
336
|
+
```svelte
|
|
337
|
+
<Sfield
|
|
338
|
+
field={fields.plan}
|
|
339
|
+
type="radio"
|
|
340
|
+
label="Plan"
|
|
341
|
+
options={[
|
|
342
|
+
{ value: 'free', label: 'Free' },
|
|
343
|
+
{ value: 'pro', label: 'Pro' },
|
|
344
|
+
{ value: 'enterprise', label: 'Enterprise' }
|
|
345
|
+
]}
|
|
346
|
+
/>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
| Prop | Type | Default | Description |
|
|
350
|
+
| --------- | ---------------------------- | ----------- | ------------------------ |
|
|
351
|
+
| `options` | `SelectOption[] \| string[]` | `undefined` | Radio options for groups |
|
|
352
|
+
|
|
353
|
+
#### Range
|
|
354
|
+
|
|
355
|
+
```svelte
|
|
356
|
+
<Sfield field={fields.volume} type="range" label="Volume" min={0} max={100} step={5} showValue />
|
|
357
|
+
<Sfield
|
|
358
|
+
field={fields.brightness}
|
|
359
|
+
type="range"
|
|
360
|
+
label="Brightness"
|
|
361
|
+
min={0}
|
|
362
|
+
max={100}
|
|
363
|
+
formatValue={(v) => `${v}%`}
|
|
364
|
+
showValue
|
|
365
|
+
/>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
| Prop | Type | Default | Description |
|
|
369
|
+
| -------------- | --------------------------- | ----------- | --------------------------- |
|
|
370
|
+
| `min` | `number \| string` | `0` | Minimum value |
|
|
371
|
+
| `max` | `number \| string` | `100` | Maximum value |
|
|
372
|
+
| `step` | `number \| string` | `1` | Step increment |
|
|
373
|
+
| `showValue` | `boolean` | `false` | Show current value |
|
|
374
|
+
| `formatValue` | `(value: number) => string` | `undefined` | Format displayed value |
|
|
375
|
+
| `autocomplete` | `string` | `undefined` | HTML autocomplete attribute |
|
|
376
|
+
|
|
377
|
+
#### Toggle
|
|
378
|
+
|
|
379
|
+
```svelte
|
|
380
|
+
<Sfield field={fields.notifications} type="toggle" label="Enable Notifications" />
|
|
381
|
+
<Sfield field={fields.darkMode} type="toggle" label="Theme" onLabel="Dark" offLabel="Light" />
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
| Prop | Type | Default | Description |
|
|
385
|
+
| ---------------- | -------- | ----------- | -------------------- |
|
|
386
|
+
| `onLabel` | `string` | `undefined` | Label when on |
|
|
387
|
+
| `offLabel` | `string` | `undefined` | Label when off |
|
|
388
|
+
| `checkedValue` | `string` | `'true'` | Value when checked |
|
|
389
|
+
| `uncheckedValue` | `string` | `'false'` | Value when unchecked |
|
|
390
|
+
|
|
391
|
+
#### Masked Input
|
|
392
|
+
|
|
393
|
+
```svelte
|
|
394
|
+
<Sfield field={fields.phone} type="masked" label="Phone" mask="(###) ###-####" />
|
|
395
|
+
<Sfield field={fields.creditCard} type="masked" label="Credit Card" mask="#### #### #### ####" />
|
|
396
|
+
<Sfield field={fields.ssn} type="masked" label="SSN" mask="###-##-####" />
|
|
397
|
+
<!-- Custom tokens -->
|
|
398
|
+
<Sfield
|
|
399
|
+
field={fields.code}
|
|
400
|
+
type="masked"
|
|
401
|
+
label="Code"
|
|
402
|
+
mask="AAAA-99-LL"
|
|
403
|
+
tokens={{ A: /[A-Z]/, L: /[a-z]/ }}
|
|
404
|
+
/>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
| Prop | Type | Default | Description |
|
|
408
|
+
| --------------------- | ------------------------ | ----------- | -------------------------------- |
|
|
409
|
+
| `mask` | `string` | required | Mask pattern |
|
|
410
|
+
| `tokens` | `Record<string, RegExp>` | `undefined` | Custom token definitions |
|
|
411
|
+
| `maskPlaceholder` | `string` | `'_'` | Placeholder character |
|
|
412
|
+
| `showMaskPlaceholder` | `boolean` | `false` | Show full mask with placeholders |
|
|
413
|
+
| `unmaskValue` | `boolean` | `true` | Store unmasked value |
|
|
414
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input |
|
|
415
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input |
|
|
416
|
+
| `autocomplete` | `string` | `undefined` | HTML autocomplete attribute |
|
|
417
|
+
|
|
418
|
+
**Mask Tokens:**
|
|
419
|
+
|
|
420
|
+
- `#` or `9` - Numeric (0-9)
|
|
421
|
+
- `a` - Alphabetic (a-z, A-Z)
|
|
422
|
+
- `A` - Alphabetic uppercase
|
|
423
|
+
- `*` - Alphanumeric
|
|
424
|
+
|
|
425
|
+
#### Hidden Input
|
|
426
|
+
|
|
427
|
+
```svelte
|
|
428
|
+
<Sfield field={fields.token} type="hidden" value={authToken} />
|
|
429
|
+
<Sfield field={fields.userId} type="hidden" value="12345" />
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
| Prop | Type | Default | Description |
|
|
433
|
+
| ---------- | --------- | ------- | ------------------------------------------------ |
|
|
434
|
+
| `value` | `string` | `''` | The value for the hidden field (can be reactive) |
|
|
435
|
+
| `disabled` | `boolean` | `false` | Disables the hidden input so it is not submitted |
|
|
436
|
+
|
|
437
|
+
Hidden inputs are useful for including data in form submissions without displaying it to the user. The `value` prop is reactive, so you can update it programmatically:
|
|
438
|
+
|
|
439
|
+
```svelte
|
|
440
|
+
<script lang="ts">
|
|
441
|
+
let token = $state(initialToken);
|
|
442
|
+
|
|
443
|
+
async function refreshToken() {
|
|
444
|
+
token = await getNewToken();
|
|
445
|
+
}
|
|
446
|
+
</script>
|
|
447
|
+
|
|
448
|
+
<Sfield field={fields.token} type="hidden" value={token} />
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
You can disable a hidden field until it is ready to be submitted. This is useful for client-only tokens that should not be sent by an SSR/no-JS form:
|
|
452
|
+
|
|
453
|
+
```svelte
|
|
454
|
+
<Sfield field={fields.recaptcha} type="hidden" value={token} disabled={!clientReady} />
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
#### Field issue ownership
|
|
458
|
+
|
|
459
|
+
By default, visible fields render their own issues and hidden fields leave their issues to `<SIssues>`. Use `issueDisplay` when a field needs different ownership:
|
|
460
|
+
|
|
461
|
+
```svelte
|
|
462
|
+
<!-- Render issues next to this field, even if it is hidden -->
|
|
463
|
+
<Sfield field={fields.token} type="hidden" value={token} issueDisplay="field" />
|
|
464
|
+
|
|
465
|
+
<!-- Leave this field's issues for <SIssues> -->
|
|
466
|
+
<Sfield field={fields.email} type="email" label="Email" issueDisplay="form" />
|
|
467
|
+
|
|
468
|
+
<!-- Mark issues as handled without rendering them -->
|
|
469
|
+
<Sfield field={fields.recaptcha} type="hidden" value={token} issueDisplay="none" />
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
| Value | Behavior |
|
|
473
|
+
| ------- | --------------------------------------------------------------------------------------- |
|
|
474
|
+
| `auto` | Default. Visible fields render their issues; hidden fields leave issues to `<SIssues>`. |
|
|
475
|
+
| `field` | The field renders its own issues. |
|
|
476
|
+
| `form` | The field leaves its issues to `<SIssues>`. |
|
|
477
|
+
| `none` | The field marks issues as handled without rendering them anywhere. |
|
|
478
|
+
|
|
479
|
+
### `<Sbutton>`
|
|
480
|
+
|
|
481
|
+
Stateful submit button that reacts to form state. Pass the `form` prop to enable typed result access.
|
|
482
|
+
|
|
483
|
+
```svelte
|
|
484
|
+
<Sbutton form={myForm} label="Submit" class="my-button" />
|
|
485
|
+
|
|
486
|
+
<!-- With custom state rendering -->
|
|
487
|
+
<Sbutton form={myForm} class="submit-btn">
|
|
488
|
+
{#snippet children(state)}
|
|
489
|
+
{#if state.state === 'pending'}
|
|
490
|
+
Submitting...
|
|
491
|
+
{:else if state.state === 'success'}
|
|
492
|
+
✓ {state.result.message}
|
|
493
|
+
{:else if state.state === 'hasIssues'}
|
|
494
|
+
Fix Errors
|
|
495
|
+
{:else}
|
|
496
|
+
Submit Form
|
|
497
|
+
{/if}
|
|
498
|
+
{/snippet}
|
|
499
|
+
</Sbutton>
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
The `state` parameter is a discriminated union of type `ButtonState<T>` where `T` is inferred from the form's result type:
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
type ButtonState<T = unknown> =
|
|
506
|
+
| { state: 'default'; pending: false; success: false; hasIssues: false; result: undefined }
|
|
507
|
+
| { state: 'pending'; pending: true; success: false; hasIssues: false; result: undefined }
|
|
508
|
+
| { state: 'success'; pending: false; success: true; hasIssues: false; result: T }
|
|
509
|
+
| { state: 'hasIssues'; pending: false; success: false; hasIssues: true; result: undefined };
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
#### Typed Result Access
|
|
513
|
+
|
|
514
|
+
The result type is automatically inferred from the `form` prop. When your remote function returns a typed result, you can access it directly:
|
|
515
|
+
|
|
516
|
+
```svelte
|
|
517
|
+
<script lang="ts">
|
|
518
|
+
import { login } from './auth.remote'; // Returns { success: boolean; message: string }
|
|
519
|
+
</script>
|
|
520
|
+
|
|
521
|
+
<Sbutton form={login} class="submit-btn">
|
|
522
|
+
{#snippet children(state)}
|
|
523
|
+
{#if state.state === 'success'}
|
|
524
|
+
{state.result.message} <!-- TypeScript knows this is string -->
|
|
525
|
+
{:else if state.state === 'pending'}
|
|
526
|
+
Logging in...
|
|
527
|
+
{:else}
|
|
528
|
+
Login
|
|
529
|
+
{/if}
|
|
530
|
+
{/snippet}
|
|
531
|
+
</Sbutton>
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
| Prop | Type | Default | Description |
|
|
535
|
+
| ------------ | ------------------------------------------- | ----------- | ----------------------------------------------------- |
|
|
536
|
+
| `form` | `RemoteForm` | required | Remote form for type inference |
|
|
537
|
+
| `label` | `string \| Snippet<[ButtonState<T>]>` | `'Submit'` | Button text, or a snippet with typed state |
|
|
538
|
+
| `buttonType` | `'submit' \| 'reset' \| 'button'` | `'submit'` | Button type |
|
|
539
|
+
| `class` | `string` | `undefined` | CSS class |
|
|
540
|
+
| `disabled` | `boolean` | `false` | Disable button |
|
|
541
|
+
| `children` | `Snippet<[ButtonState<T>]>` | `undefined` | Custom content with typed state; takes precedence over `label` |
|
|
542
|
+
| `onsubmit` | `() => void \| Promise<void>` | `undefined` | Callback before lifecycle `beforeSubmit` and submit |
|
|
543
|
+
|
|
544
|
+
`children` and `label` (as a snippet) both receive the same typed `ButtonState<T>` and render identically; `children` wins if both are provided.
|
|
545
|
+
|
|
546
|
+
### `<SIssues>`
|
|
547
|
+
|
|
548
|
+
Displays form-level issues and issues not shown by any Sfield component (e.g., hidden field issues or programmatic validation via `invalid()`).
|
|
549
|
+
|
|
550
|
+
```svelte
|
|
551
|
+
<SIssues message="There are some issues with your form:" />
|
|
552
|
+
|
|
553
|
+
<!-- With custom message snippet -->
|
|
554
|
+
<SIssues>
|
|
555
|
+
{#snippet message()}
|
|
556
|
+
<strong>⚠️ Please fix the following issues:</strong>
|
|
557
|
+
{/snippet}
|
|
558
|
+
</SIssues>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
| Prop | Type | Default | Description |
|
|
562
|
+
| ----------- | ------------------- | --------------------- | --------------------------------- |
|
|
563
|
+
| `message` | `string \| Snippet` | `undefined` | General message shown when issues |
|
|
564
|
+
| `class` | `string` | `'sform-issues'` | CSS class for wrapper |
|
|
565
|
+
| `listClass` | `string` | `'sform-issues-list'` | CSS class for issues list |
|
|
566
|
+
|
|
567
|
+
The component filters issues to only show:
|
|
568
|
+
|
|
569
|
+
- Form-level issues (from `invalid("message")`)
|
|
570
|
+
- Field issues for hidden inputs (no Sfield displays them)
|
|
571
|
+
- Issues for fields without a corresponding Sfield
|
|
572
|
+
|
|
573
|
+
### `<SResult>`
|
|
574
|
+
|
|
575
|
+
Displays form result with typed access. Only renders when the form has a result. Pass the `form` prop to enable typed result access in the children snippet.
|
|
576
|
+
|
|
577
|
+
```svelte
|
|
578
|
+
<SResult form={myLogin} class="sform-result sform-result-success">
|
|
579
|
+
{#snippet children(result)}
|
|
580
|
+
{result.message}
|
|
581
|
+
{/snippet}
|
|
582
|
+
</SResult>
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
The `result` parameter is typed based on your remote function's return type:
|
|
586
|
+
|
|
587
|
+
```svelte
|
|
588
|
+
<script lang="ts">
|
|
589
|
+
import { login } from './auth.remote'; // Returns { success: boolean; message: string }
|
|
590
|
+
</script>
|
|
591
|
+
|
|
592
|
+
<SResult form={login} class="success-message">
|
|
593
|
+
{#snippet children(result)}
|
|
594
|
+
<!-- TypeScript knows result is { success: boolean; message: string } -->
|
|
595
|
+
<h2>Welcome!</h2>
|
|
596
|
+
<p>{result.message}</p>
|
|
597
|
+
{/snippet}
|
|
598
|
+
</SResult>
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
| Prop | Type | Default | Description |
|
|
602
|
+
| ---------- | -------------- | ----------- | ------------------------------ |
|
|
603
|
+
| `form` | `RemoteForm` | required | Remote form for type inference |
|
|
604
|
+
| `children` | `Snippet<[T]>` | required | Content with typed result |
|
|
605
|
+
| `class` | `string` | `undefined` | CSS class for wrapper |
|
|
606
|
+
|
|
607
|
+
The component only renders when `form.result !== undefined`, so the `result` parameter in the children snippet is guaranteed to be defined.
|
|
608
|
+
|
|
609
|
+
## Styling
|
|
610
|
+
|
|
611
|
+
### CSS Classes
|
|
612
|
+
|
|
613
|
+
Sfield adds these classes automatically:
|
|
614
|
+
|
|
615
|
+
- `.sform-field` - Wrapper element
|
|
616
|
+
- `.sform-label` - Label element
|
|
617
|
+
- `.sform-input` - Input element
|
|
618
|
+
- `.sform-messages` - Error messages container
|
|
619
|
+
- `.sform-field-error` - Added to wrapper when field has errors
|
|
620
|
+
|
|
621
|
+
### Custom Classes
|
|
622
|
+
|
|
623
|
+
```svelte
|
|
624
|
+
<!-- String class applies to wrapper -->
|
|
625
|
+
<Sfield field={fields.email} type="email" class="my-field" />
|
|
626
|
+
|
|
627
|
+
<!-- Object for granular control -->
|
|
628
|
+
<Sfield
|
|
629
|
+
field={fields.email}
|
|
630
|
+
type="email"
|
|
631
|
+
class={{
|
|
632
|
+
wrapper: 'field-wrapper',
|
|
633
|
+
label: 'field-label',
|
|
634
|
+
input: 'field-input',
|
|
635
|
+
messages: 'field-errors'
|
|
636
|
+
}}
|
|
637
|
+
/>
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## Validation
|
|
641
|
+
|
|
642
|
+
Sform uses preflight validation with Valibot schemas. Native browser validation (required, minlength, pattern) is disabled to allow showing all errors at once on submit.
|
|
643
|
+
|
|
644
|
+
### Schema Example
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
import * as v from 'valibot';
|
|
648
|
+
|
|
649
|
+
const signupSchema = v.object({
|
|
650
|
+
email: v.pipe(v.string(), v.email('Please enter a valid email')),
|
|
651
|
+
_password: v.pipe(
|
|
652
|
+
v.string(),
|
|
653
|
+
v.minLength(8, 'Password must be at least 8 characters'),
|
|
654
|
+
v.regex(/[A-Z]/, 'Password must contain an uppercase letter'),
|
|
655
|
+
v.regex(/[0-9]/, 'Password must contain a number')
|
|
656
|
+
),
|
|
657
|
+
age: v.pipe(v.number(), v.minValue(18, 'Must be at least 18 years old'))
|
|
658
|
+
});
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## Type Safety
|
|
662
|
+
|
|
663
|
+
Sform uses TypeScript discriminated unions to provide type-safe props for each input type:
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
// ✅ TypeScript knows 'showToggle' is only valid for password type
|
|
667
|
+
<Sfield field={fields._password} type="password" showToggle={false} />
|
|
668
|
+
|
|
669
|
+
// ✅ TypeScript knows 'options' is required for select type
|
|
670
|
+
<Sfield field={fields.country} type="select" options={countries} />
|
|
671
|
+
|
|
672
|
+
// ✅ TypeScript knows 'min', 'max', 'step' are valid for number type
|
|
673
|
+
<Sfield field={fields.age} type="number" min={0} max={150} />
|
|
674
|
+
|
|
675
|
+
// ❌ TypeScript error: 'showToggle' doesn't exist on text type
|
|
676
|
+
<Sfield field={fields.username} type="text" showToggle />
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
## Development
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
# Install dependencies
|
|
683
|
+
npm install
|
|
684
|
+
|
|
685
|
+
# Start dev server
|
|
686
|
+
npm run dev
|
|
687
|
+
|
|
688
|
+
# Run tests
|
|
689
|
+
npm test
|
|
690
|
+
|
|
691
|
+
# Build library
|
|
692
|
+
npm run package
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
## License
|
|
696
|
+
|
|
697
|
+
MIT
|