@samuel-charpentier/sform 0.0.2 → 0.0.4
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 +281 -76
- package/dist/Sform/SIssues.svelte +49 -0
- package/dist/Sform/SIssues.svelte.d.ts +14 -0
- package/dist/Sform/SResult.svelte +36 -0
- package/dist/Sform/SResult.svelte.d.ts +39 -0
- package/dist/Sform/Sfield.svelte +7 -1
- package/dist/Sform/Sform.svelte +2 -1
- package/dist/Sform/context.svelte.d.ts +11 -2
- package/dist/Sform/context.svelte.js +62 -2
- package/dist/Sform/index.d.ts +3 -1
- package/dist/Sform/index.js +2 -0
- package/dist/Sform/inputs/ButtonInput.svelte +26 -45
- package/dist/Sform/inputs/ButtonInput.svelte.d.ts +43 -29
- package/dist/Sform/inputs/CheckboxGroupInput.svelte +3 -16
- package/dist/Sform/inputs/HiddenInput.svelte +10 -0
- package/dist/Sform/inputs/HiddenInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/RadioInput.svelte +3 -5
- package/dist/Sform/inputs/TextInput.svelte +26 -30
- package/dist/Sform/sform.css +14 -0
- package/dist/Sform/types.d.ts +95 -15
- package/dist/Sform/utils/Fieldset.svelte +24 -0
- package/dist/Sform/utils/Fieldset.svelte.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
A type-safe form library for **Svelte 5** with **SvelteKit remote functions**.
|
|
4
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
|
+
- [Toggle Options](#toggle-options)
|
|
27
|
+
- [Masked Input](#masked-input)
|
|
28
|
+
- [Hidden Input](#hidden-input)
|
|
29
|
+
- [`<Sbutton>`](#sbutton)
|
|
30
|
+
- [`<SIssues>`](#sissues)
|
|
31
|
+
- [`<SResult>`](#sresult)
|
|
32
|
+
- [Styling](#styling)
|
|
33
|
+
- [Validation](#validation)
|
|
34
|
+
- [Type Safety](#type-safety)
|
|
35
|
+
- [Development](#development)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
|
|
5
38
|
## Features
|
|
6
39
|
|
|
7
40
|
- ✅ **Type-safe** - Discriminated union types for each input type
|
|
@@ -69,10 +102,12 @@ export const login = form(loginSchema, async ({ username, _password }) => {
|
|
|
69
102
|
</script>
|
|
70
103
|
|
|
71
104
|
<Sform form={login} validateOn="blur">
|
|
72
|
-
|
|
73
|
-
|
|
105
|
+
{#snippet children(fields)}
|
|
106
|
+
<Sfield field={fields.username} type="text" label="Username" />
|
|
107
|
+
<Sfield field={fields._password} type="password" label="Password" />
|
|
74
108
|
|
|
75
|
-
|
|
109
|
+
<Sbutton form={login} label="Login" />
|
|
110
|
+
{/snippet}
|
|
76
111
|
</Sform>
|
|
77
112
|
```
|
|
78
113
|
|
|
@@ -84,7 +119,9 @@ Wrapper component that provides form context to all child fields.
|
|
|
84
119
|
|
|
85
120
|
```svelte
|
|
86
121
|
<Sform form={remoteForm} validateOn="blur" class="my-form">
|
|
87
|
-
|
|
122
|
+
{#snippet children(fields)}
|
|
123
|
+
<!-- Sfield components here -->
|
|
124
|
+
{/snippet}
|
|
88
125
|
</Sform>
|
|
89
126
|
```
|
|
90
127
|
|
|
@@ -106,27 +143,28 @@ Smart field component with type-safe props based on input type.
|
|
|
106
143
|
|
|
107
144
|
#### Common Props (all types)
|
|
108
145
|
|
|
109
|
-
| Prop | Type | Default | Description
|
|
110
|
-
| ------------- | ------------------------- | ----------- |
|
|
111
|
-
| `
|
|
112
|
-
| `type` | `InputType` | required | Input type
|
|
113
|
-
| `label` | `string` | `undefined` | Field label
|
|
114
|
-
| `placeholder` | `string` | `undefined` | Placeholder text
|
|
115
|
-
| `disabled` | `boolean` | `false` | Disable the field
|
|
116
|
-
| `readonly` | `boolean` | `false` | Make field readonly
|
|
117
|
-
| `validateOn` | `ValidateOn` | inherited | Override form validateOn
|
|
118
|
-
| `class` | `SfieldClasses \| string` | `undefined` | CSS classes
|
|
146
|
+
| Prop | Type | Default | Description |
|
|
147
|
+
| ------------- | ------------------------- | ----------- | ------------------------------------- |
|
|
148
|
+
| `field` | `RemoteFormField` | required | Field from `fields` snippet parameter |
|
|
149
|
+
| `type` | `InputType` | required | Input type |
|
|
150
|
+
| `label` | `string` | `undefined` | Field label |
|
|
151
|
+
| `placeholder` | `string` | `undefined` | Placeholder text (text/password/etc) |
|
|
152
|
+
| `disabled` | `boolean` | `false` | Disable the field |
|
|
153
|
+
| `readonly` | `boolean` | `false` | Make field readonly |
|
|
154
|
+
| `validateOn` | `ValidateOn` | inherited | Override form validateOn |
|
|
155
|
+
| `class` | `SfieldClasses \| string` | `undefined` | CSS classes |
|
|
156
|
+
| `hint` | `string \| Snippet` | `undefined` | Help text shown below the field |
|
|
119
157
|
|
|
120
158
|
#### Text Inputs
|
|
121
159
|
|
|
122
160
|
```svelte
|
|
123
|
-
<Sfield
|
|
124
|
-
<Sfield
|
|
125
|
-
<Sfield
|
|
126
|
-
<Sfield
|
|
161
|
+
<Sfield field={fields.email} type="email" label="Email" placeholder="you@example.com" />
|
|
162
|
+
<Sfield field={fields.search} type="search" label="Search" />
|
|
163
|
+
<Sfield field={fields.phone} type="tel" label="Phone" />
|
|
164
|
+
<Sfield field={fields.website} type="url" label="Website" prefix="https://" />
|
|
127
165
|
```
|
|
128
166
|
|
|
129
|
-
Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime-local`, `time`, `month`, `week`, `color`, `file
|
|
167
|
+
Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime-local`, `time`, `month`, `week`, `color`, `file`
|
|
130
168
|
|
|
131
169
|
| Prop | Type | Default | Description |
|
|
132
170
|
| -------- | ------------------- | ----------- | -------------------- |
|
|
@@ -136,20 +174,32 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
136
174
|
#### Password Input
|
|
137
175
|
|
|
138
176
|
```svelte
|
|
139
|
-
<Sfield
|
|
140
|
-
<Sfield
|
|
177
|
+
<Sfield field={fields._password} type="password" label="Password" />
|
|
178
|
+
<Sfield field={fields._password} type="password" label="Password" showToggle={false} />
|
|
179
|
+
<Sfield field={fields._password} type="password" label="Password">
|
|
180
|
+
{#snippet showToggleIcon(passwordShown)}
|
|
181
|
+
{#if passwordShown}
|
|
182
|
+
🙈
|
|
183
|
+
{:else}
|
|
184
|
+
👁️
|
|
185
|
+
{/if}
|
|
186
|
+
{/snippet}
|
|
187
|
+
</Sfield>
|
|
141
188
|
```
|
|
142
189
|
|
|
143
|
-
| Prop
|
|
144
|
-
|
|
|
145
|
-
| `showToggle`
|
|
190
|
+
| Prop | Type | Default | Description |
|
|
191
|
+
| ---------------- | ----------------------------------- | ----------- | ---------------------------------- |
|
|
192
|
+
| `showToggle` | `boolean` | `true` | Show eye icon to toggle visibility |
|
|
193
|
+
| `showToggleIcon` | `Snippet<[passwordShown: boolean]>` | `undefined` | Custom toggle icon snippet |
|
|
194
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input |
|
|
195
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input |
|
|
146
196
|
|
|
147
197
|
#### Number Input
|
|
148
198
|
|
|
149
199
|
```svelte
|
|
150
|
-
<Sfield
|
|
151
|
-
<Sfield
|
|
152
|
-
<Sfield
|
|
200
|
+
<Sfield field={fields.age} type="number" label="Age" min={0} max={150} step={1} />
|
|
201
|
+
<Sfield field={fields.price} type="number" label="Price" prefix="$" suffix="USD" align="end" />
|
|
202
|
+
<Sfield field={fields.quantity} type="number" label="Qty" showControls={false} maxDecimals={0} />
|
|
153
203
|
```
|
|
154
204
|
|
|
155
205
|
| Prop | Type | Default | Description |
|
|
@@ -162,18 +212,26 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
162
212
|
| `showControls` | `boolean` | `true` | Show spinner controls |
|
|
163
213
|
| `align` | `'start' \| 'end'` | `'start'` | Text alignment |
|
|
164
214
|
| `maxDecimals` | `number` | `undefined` | Max decimal places (0 = integers only) |
|
|
215
|
+
| `autocomplete` | `string` | `undefined` | HTML autocomplete attribute |
|
|
165
216
|
|
|
166
217
|
#### Textarea
|
|
167
218
|
|
|
168
219
|
```svelte
|
|
169
|
-
<Sfield
|
|
220
|
+
<Sfield field={fields.bio} type="textarea" label="Bio" placeholder="Tell us about yourself" />
|
|
221
|
+
<Sfield field={fields.notes} type="textarea" label="Notes" prefix="📝" suffix="(max 500 chars)" />
|
|
170
222
|
```
|
|
171
223
|
|
|
224
|
+
| Prop | Type | Default | Description |
|
|
225
|
+
| -------------- | ------------------- | ----------- | --------------------------- |
|
|
226
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input |
|
|
227
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input |
|
|
228
|
+
| `autocomplete` | `string` | `undefined` | HTML autocomplete attribute |
|
|
229
|
+
|
|
172
230
|
#### Select
|
|
173
231
|
|
|
174
232
|
```svelte
|
|
175
233
|
<Sfield
|
|
176
|
-
|
|
234
|
+
field={fields.country}
|
|
177
235
|
type="select"
|
|
178
236
|
label="Country"
|
|
179
237
|
options={[
|
|
@@ -191,14 +249,14 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
191
249
|
#### Checkbox
|
|
192
250
|
|
|
193
251
|
```svelte
|
|
194
|
-
<Sfield
|
|
252
|
+
<Sfield field={fields.subscribe} type="checkbox" label="Subscribe to newsletter" />
|
|
195
253
|
```
|
|
196
254
|
|
|
197
255
|
#### Radio
|
|
198
256
|
|
|
199
257
|
```svelte
|
|
200
258
|
<Sfield
|
|
201
|
-
|
|
259
|
+
field={fields.plan}
|
|
202
260
|
type="radio"
|
|
203
261
|
label="Plan"
|
|
204
262
|
options={[
|
|
@@ -216,7 +274,16 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
216
274
|
#### Range
|
|
217
275
|
|
|
218
276
|
```svelte
|
|
219
|
-
<Sfield
|
|
277
|
+
<Sfield field={fields.volume} type="range" label="Volume" min={0} max={100} step={5} showValue />
|
|
278
|
+
<Sfield
|
|
279
|
+
field={fields.brightness}
|
|
280
|
+
type="range"
|
|
281
|
+
label="Brightness"
|
|
282
|
+
min={0}
|
|
283
|
+
max={100}
|
|
284
|
+
formatValue={(v) => `${v}%`}
|
|
285
|
+
showValue
|
|
286
|
+
/>
|
|
220
287
|
```
|
|
221
288
|
|
|
222
289
|
| Prop | Type | Default | Description |
|
|
@@ -230,8 +297,8 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
230
297
|
#### Toggle
|
|
231
298
|
|
|
232
299
|
```svelte
|
|
233
|
-
<Sfield
|
|
234
|
-
<Sfield
|
|
300
|
+
<Sfield field={fields.notifications} type="toggle" label="Enable Notifications" />
|
|
301
|
+
<Sfield field={fields.darkMode} type="toggle" label="Theme" onLabel="Dark" offLabel="Light" />
|
|
235
302
|
```
|
|
236
303
|
|
|
237
304
|
| Prop | Type | Default | Description |
|
|
@@ -245,7 +312,7 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
245
312
|
|
|
246
313
|
```svelte
|
|
247
314
|
<Sfield
|
|
248
|
-
|
|
315
|
+
field={fields.theme}
|
|
249
316
|
type="toggle-options"
|
|
250
317
|
label="Theme"
|
|
251
318
|
options={[
|
|
@@ -254,6 +321,18 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
254
321
|
{ value: 'auto', label: 'Auto' }
|
|
255
322
|
]}
|
|
256
323
|
/>
|
|
324
|
+
<!-- Multiple selection -->
|
|
325
|
+
<Sfield
|
|
326
|
+
field={fields.features}
|
|
327
|
+
type="toggle-options"
|
|
328
|
+
label="Features"
|
|
329
|
+
multiple={true}
|
|
330
|
+
options={[
|
|
331
|
+
{ value: 'push', label: 'Push Notifications' },
|
|
332
|
+
{ value: 'email', label: 'Email' },
|
|
333
|
+
{ value: 'sms', label: 'SMS' }
|
|
334
|
+
]}
|
|
335
|
+
/>
|
|
257
336
|
```
|
|
258
337
|
|
|
259
338
|
| Prop | Type | Default | Description |
|
|
@@ -264,19 +343,28 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
264
343
|
#### Masked Input
|
|
265
344
|
|
|
266
345
|
```svelte
|
|
267
|
-
<Sfield
|
|
268
|
-
<Sfield
|
|
269
|
-
<Sfield
|
|
346
|
+
<Sfield field={fields.phone} type="masked" label="Phone" mask="(###) ###-####" />
|
|
347
|
+
<Sfield field={fields.creditCard} type="masked" label="Credit Card" mask="#### #### #### ####" />
|
|
348
|
+
<Sfield field={fields.ssn} type="masked" label="SSN" mask="###-##-####" />
|
|
349
|
+
<!-- Custom tokens -->
|
|
350
|
+
<Sfield
|
|
351
|
+
field={fields.code}
|
|
352
|
+
type="masked"
|
|
353
|
+
label="Code"
|
|
354
|
+
mask="AAAA-99-LL"
|
|
355
|
+
tokens={{ A: /[A-Z]/, L: /[a-z]/ }}
|
|
356
|
+
/>
|
|
270
357
|
```
|
|
271
358
|
|
|
272
|
-
| Prop | Type
|
|
273
|
-
| --------------------- |
|
|
274
|
-
| `mask` | `string`
|
|
275
|
-
| `
|
|
276
|
-
| `
|
|
277
|
-
| `
|
|
278
|
-
| `
|
|
279
|
-
| `
|
|
359
|
+
| Prop | Type | Default | Description |
|
|
360
|
+
| --------------------- | ------------------------ | ----------- | -------------------------------- |
|
|
361
|
+
| `mask` | `string` | required | Mask pattern |
|
|
362
|
+
| `tokens` | `Record<string, RegExp>` | `undefined` | Custom token definitions |
|
|
363
|
+
| `maskPlaceholder` | `string` | `'_'` | Placeholder character |
|
|
364
|
+
| `showMaskPlaceholder` | `boolean` | `false` | Show full mask with placeholders |
|
|
365
|
+
| `unmaskValue` | `boolean` | `true` | Store unmasked value |
|
|
366
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input |
|
|
367
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input |
|
|
280
368
|
|
|
281
369
|
**Mask Tokens:**
|
|
282
370
|
|
|
@@ -285,41 +373,158 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
285
373
|
- `A` - Alphabetic uppercase
|
|
286
374
|
- `*` - Alphanumeric
|
|
287
375
|
|
|
376
|
+
#### Hidden Input
|
|
377
|
+
|
|
378
|
+
```svelte
|
|
379
|
+
<Sfield field={fields.token} type="hidden" value={authToken} />
|
|
380
|
+
<Sfield field={fields.userId} type="hidden" value="12345" />
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
| Prop | Type | Default | Description |
|
|
384
|
+
| ------- | -------- | ------- | ------------------------------------------------ |
|
|
385
|
+
| `value` | `string` | `''` | The value for the hidden field (can be reactive) |
|
|
386
|
+
|
|
387
|
+
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:
|
|
388
|
+
|
|
389
|
+
```svelte
|
|
390
|
+
<script lang="ts">
|
|
391
|
+
let token = $state(initialToken);
|
|
392
|
+
|
|
393
|
+
async function refreshToken() {
|
|
394
|
+
token = await getNewToken();
|
|
395
|
+
}
|
|
396
|
+
</script>
|
|
397
|
+
|
|
398
|
+
<Sfield field={fields.token} type="hidden" value={token} />
|
|
399
|
+
```
|
|
400
|
+
|
|
288
401
|
### `<Sbutton>`
|
|
289
402
|
|
|
290
|
-
Stateful submit button that reacts to form state.
|
|
403
|
+
Stateful submit button that reacts to form state. Pass the `form` prop to enable typed result access.
|
|
404
|
+
|
|
405
|
+
```svelte
|
|
406
|
+
<Sbutton form={myForm} label="Submit" class="my-button" />
|
|
407
|
+
|
|
408
|
+
<!-- With custom state rendering -->
|
|
409
|
+
<Sbutton form={myForm} class="submit-btn">
|
|
410
|
+
{#snippet children(state)}
|
|
411
|
+
{#if state.state === 'pending'}
|
|
412
|
+
Submitting...
|
|
413
|
+
{:else if state.state === 'success'}
|
|
414
|
+
✓ {state.result.message}
|
|
415
|
+
{:else if state.state === 'hasIssues'}
|
|
416
|
+
Fix Errors
|
|
417
|
+
{:else}
|
|
418
|
+
Submit Form
|
|
419
|
+
{/if}
|
|
420
|
+
{/snippet}
|
|
421
|
+
</Sbutton>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The `state` parameter is a discriminated union of type `ButtonState<T>` where `T` is inferred from the form's result type:
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
type ButtonState<T = unknown> =
|
|
428
|
+
| { state: 'default'; pending: false; success: false; hasIssues: false; result: undefined }
|
|
429
|
+
| { state: 'pending'; pending: true; success: false; hasIssues: false; result: undefined }
|
|
430
|
+
| { state: 'success'; pending: false; success: true; hasIssues: false; result: T }
|
|
431
|
+
| { state: 'hasIssues'; pending: false; success: false; hasIssues: true; result: undefined };
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
#### Typed Result Access
|
|
435
|
+
|
|
436
|
+
The result type is automatically inferred from the `form` prop. When your remote function returns a typed result, you can access it directly:
|
|
291
437
|
|
|
292
438
|
```svelte
|
|
293
|
-
<
|
|
439
|
+
<script lang="ts">
|
|
440
|
+
import { login } from './auth.remote'; // Returns { success: boolean; message: string }
|
|
441
|
+
</script>
|
|
294
442
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
443
|
+
<Sbutton form={login} class="submit-btn">
|
|
444
|
+
{#snippet children(state)}
|
|
445
|
+
{#if state.state === 'success'}
|
|
446
|
+
{state.result.message} <!-- TypeScript knows this is string -->
|
|
447
|
+
{:else if state.state === 'pending'}
|
|
448
|
+
Logging in...
|
|
449
|
+
{:else}
|
|
450
|
+
Login
|
|
451
|
+
{/if}
|
|
299
452
|
{/snippet}
|
|
300
|
-
|
|
301
|
-
|
|
453
|
+
</Sbutton>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
| Prop | Type | Default | Description |
|
|
457
|
+
| ------------ | --------------------------------- | ----------- | --------------------------------- |
|
|
458
|
+
| `form` | `RemoteForm` | required | Remote form for type inference |
|
|
459
|
+
| `label` | `string` | `'Submit'` | Button text (when no children) |
|
|
460
|
+
| `buttonType` | `'submit' \| 'reset' \| 'button'` | `'submit'` | Button type |
|
|
461
|
+
| `class` | `string` | `undefined` | CSS class |
|
|
462
|
+
| `disabled` | `boolean` | `false` | Disable button |
|
|
463
|
+
| `children` | `Snippet<[ButtonState<T>]>` | `undefined` | Custom content with typed state |
|
|
464
|
+
| `onsubmit` | `() => void \| Promise<void>` | `undefined` | Callback before validation/submit |
|
|
465
|
+
|
|
466
|
+
### `<SIssues>`
|
|
467
|
+
|
|
468
|
+
Displays form-level issues and issues not shown by any Sfield component (e.g., hidden field issues or programmatic validation via `invalid()`).
|
|
469
|
+
|
|
470
|
+
```svelte
|
|
471
|
+
<SIssues message="There are some issues with your form:" />
|
|
472
|
+
|
|
473
|
+
<!-- With custom message snippet -->
|
|
474
|
+
<SIssues>
|
|
475
|
+
{#snippet message()}
|
|
476
|
+
<strong>⚠️ Please fix the following issues:</strong>
|
|
302
477
|
{/snippet}
|
|
303
|
-
|
|
304
|
-
|
|
478
|
+
</SIssues>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
| Prop | Type | Default | Description |
|
|
482
|
+
| ----------- | ------------------- | --------------------- | --------------------------------- |
|
|
483
|
+
| `message` | `string \| Snippet` | `undefined` | General message shown when issues |
|
|
484
|
+
| `class` | `string` | `'sform-issues'` | CSS class for wrapper |
|
|
485
|
+
| `listClass` | `string` | `'sform-issues-list'` | CSS class for issues list |
|
|
486
|
+
|
|
487
|
+
The component filters issues to only show:
|
|
488
|
+
|
|
489
|
+
- Form-level issues (from `invalid("message")`)
|
|
490
|
+
- Field issues for hidden inputs (no Sfield displays them)
|
|
491
|
+
- Issues for fields without a corresponding Sfield
|
|
492
|
+
|
|
493
|
+
### `<SResult>`
|
|
494
|
+
|
|
495
|
+
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.
|
|
496
|
+
|
|
497
|
+
```svelte
|
|
498
|
+
<SResult form={myLogin} class="sform-result sform-result-success">
|
|
499
|
+
{#snippet children(result)}
|
|
500
|
+
{result.message}
|
|
305
501
|
{/snippet}
|
|
306
|
-
|
|
307
|
-
|
|
502
|
+
</SResult>
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
The `result` parameter is typed based on your remote function's return type:
|
|
506
|
+
|
|
507
|
+
```svelte
|
|
508
|
+
<script lang="ts">
|
|
509
|
+
import { login } from './auth.remote'; // Returns { success: boolean; message: string }
|
|
510
|
+
</script>
|
|
511
|
+
|
|
512
|
+
<SResult form={login} class="success-message">
|
|
513
|
+
{#snippet children(result)}
|
|
514
|
+
<!-- TypeScript knows result is { success: boolean; message: string } -->
|
|
515
|
+
<h2>Welcome!</h2>
|
|
516
|
+
<p>{result.message}</p>
|
|
308
517
|
{/snippet}
|
|
309
|
-
</
|
|
518
|
+
</SResult>
|
|
310
519
|
```
|
|
311
520
|
|
|
312
|
-
| Prop
|
|
313
|
-
|
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
316
|
-
| `class`
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
| `defaultState` | `Snippet` | `undefined` | Default state snippet |
|
|
320
|
-
| `pendingState` | `Snippet` | `undefined` | Pending state snippet |
|
|
321
|
-
| `successState` | `Snippet` | `undefined` | Success state snippet |
|
|
322
|
-
| `errorState` | `Snippet` | `undefined` | Error state snippet |
|
|
521
|
+
| Prop | Type | Default | Description |
|
|
522
|
+
| ---------- | -------------- | ----------- | ------------------------------ |
|
|
523
|
+
| `form` | `RemoteForm` | required | Remote form for type inference |
|
|
524
|
+
| `children` | `Snippet<[T]>` | required | Content with typed result |
|
|
525
|
+
| `class` | `string` | `undefined` | CSS class for wrapper |
|
|
526
|
+
|
|
527
|
+
The component only renders when `form.result !== undefined`, so the `result` parameter in the children snippet is guaranteed to be defined.
|
|
323
528
|
|
|
324
529
|
## Styling
|
|
325
530
|
|
|
@@ -337,11 +542,11 @@ Sfield adds these classes automatically:
|
|
|
337
542
|
|
|
338
543
|
```svelte
|
|
339
544
|
<!-- String class applies to wrapper -->
|
|
340
|
-
<Sfield
|
|
545
|
+
<Sfield field={fields.email} type="email" class="my-field" />
|
|
341
546
|
|
|
342
547
|
<!-- Object for granular control -->
|
|
343
548
|
<Sfield
|
|
344
|
-
|
|
549
|
+
field={fields.email}
|
|
345
550
|
type="email"
|
|
346
551
|
class={{
|
|
347
552
|
wrapper: 'field-wrapper',
|
|
@@ -379,16 +584,16 @@ Sform uses TypeScript discriminated unions to provide type-safe props for each i
|
|
|
379
584
|
|
|
380
585
|
```typescript
|
|
381
586
|
// ✅ TypeScript knows 'showToggle' is only valid for password type
|
|
382
|
-
<Sfield
|
|
587
|
+
<Sfield field={fields._password} type="password" showToggle={false} />
|
|
383
588
|
|
|
384
589
|
// ✅ TypeScript knows 'options' is required for select type
|
|
385
|
-
<Sfield
|
|
590
|
+
<Sfield field={fields.country} type="select" options={countries} />
|
|
386
591
|
|
|
387
592
|
// ✅ TypeScript knows 'min', 'max', 'step' are valid for number type
|
|
388
|
-
<Sfield
|
|
593
|
+
<Sfield field={fields.age} type="number" min={0} max={150} />
|
|
389
594
|
|
|
390
595
|
// ❌ TypeScript error: 'showToggle' doesn't exist on text type
|
|
391
|
-
<Sfield
|
|
596
|
+
<Sfield field={fields.username} type="text" showToggle />
|
|
392
597
|
```
|
|
393
598
|
|
|
394
599
|
## Development
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { getSformContext } from './context.svelte.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** General message shown when there are any issues (string or snippet) */
|
|
7
|
+
message?: string | Snippet;
|
|
8
|
+
/** Display message only if */
|
|
9
|
+
showMessageIf?: 'noUnhandledIssue' | 'hasUnhandledIssue' | 'hasAnyIssue';
|
|
10
|
+
/** CSS class for the wrapper */
|
|
11
|
+
class?: string;
|
|
12
|
+
/** CSS class for the issues list */
|
|
13
|
+
listClass?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { message, class: className, listClass, showMessageIf = 'hasAnyIssue' }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const context = getSformContext();
|
|
19
|
+
|
|
20
|
+
// Get unhandled issues from context (issues not displayed by any Sfield)
|
|
21
|
+
const unhandledIssues = $derived(context.getUnhandledIssues());
|
|
22
|
+
const formState = $derived(context.getFormState());
|
|
23
|
+
|
|
24
|
+
// Show issues only after form submission and when there are issues
|
|
25
|
+
const shouldShow = $derived(context.submitted && formState.hasIssues);
|
|
26
|
+
const hasUnhandledIssues = $derived(unhandledIssues.length > 0);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
{#if shouldShow}
|
|
30
|
+
<div class={className ?? 'sform-issues'}>
|
|
31
|
+
{#if message && (showMessageIf === 'hasAnyIssue' || (showMessageIf === 'noUnhandledIssue' && !hasUnhandledIssues) || (showMessageIf === 'hasUnhandledIssue' && hasUnhandledIssues))}
|
|
32
|
+
<div class="sform-issues-message">
|
|
33
|
+
{#if typeof message === 'string'}
|
|
34
|
+
{message}
|
|
35
|
+
{:else}
|
|
36
|
+
{@render message()}
|
|
37
|
+
{/if}
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
|
|
41
|
+
{#if hasUnhandledIssues}
|
|
42
|
+
<ul class={listClass ?? 'sform-issues-list'}>
|
|
43
|
+
{#each unhandledIssues as issue, i (i)}
|
|
44
|
+
<li>{issue.message}</li>
|
|
45
|
+
{/each}
|
|
46
|
+
</ul>
|
|
47
|
+
{/if}
|
|
48
|
+
</div>
|
|
49
|
+
{/if}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** General message shown when there are any issues (string or snippet) */
|
|
4
|
+
message?: string | Snippet;
|
|
5
|
+
/** Display message only if */
|
|
6
|
+
showMessageIf?: 'noUnhandledIssue' | 'hasUnhandledIssue' | 'hasAnyIssue';
|
|
7
|
+
/** CSS class for the wrapper */
|
|
8
|
+
class?: string;
|
|
9
|
+
/** CSS class for the issues list */
|
|
10
|
+
listClass?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const SIssues: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type SIssues = ReturnType<typeof SIssues>;
|
|
14
|
+
export default SIssues;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts" generics="T = unknown">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { RemoteFormIssue } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal form shape needed for type inference.
|
|
7
|
+
* This allows the component to infer T from the form's result type.
|
|
8
|
+
*/
|
|
9
|
+
interface FormLike<Output> {
|
|
10
|
+
result?: Output;
|
|
11
|
+
fields: {
|
|
12
|
+
allIssues?: () => RemoteFormIssue[] | undefined;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** The remote form - used to infer the result type T */
|
|
19
|
+
form: FormLike<T>;
|
|
20
|
+
/** Children snippet receives the result (guaranteed to be defined) */
|
|
21
|
+
children: Snippet<[T]>;
|
|
22
|
+
/** CSS class for the wrapper */
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { form, children, class: className }: Props = $props();
|
|
27
|
+
|
|
28
|
+
// Only show when there's a result
|
|
29
|
+
const hasResult = $derived(form.result !== undefined);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
{#if hasResult}
|
|
33
|
+
<div class={className}>
|
|
34
|
+
{@render children(form.result as T)}
|
|
35
|
+
</div>
|
|
36
|
+
{/if}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { RemoteFormIssue } from './types.js';
|
|
3
|
+
declare function $$render<T = unknown>(): {
|
|
4
|
+
props: {
|
|
5
|
+
/** The remote form - used to infer the result type T */
|
|
6
|
+
form: {
|
|
7
|
+
result?: T | undefined;
|
|
8
|
+
fields: {
|
|
9
|
+
allIssues?: () => RemoteFormIssue[] | undefined;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** Children snippet receives the result (guaranteed to be defined) */
|
|
14
|
+
children: Snippet<[T]>;
|
|
15
|
+
/** CSS class for the wrapper */
|
|
16
|
+
class?: string;
|
|
17
|
+
};
|
|
18
|
+
exports: {};
|
|
19
|
+
bindings: "";
|
|
20
|
+
slots: {};
|
|
21
|
+
events: {};
|
|
22
|
+
};
|
|
23
|
+
declare class __sveltets_Render<T = unknown> {
|
|
24
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
25
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
26
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
27
|
+
bindings(): "";
|
|
28
|
+
exports(): {};
|
|
29
|
+
}
|
|
30
|
+
interface $$IsomorphicComponent {
|
|
31
|
+
new <T = unknown>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
32
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
33
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
34
|
+
<T = unknown>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
35
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
36
|
+
}
|
|
37
|
+
declare const SResult: $$IsomorphicComponent;
|
|
38
|
+
type SResult<T = unknown> = InstanceType<typeof SResult<T>>;
|
|
39
|
+
export default SResult;
|