@samuel-charpentier/sform 0.0.1 → 0.0.3
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 +298 -86
- 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 +12 -1
- package/dist/Sform/Sform.svelte +13 -1
- package/dist/Sform/context.svelte.d.ts +11 -2
- package/dist/Sform/context.svelte.js +68 -3
- package/dist/Sform/index.d.ts +3 -1
- package/dist/Sform/index.js +2 -0
- package/dist/Sform/inputs/ButtonInput.svelte +43 -41
- package/dist/Sform/inputs/ButtonInput.svelte.d.ts +43 -27
- package/dist/Sform/inputs/HiddenInput.svelte +10 -0
- package/dist/Sform/inputs/HiddenInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/TextInput.svelte +26 -30
- package/dist/Sform/sform.css +14 -0
- package/dist/Sform/types.d.ts +89 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -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
|
|
@@ -23,7 +56,7 @@ A type-safe form library for **Svelte 5** with **SvelteKit remote functions**.
|
|
|
23
56
|
## Installation
|
|
24
57
|
|
|
25
58
|
```bash
|
|
26
|
-
npm install
|
|
59
|
+
npm install @samuel-charpentier/sform
|
|
27
60
|
```
|
|
28
61
|
|
|
29
62
|
Enable remote functions in `svelte.config.js`:
|
|
@@ -64,15 +97,17 @@ export const login = form(loginSchema, async ({ username, _password }) => {
|
|
|
64
97
|
|
|
65
98
|
```svelte
|
|
66
99
|
<script lang="ts">
|
|
67
|
-
import { Sform, Sfield, Sbutton } from '
|
|
100
|
+
import { Sform, Sfield, Sbutton } from '@samuel-charpentier/sform';
|
|
68
101
|
import { login } from './auth.remote.ts';
|
|
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,62 +143,95 @@ 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`
|
|
168
|
+
|
|
169
|
+
| Prop | Type | Default | Description |
|
|
170
|
+
| -------- | ------------------- | ----------- | -------------------- |
|
|
171
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input |
|
|
172
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input |
|
|
130
173
|
|
|
131
174
|
#### Password Input
|
|
132
175
|
|
|
133
176
|
```svelte
|
|
134
|
-
<Sfield
|
|
135
|
-
<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>
|
|
136
188
|
```
|
|
137
189
|
|
|
138
|
-
| Prop
|
|
139
|
-
|
|
|
140
|
-
| `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 |
|
|
141
196
|
|
|
142
197
|
#### Number Input
|
|
143
198
|
|
|
144
199
|
```svelte
|
|
145
|
-
<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} />
|
|
146
203
|
```
|
|
147
204
|
|
|
148
|
-
| Prop
|
|
149
|
-
|
|
|
150
|
-
| `min`
|
|
151
|
-
| `max`
|
|
152
|
-
| `step`
|
|
205
|
+
| Prop | Type | Default | Description |
|
|
206
|
+
| -------------- | ------------------- | ----------- | -------------------------------------- |
|
|
207
|
+
| `min` | `number \| string` | `undefined` | Minimum value |
|
|
208
|
+
| `max` | `number \| string` | `undefined` | Maximum value |
|
|
209
|
+
| `step` | `number \| string` | `undefined` | Step increment |
|
|
210
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input (e.g., "$") |
|
|
211
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input (e.g., "USD") |
|
|
212
|
+
| `showControls` | `boolean` | `true` | Show spinner controls |
|
|
213
|
+
| `align` | `'start' \| 'end'` | `'start'` | Text alignment |
|
|
214
|
+
| `maxDecimals` | `number` | `undefined` | Max decimal places (0 = integers only) |
|
|
215
|
+
| `autocomplete` | `string` | `undefined` | HTML autocomplete attribute |
|
|
153
216
|
|
|
154
217
|
#### Textarea
|
|
155
218
|
|
|
156
219
|
```svelte
|
|
157
|
-
<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)" />
|
|
158
222
|
```
|
|
159
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
|
+
|
|
160
230
|
#### Select
|
|
161
231
|
|
|
162
232
|
```svelte
|
|
163
233
|
<Sfield
|
|
164
|
-
|
|
234
|
+
field={fields.country}
|
|
165
235
|
type="select"
|
|
166
236
|
label="Country"
|
|
167
237
|
options={[
|
|
@@ -179,14 +249,14 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
179
249
|
#### Checkbox
|
|
180
250
|
|
|
181
251
|
```svelte
|
|
182
|
-
<Sfield
|
|
252
|
+
<Sfield field={fields.subscribe} type="checkbox" label="Subscribe to newsletter" />
|
|
183
253
|
```
|
|
184
254
|
|
|
185
255
|
#### Radio
|
|
186
256
|
|
|
187
257
|
```svelte
|
|
188
258
|
<Sfield
|
|
189
|
-
|
|
259
|
+
field={fields.plan}
|
|
190
260
|
type="radio"
|
|
191
261
|
label="Plan"
|
|
192
262
|
options={[
|
|
@@ -204,7 +274,16 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
204
274
|
#### Range
|
|
205
275
|
|
|
206
276
|
```svelte
|
|
207
|
-
<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
|
+
/>
|
|
208
287
|
```
|
|
209
288
|
|
|
210
289
|
| Prop | Type | Default | Description |
|
|
@@ -218,8 +297,8 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
218
297
|
#### Toggle
|
|
219
298
|
|
|
220
299
|
```svelte
|
|
221
|
-
<Sfield
|
|
222
|
-
<Sfield
|
|
300
|
+
<Sfield field={fields.notifications} type="toggle" label="Enable Notifications" />
|
|
301
|
+
<Sfield field={fields.darkMode} type="toggle" label="Theme" onLabel="Dark" offLabel="Light" />
|
|
223
302
|
```
|
|
224
303
|
|
|
225
304
|
| Prop | Type | Default | Description |
|
|
@@ -233,7 +312,7 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
233
312
|
|
|
234
313
|
```svelte
|
|
235
314
|
<Sfield
|
|
236
|
-
|
|
315
|
+
field={fields.theme}
|
|
237
316
|
type="toggle-options"
|
|
238
317
|
label="Theme"
|
|
239
318
|
options={[
|
|
@@ -242,6 +321,18 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
242
321
|
{ value: 'auto', label: 'Auto' }
|
|
243
322
|
]}
|
|
244
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
|
+
/>
|
|
245
336
|
```
|
|
246
337
|
|
|
247
338
|
| Prop | Type | Default | Description |
|
|
@@ -252,17 +343,28 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
252
343
|
#### Masked Input
|
|
253
344
|
|
|
254
345
|
```svelte
|
|
255
|
-
<Sfield
|
|
256
|
-
<Sfield
|
|
257
|
-
<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
|
+
/>
|
|
258
357
|
```
|
|
259
358
|
|
|
260
|
-
| Prop | Type
|
|
261
|
-
| --------------------- |
|
|
262
|
-
| `mask` | `string`
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
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 |
|
|
266
368
|
|
|
267
369
|
**Mask Tokens:**
|
|
268
370
|
|
|
@@ -271,40 +373,158 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
271
373
|
- `A` - Alphabetic uppercase
|
|
272
374
|
- `*` - Alphanumeric
|
|
273
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
|
+
|
|
274
401
|
### `<Sbutton>`
|
|
275
402
|
|
|
276
|
-
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:
|
|
277
437
|
|
|
278
438
|
```svelte
|
|
279
|
-
<
|
|
439
|
+
<script lang="ts">
|
|
440
|
+
import { login } from './auth.remote'; // Returns { success: boolean; message: string }
|
|
441
|
+
</script>
|
|
280
442
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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}
|
|
285
452
|
{/snippet}
|
|
286
|
-
|
|
287
|
-
|
|
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>
|
|
288
477
|
{/snippet}
|
|
289
|
-
|
|
290
|
-
|
|
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}
|
|
291
501
|
{/snippet}
|
|
292
|
-
|
|
293
|
-
|
|
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>
|
|
294
517
|
{/snippet}
|
|
295
|
-
</
|
|
518
|
+
</SResult>
|
|
296
519
|
```
|
|
297
520
|
|
|
298
|
-
| Prop
|
|
299
|
-
|
|
|
300
|
-
| `
|
|
301
|
-
| `
|
|
302
|
-
| `class`
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
| `pendingState` | `Snippet` | `undefined` | Pending state snippet |
|
|
306
|
-
| `successState` | `Snippet` | `undefined` | Success state snippet |
|
|
307
|
-
| `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.
|
|
308
528
|
|
|
309
529
|
## Styling
|
|
310
530
|
|
|
@@ -322,11 +542,11 @@ Sfield adds these classes automatically:
|
|
|
322
542
|
|
|
323
543
|
```svelte
|
|
324
544
|
<!-- String class applies to wrapper -->
|
|
325
|
-
<Sfield
|
|
545
|
+
<Sfield field={fields.email} type="email" class="my-field" />
|
|
326
546
|
|
|
327
547
|
<!-- Object for granular control -->
|
|
328
548
|
<Sfield
|
|
329
|
-
|
|
549
|
+
field={fields.email}
|
|
330
550
|
type="email"
|
|
331
551
|
class={{
|
|
332
552
|
wrapper: 'field-wrapper',
|
|
@@ -364,16 +584,16 @@ Sform uses TypeScript discriminated unions to provide type-safe props for each i
|
|
|
364
584
|
|
|
365
585
|
```typescript
|
|
366
586
|
// ✅ TypeScript knows 'showToggle' is only valid for password type
|
|
367
|
-
<Sfield
|
|
587
|
+
<Sfield field={fields._password} type="password" showToggle={false} />
|
|
368
588
|
|
|
369
589
|
// ✅ TypeScript knows 'options' is required for select type
|
|
370
|
-
<Sfield
|
|
590
|
+
<Sfield field={fields.country} type="select" options={countries} />
|
|
371
591
|
|
|
372
592
|
// ✅ TypeScript knows 'min', 'max', 'step' are valid for number type
|
|
373
|
-
<Sfield
|
|
593
|
+
<Sfield field={fields.age} type="number" min={0} max={150} />
|
|
374
594
|
|
|
375
595
|
// ❌ TypeScript error: 'showToggle' doesn't exist on text type
|
|
376
|
-
<Sfield
|
|
596
|
+
<Sfield field={fields.username} type="text" showToggle />
|
|
377
597
|
```
|
|
378
598
|
|
|
379
599
|
## Development
|
|
@@ -395,11 +615,3 @@ npm run package
|
|
|
395
615
|
## License
|
|
396
616
|
|
|
397
617
|
MIT
|
|
398
|
-
|
|
399
|
-
Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
|
|
400
|
-
|
|
401
|
-
To publish your library to [npm](https://www.npmjs.com):
|
|
402
|
-
|
|
403
|
-
```sh
|
|
404
|
-
npm publish
|
|
405
|
-
```
|
|
@@ -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}
|