@samuel-charpentier/sform 0.0.2 → 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 +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/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 +85 -15
- 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;
|
package/dist/Sform/Sfield.svelte
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import ToggleOptionsInput from './inputs/ToggleOptionsInput.svelte';
|
|
19
19
|
import MaskedInput from './inputs/MaskedInput.svelte';
|
|
20
20
|
import PasswordInput from './inputs/PasswordInput.svelte';
|
|
21
|
+
import HiddenInput from './inputs/HiddenInput.svelte';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Sfield - Type-safe form field component.
|
|
@@ -41,6 +42,10 @@
|
|
|
41
42
|
// Register this field with the context on mount
|
|
42
43
|
$effect(() => {
|
|
43
44
|
context.registerField(name);
|
|
45
|
+
// Register for issue display tracking (all types except hidden display their issues)
|
|
46
|
+
if (props.type !== 'hidden') {
|
|
47
|
+
context.registerFieldWithIssueDisplay(name);
|
|
48
|
+
}
|
|
44
49
|
});
|
|
45
50
|
|
|
46
51
|
const classes: SfieldClasses = $derived(
|
|
@@ -112,7 +117,6 @@
|
|
|
112
117
|
'month',
|
|
113
118
|
'week',
|
|
114
119
|
'color',
|
|
115
|
-
'hidden',
|
|
116
120
|
'file'
|
|
117
121
|
].includes(props.type)
|
|
118
122
|
);
|
|
@@ -147,6 +151,8 @@
|
|
|
147
151
|
<ToggleOptionsInput {...passthroughProps()} {...internalProps} />
|
|
148
152
|
{:else if props.type === 'masked'}
|
|
149
153
|
<MaskedInput {...passthroughProps()} {...internalProps} />
|
|
154
|
+
{:else if props.type === 'hidden'}
|
|
155
|
+
<HiddenInput {...passthroughProps()} {...internalProps} />
|
|
150
156
|
{/if}
|
|
151
157
|
|
|
152
158
|
{#if props.hint}
|
package/dist/Sform/Sform.svelte
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
import type { SformContext, ValidateOn } from './types.js';
|
|
2
|
-
|
|
1
|
+
import type { RemoteFormIssue, SformContext, ValidateOn } from './types.js';
|
|
2
|
+
interface FormLike {
|
|
3
|
+
pending?: number;
|
|
4
|
+
result?: unknown;
|
|
5
|
+
fields: {
|
|
6
|
+
allIssues?: () => RemoteFormIssue[] | undefined;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function createSformContext(getValidateOn: () => ValidateOn, getFieldNames: () => string[], triggerValidation: () => void, submitForm: () => void, getForm: () => FormLike): SformContext;
|
|
3
11
|
export declare function getSformContext(): SformContext;
|
|
12
|
+
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { getContext, setContext } from 'svelte';
|
|
2
2
|
import { SvelteSet } from 'svelte/reactivity';
|
|
3
3
|
const SFORM_CONTEXT_KEY = Symbol('sform-context');
|
|
4
|
-
export function createSformContext(getValidateOn, getFieldNames, triggerValidation, submitForm) {
|
|
4
|
+
export function createSformContext(getValidateOn, getFieldNames, triggerValidation, submitForm, getForm) {
|
|
5
5
|
const touched = new SvelteSet();
|
|
6
6
|
const dirty = new SvelteSet();
|
|
7
7
|
const registeredFields = new SvelteSet();
|
|
8
|
+
const fieldsWithIssueDisplay = new SvelteSet();
|
|
8
9
|
let submitted = $state(false);
|
|
9
10
|
const context = {
|
|
10
11
|
get validateOn() {
|
|
@@ -51,12 +52,71 @@ export function createSformContext(getValidateOn, getFieldNames, triggerValidati
|
|
|
51
52
|
registerField: (name) => {
|
|
52
53
|
registeredFields.add(name);
|
|
53
54
|
},
|
|
55
|
+
registerFieldWithIssueDisplay: (name) => {
|
|
56
|
+
fieldsWithIssueDisplay.add(name);
|
|
57
|
+
},
|
|
54
58
|
resetFieldStates: () => {
|
|
55
59
|
touched.clear();
|
|
56
60
|
dirty.clear();
|
|
57
61
|
submitted = false;
|
|
58
62
|
},
|
|
59
|
-
submitForm
|
|
63
|
+
submitForm,
|
|
64
|
+
getFormState: () => {
|
|
65
|
+
const form = getForm();
|
|
66
|
+
const pending = (form.pending ?? 0) !== 0;
|
|
67
|
+
const hasResult = form.result !== undefined;
|
|
68
|
+
const issues = form.fields.allIssues?.() ?? [];
|
|
69
|
+
const hasIssues = issues.length > 0;
|
|
70
|
+
if (pending) {
|
|
71
|
+
return {
|
|
72
|
+
state: 'pending',
|
|
73
|
+
pending: true,
|
|
74
|
+
success: false,
|
|
75
|
+
hasIssues: false,
|
|
76
|
+
result: undefined
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (hasResult) {
|
|
80
|
+
// If there's a result, it's success (issues come via invalid() which throws, no result)
|
|
81
|
+
return {
|
|
82
|
+
state: 'success',
|
|
83
|
+
pending: false,
|
|
84
|
+
success: true,
|
|
85
|
+
hasIssues: false,
|
|
86
|
+
result: form.result
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (hasIssues) {
|
|
90
|
+
return {
|
|
91
|
+
state: 'hasIssues',
|
|
92
|
+
pending: false,
|
|
93
|
+
success: false,
|
|
94
|
+
hasIssues: true,
|
|
95
|
+
result: undefined
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
state: 'default',
|
|
100
|
+
pending: false,
|
|
101
|
+
success: false,
|
|
102
|
+
hasIssues: false,
|
|
103
|
+
result: undefined
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
getUnhandledIssues: () => {
|
|
107
|
+
const form = getForm();
|
|
108
|
+
const allIssues = form.fields.allIssues?.() ?? [];
|
|
109
|
+
// Filter out issues that are linked to fields with issue display
|
|
110
|
+
return allIssues.filter((issue) => {
|
|
111
|
+
// If issue has no path, it's a form-level issue (from invalid("message"))
|
|
112
|
+
if (!issue.path || issue.path.length === 0) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
// Check if the first path segment is a field that displays issues
|
|
116
|
+
const fieldName = String(issue.path[0]);
|
|
117
|
+
return !fieldsWithIssueDisplay.has(fieldName);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
60
120
|
};
|
|
61
121
|
setContext(SFORM_CONTEXT_KEY, context);
|
|
62
122
|
return context;
|
package/dist/Sform/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { default as Sform } from './Sform.svelte';
|
|
2
2
|
export { default as Sfield } from './Sfield.svelte';
|
|
3
3
|
export { default as Sbutton } from './inputs/ButtonInput.svelte';
|
|
4
|
+
export { default as SIssues } from './SIssues.svelte';
|
|
5
|
+
export { default as SResult } from './SResult.svelte';
|
|
4
6
|
export { applyMask, unmask, MASK_PATTERNS, DEFAULT_TOKENS, type MaskOptions, type MaskResult, type MaskToken, type MaskPattern } from './utils/mask.js';
|
|
5
|
-
export type { ValidateOn, FieldState, SfieldClasses, InputType, SelectOption, SformContext, SformProps, ButtonFormState, ButtonInputProps, InputAffixProps, RangeInputProps, ToggleInputProps, ToggleOption, ToggleOptionsInputProps, SfieldTypeMap, AllowedSfieldType, TypedSfieldProps, SfieldBaseProps, TypedBaseSfieldProps, SfieldTextProps, SfieldPasswordProps, SfieldNumberProps, SfieldTextareaProps, SfieldSelectProps, SfieldCheckboxProps, SfieldCheckboxGroupProps, SfieldRadioProps, SfieldRangeProps, SfieldToggleProps, SfieldToggleOptionsProps, SfieldMaskedProps, RemoteForm, RemoteFormField, RemoteFormFields, RemoteFormFieldValue, RemoteFormInput, RemoteFormIssue } from './types.js';
|
|
7
|
+
export type { ValidateOn, FieldState, SfieldClasses, InputType, SelectOption, SformContext, SformProps, ButtonState, ButtonFormState, ButtonInputProps, SIssuesProps, InputAffixProps, RangeInputProps, ToggleInputProps, ToggleOption, ToggleOptionsInputProps, SfieldTypeMap, AllowedSfieldType, TypedSfieldProps, SfieldBaseProps, TypedBaseSfieldProps, SfieldTextProps, SfieldPasswordProps, SfieldNumberProps, SfieldTextareaProps, SfieldSelectProps, SfieldCheckboxProps, SfieldCheckboxGroupProps, SfieldRadioProps, SfieldRangeProps, SfieldToggleProps, SfieldToggleOptionsProps, SfieldMaskedProps, RemoteForm, RemoteFormField, RemoteFormFields, RemoteFormFieldValue, RemoteFormInput, RemoteFormIssue } from './types.js';
|
package/dist/Sform/index.js
CHANGED
|
@@ -3,5 +3,7 @@ export { default as Sform } from './Sform.svelte';
|
|
|
3
3
|
export { default as Sfield } from './Sfield.svelte';
|
|
4
4
|
// Standalone components (not routed through Sfield)
|
|
5
5
|
export { default as Sbutton } from './inputs/ButtonInput.svelte';
|
|
6
|
+
export { default as SIssues } from './SIssues.svelte';
|
|
7
|
+
export { default as SResult } from './SResult.svelte';
|
|
6
8
|
// Utilities
|
|
7
9
|
export { applyMask, unmask, MASK_PATTERNS, DEFAULT_TOKENS } from './utils/mask.js';
|
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="T = unknown">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ButtonState, RemoteFormIssue } from '../types.js';
|
|
4
4
|
import { getSformContext } from '../context.svelte.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Minimal form shape needed for type inference.
|
|
8
|
+
* This allows the component to infer T from the form's result type.
|
|
9
|
+
*/
|
|
10
|
+
interface FormLike<Output> {
|
|
11
|
+
result?: Output;
|
|
7
12
|
pending?: number;
|
|
8
|
-
result?: unknown;
|
|
9
13
|
fields: {
|
|
10
14
|
allIssues?: () => RemoteFormIssue[] | undefined;
|
|
15
|
+
[key: string]: unknown;
|
|
11
16
|
};
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
interface Props {
|
|
15
|
-
/** The remote form -
|
|
16
|
-
form: FormLike
|
|
17
|
-
/** Button text (used if no
|
|
20
|
+
/** The remote form - used to infer the result type T */
|
|
21
|
+
form: FormLike<T>;
|
|
22
|
+
/** Button text (used if no children snippet provided) */
|
|
18
23
|
label?: string;
|
|
19
24
|
/** Button type */
|
|
20
25
|
buttonType?: 'submit' | 'reset' | 'button';
|
|
@@ -22,55 +27,37 @@
|
|
|
22
27
|
class?: string;
|
|
23
28
|
/** Whether button is disabled */
|
|
24
29
|
disabled?: boolean;
|
|
30
|
+
/** Children snippet receives ButtonState<T> for custom rendering with typed result */
|
|
31
|
+
children?: Snippet<[ButtonState<T>]>;
|
|
25
32
|
/** Callback that runs before validation/submission (can be async) */
|
|
26
33
|
onsubmit?: () => void | Promise<void>;
|
|
27
|
-
/** Snippet for default state */
|
|
28
|
-
defaultState?: Snippet<[ButtonFormState]>;
|
|
29
|
-
/** Snippet for pending state */
|
|
30
|
-
pendingState?: Snippet<[ButtonFormState]>;
|
|
31
|
-
/** Snippet for success state */
|
|
32
|
-
successState?: Snippet<[ButtonFormState]>;
|
|
33
|
-
/** Snippet for error/issues state */
|
|
34
|
-
errorState?: Snippet<[ButtonFormState]>;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
let {
|
|
38
|
-
form,
|
|
39
37
|
label = 'Submit',
|
|
40
38
|
buttonType = 'submit',
|
|
41
39
|
class: className,
|
|
42
40
|
disabled = false,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
pendingState,
|
|
46
|
-
successState,
|
|
47
|
-
errorState
|
|
41
|
+
children,
|
|
42
|
+
onsubmit
|
|
48
43
|
}: Props = $props();
|
|
49
44
|
|
|
50
|
-
// Get Sform context
|
|
45
|
+
// Get Sform context for form state and actions
|
|
51
46
|
const sformContext = getSformContext();
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const hasResult = form.result !== undefined;
|
|
56
|
-
const issues = form.fields.allIssues?.() ?? [];
|
|
57
|
-
const hasIssues = issues.length > 0;
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
pending,
|
|
61
|
-
success: hasResult && !hasIssues && !pending,
|
|
62
|
-
hasIssues,
|
|
63
|
-
result: form.result
|
|
64
|
-
};
|
|
65
|
-
});
|
|
48
|
+
// Get form state from context with the generic type
|
|
49
|
+
const formState = $derived.by(sformContext.getFormState<T>);
|
|
66
50
|
|
|
67
51
|
const isDisabled = $derived(disabled || formState.pending);
|
|
68
52
|
|
|
69
53
|
async function handleClick(event: MouseEvent) {
|
|
70
|
-
if (buttonType !== 'submit'
|
|
54
|
+
if (buttonType !== 'submit') return;
|
|
71
55
|
|
|
72
56
|
event.preventDefault();
|
|
73
|
-
|
|
57
|
+
|
|
58
|
+
if (onsubmit) {
|
|
59
|
+
await onsubmit();
|
|
60
|
+
}
|
|
74
61
|
|
|
75
62
|
// Mark form as submitted and all fields dirty so issues display when server responds
|
|
76
63
|
sformContext.markSubmitted();
|
|
@@ -82,14 +69,8 @@
|
|
|
82
69
|
</script>
|
|
83
70
|
|
|
84
71
|
<button type={buttonType} class={className} disabled={isDisabled} onclick={handleClick}>
|
|
85
|
-
{#if
|
|
86
|
-
{@render
|
|
87
|
-
{:else if formState.success && successState}
|
|
88
|
-
{@render successState(formState)}
|
|
89
|
-
{:else if formState.hasIssues && errorState}
|
|
90
|
-
{@render errorState(formState)}
|
|
91
|
-
{:else if defaultState}
|
|
92
|
-
{@render defaultState(formState)}
|
|
72
|
+
{#if children}
|
|
73
|
+
{@render children(formState)}
|
|
93
74
|
{:else}
|
|
94
75
|
{label}
|
|
95
76
|
{/if}
|
|
@@ -1,34 +1,48 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import type { ButtonState, 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
|
+
pending?: number;
|
|
9
|
+
fields: {
|
|
10
|
+
allIssues?: () => RemoteFormIssue[] | undefined;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/** Button text (used if no children snippet provided) */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Button type */
|
|
17
|
+
buttonType?: "submit" | "reset" | "button";
|
|
18
|
+
/** Button class */
|
|
19
|
+
class?: string;
|
|
20
|
+
/** Whether button is disabled */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Children snippet receives ButtonState<T> for custom rendering with typed result */
|
|
23
|
+
children?: Snippet<[ButtonState<T>]>;
|
|
24
|
+
/** Callback that runs before validation/submission (can be async) */
|
|
25
|
+
onsubmit?: () => void | Promise<void>;
|
|
8
26
|
};
|
|
27
|
+
exports: {};
|
|
28
|
+
bindings: "";
|
|
29
|
+
slots: {};
|
|
30
|
+
events: {};
|
|
31
|
+
};
|
|
32
|
+
declare class __sveltets_Render<T = unknown> {
|
|
33
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
34
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
35
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
36
|
+
bindings(): "";
|
|
37
|
+
exports(): {};
|
|
9
38
|
}
|
|
10
|
-
interface
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
buttonType?: 'submit' | 'reset' | 'button';
|
|
17
|
-
/** Button class */
|
|
18
|
-
class?: string;
|
|
19
|
-
/** Whether button is disabled */
|
|
20
|
-
disabled?: boolean;
|
|
21
|
-
/** Callback that runs before validation/submission (can be async) */
|
|
22
|
-
onsubmit?: () => void | Promise<void>;
|
|
23
|
-
/** Snippet for default state */
|
|
24
|
-
defaultState?: Snippet<[ButtonFormState]>;
|
|
25
|
-
/** Snippet for pending state */
|
|
26
|
-
pendingState?: Snippet<[ButtonFormState]>;
|
|
27
|
-
/** Snippet for success state */
|
|
28
|
-
successState?: Snippet<[ButtonFormState]>;
|
|
29
|
-
/** Snippet for error/issues state */
|
|
30
|
-
errorState?: Snippet<[ButtonFormState]>;
|
|
39
|
+
interface $$IsomorphicComponent {
|
|
40
|
+
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']>> & {
|
|
41
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
42
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
43
|
+
<T = unknown>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
44
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
31
45
|
}
|
|
32
|
-
declare const ButtonInput:
|
|
33
|
-
type ButtonInput =
|
|
46
|
+
declare const ButtonInput: $$IsomorphicComponent;
|
|
47
|
+
type ButtonInput<T = unknown> = InstanceType<typeof ButtonInput<T>>;
|
|
34
48
|
export default ButtonInput;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HiddenInputProps } from '../types.js';
|
|
3
|
+
|
|
4
|
+
let { field, name, value }: HiddenInputProps = $props();
|
|
5
|
+
|
|
6
|
+
// Hidden fields require the value to be passed to field.as('hidden', value)
|
|
7
|
+
const fieldAttrs = $derived(field.as('hidden', value ?? ''));
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<input {...fieldAttrs} type="hidden" id={name} />
|
|
@@ -28,34 +28,30 @@
|
|
|
28
28
|
let inputElement: HTMLInputElement | undefined = $state();
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
|
-
{#if
|
|
32
|
-
{
|
|
33
|
-
<label class={labelClass} for={name}>{label}</label>
|
|
34
|
-
{/if}
|
|
35
|
-
<div class="sform-input-wrapper {wrapperClass ?? ''}">
|
|
36
|
-
{#if prefix}
|
|
37
|
-
<div class="sform-prefix" onclick={() => inputElement?.focus()} role="presentation">
|
|
38
|
-
{#if typeof prefix === 'function'}{@render prefix()}{:else}{prefix}{/if}
|
|
39
|
-
</div>
|
|
40
|
-
{/if}
|
|
41
|
-
<input
|
|
42
|
-
bind:this={inputElement}
|
|
43
|
-
{...fieldAttrs}
|
|
44
|
-
id={name}
|
|
45
|
-
class={className}
|
|
46
|
-
{placeholder}
|
|
47
|
-
{disabled}
|
|
48
|
-
{readonly}
|
|
49
|
-
{autocomplete}
|
|
50
|
-
{onblur}
|
|
51
|
-
{oninput}
|
|
52
|
-
/>
|
|
53
|
-
{#if suffix}
|
|
54
|
-
<div class="sform-suffix" onclick={() => inputElement?.focus()} role="presentation">
|
|
55
|
-
{#if typeof suffix === 'function'}{@render suffix()}{:else}{suffix}{/if}
|
|
56
|
-
</div>
|
|
57
|
-
{/if}
|
|
58
|
-
</div>
|
|
59
|
-
{:else}
|
|
60
|
-
<input {...fieldAttrs} type="hidden" id={name} />
|
|
31
|
+
{#if label}
|
|
32
|
+
<label class={labelClass} for={name}>{label}</label>
|
|
61
33
|
{/if}
|
|
34
|
+
<div class="sform-input-wrapper {wrapperClass ?? ''}">
|
|
35
|
+
{#if prefix}
|
|
36
|
+
<div class="sform-prefix" onclick={() => inputElement?.focus()} role="presentation">
|
|
37
|
+
{#if typeof prefix === 'function'}{@render prefix()}{:else}{prefix}{/if}
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
<input
|
|
41
|
+
bind:this={inputElement}
|
|
42
|
+
{...fieldAttrs}
|
|
43
|
+
id={name}
|
|
44
|
+
class={className}
|
|
45
|
+
{placeholder}
|
|
46
|
+
{disabled}
|
|
47
|
+
{readonly}
|
|
48
|
+
{autocomplete}
|
|
49
|
+
{onblur}
|
|
50
|
+
{oninput}
|
|
51
|
+
/>
|
|
52
|
+
{#if suffix}
|
|
53
|
+
<div class="sform-suffix" onclick={() => inputElement?.focus()} role="presentation">
|
|
54
|
+
{#if typeof suffix === 'function'}{@render suffix()}{:else}{suffix}{/if}
|
|
55
|
+
</div>
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
package/dist/Sform/sform.css
CHANGED
|
@@ -600,6 +600,7 @@ select.sform-input {
|
|
|
600
600
|
.sform-result-success {
|
|
601
601
|
color: #065f46;
|
|
602
602
|
background: var(--sform-success-bg);
|
|
603
|
+
margin-block-start: 1rem;
|
|
603
604
|
}
|
|
604
605
|
|
|
605
606
|
.sform-result-error {
|
|
@@ -612,6 +613,19 @@ select.sform-input {
|
|
|
612
613
|
background: var(--sform-warning-bg);
|
|
613
614
|
}
|
|
614
615
|
|
|
616
|
+
/* ===========================================
|
|
617
|
+
Issues List
|
|
618
|
+
=========================================== */
|
|
619
|
+
|
|
620
|
+
.sform-issues-message {
|
|
621
|
+
font-weight: bold;
|
|
622
|
+
margin-bottom: 0.25rem;
|
|
623
|
+
}
|
|
624
|
+
.sform-issues-list {
|
|
625
|
+
margin: 0;
|
|
626
|
+
padding-inline-start: 1.25rem;
|
|
627
|
+
}
|
|
628
|
+
|
|
615
629
|
/* ===========================================
|
|
616
630
|
Utilities
|
|
617
631
|
=========================================== */
|
package/dist/Sform/types.d.ts
CHANGED
|
@@ -82,7 +82,7 @@ export type AllowedSfieldType<T> = {
|
|
|
82
82
|
*/
|
|
83
83
|
export type InputType = 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search' | 'date' | 'datetime-local' | 'time' | 'month' | 'week' | 'color' | 'textarea' | 'select' | 'checkbox' | 'checkbox-group' | 'radio' | 'file' | 'hidden' | 'range' | 'toggle' | 'toggle-options' | 'masked';
|
|
84
84
|
/** Text-like input types */
|
|
85
|
-
export type TextInputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'date' | 'datetime-local' | 'time' | 'month' | 'week' | 'color' | 'file'
|
|
85
|
+
export type TextInputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'date' | 'datetime-local' | 'time' | 'month' | 'week' | 'color' | 'file';
|
|
86
86
|
/** Numeric input types */
|
|
87
87
|
export type NumericInputType = 'number';
|
|
88
88
|
/**
|
|
@@ -105,6 +105,37 @@ export type RemoteForm<Input extends RemoteFormInput | void = RemoteFormInput, O
|
|
|
105
105
|
* The .for(id) method returns Omit<RemoteForm, 'for'>, so this type accepts both.
|
|
106
106
|
*/
|
|
107
107
|
export type RemoteFormInstance<Input extends RemoteFormInput | void = RemoteFormInput, Output = unknown> = SvelteKitRemoteForm<Input, Output> | Omit<SvelteKitRemoteForm<Input, Output>, 'for'>;
|
|
108
|
+
/**
|
|
109
|
+
* Form state for buttons and state-aware components.
|
|
110
|
+
* State is derived from context - no result means default or pending,
|
|
111
|
+
* result with no issues means success, issues (via invalid()) means hasIssues.
|
|
112
|
+
* @template T - The type of the form result (defaults to unknown)
|
|
113
|
+
*/
|
|
114
|
+
export type ButtonState<T = unknown> = {
|
|
115
|
+
state: 'default';
|
|
116
|
+
pending: false;
|
|
117
|
+
success: false;
|
|
118
|
+
hasIssues: false;
|
|
119
|
+
result: undefined;
|
|
120
|
+
} | {
|
|
121
|
+
state: 'pending';
|
|
122
|
+
pending: true;
|
|
123
|
+
success: false;
|
|
124
|
+
hasIssues: false;
|
|
125
|
+
result: undefined;
|
|
126
|
+
} | {
|
|
127
|
+
state: 'success';
|
|
128
|
+
pending: false;
|
|
129
|
+
success: true;
|
|
130
|
+
hasIssues: false;
|
|
131
|
+
result: T;
|
|
132
|
+
} | {
|
|
133
|
+
state: 'hasIssues';
|
|
134
|
+
pending: false;
|
|
135
|
+
success: false;
|
|
136
|
+
hasIssues: true;
|
|
137
|
+
result: undefined;
|
|
138
|
+
};
|
|
108
139
|
/**
|
|
109
140
|
* Form context provided by Sform to children.
|
|
110
141
|
* Manages field state (touched, dirty, submitted) for validation display.
|
|
@@ -132,8 +163,14 @@ export interface SformContext {
|
|
|
132
163
|
resetFieldStates: () => void;
|
|
133
164
|
/** Register a field name (called by Sfield on mount) */
|
|
134
165
|
registerField: (name: string) => void;
|
|
166
|
+
/** Register a field that displays its own issues (all except hidden) */
|
|
167
|
+
registerFieldWithIssueDisplay: (name: string) => void;
|
|
135
168
|
/** Programmatically submit the form */
|
|
136
169
|
submitForm: () => void;
|
|
170
|
+
/** Get the current form state (for buttons and state-aware components) */
|
|
171
|
+
getFormState: <T = unknown>() => ButtonState<T>;
|
|
172
|
+
/** Get issues that are not displayed by any Sfield component */
|
|
173
|
+
getUnhandledIssues: () => RemoteFormIssue[];
|
|
137
174
|
}
|
|
138
175
|
/**
|
|
139
176
|
* Enhance callback options - typed based on form Input type.
|
|
@@ -365,8 +402,10 @@ export type SfieldToggleProps = SfieldPropsFrom<ToggleInputProps, boolean, 'togg
|
|
|
365
402
|
export type SfieldToggleOptionsProps = SfieldPropsFrom<ToggleOptionsInputProps, string, 'toggle-options'>;
|
|
366
403
|
/** Props for masked input */
|
|
367
404
|
export type SfieldMaskedProps = SfieldPropsFrom<MaskedInputProps, string, 'masked'>;
|
|
405
|
+
/** Props for hidden input */
|
|
406
|
+
export type SfieldHiddenProps = SfieldPropsFrom<HiddenInputProps, string, 'hidden'>;
|
|
368
407
|
/** All possible Sfield props as a discriminated union */
|
|
369
|
-
type AllSfieldProps = SfieldTextProps | SfieldPasswordProps | SfieldNumberProps | SfieldTextareaProps | SfieldSelectProps | SfieldCheckboxProps | SfieldCheckboxGroupProps | SfieldRadioProps | SfieldRangeProps | SfieldToggleProps | SfieldToggleOptionsProps | SfieldMaskedProps;
|
|
408
|
+
type AllSfieldProps = SfieldTextProps | SfieldPasswordProps | SfieldNumberProps | SfieldTextareaProps | SfieldSelectProps | SfieldCheckboxProps | SfieldCheckboxGroupProps | SfieldRadioProps | SfieldRangeProps | SfieldToggleProps | SfieldToggleOptionsProps | SfieldMaskedProps | SfieldHiddenProps;
|
|
370
409
|
/**
|
|
371
410
|
* Extract props that match a specific field value type.
|
|
372
411
|
* This ensures type="number" is only valid for number fields, etc.
|
|
@@ -515,7 +554,18 @@ export interface RadioInputProps extends BaseInputComponentProps {
|
|
|
515
554
|
options: SelectOption[] | string[];
|
|
516
555
|
}
|
|
517
556
|
/**
|
|
518
|
-
*
|
|
557
|
+
* Props for hidden input component
|
|
558
|
+
*/
|
|
559
|
+
export interface HiddenInputProps {
|
|
560
|
+
/** Remote field from form */
|
|
561
|
+
field: RemoteFormField<RemoteFormFieldValue>;
|
|
562
|
+
/** Field name */
|
|
563
|
+
name: string;
|
|
564
|
+
/** The value for the hidden field - required for hidden inputs */
|
|
565
|
+
value?: string;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* @deprecated Use ButtonState instead
|
|
519
569
|
*/
|
|
520
570
|
export interface ButtonFormState {
|
|
521
571
|
/** Whether form is submitting */
|
|
@@ -527,11 +577,26 @@ export interface ButtonFormState {
|
|
|
527
577
|
/** The form result if available */
|
|
528
578
|
result: unknown;
|
|
529
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Minimal form shape for Sbutton type inference.
|
|
582
|
+
* Allows the button to infer the result type T from the form.
|
|
583
|
+
*/
|
|
584
|
+
export interface ButtonFormLike<T = unknown> {
|
|
585
|
+
result?: T;
|
|
586
|
+
pending?: number;
|
|
587
|
+
fields: {
|
|
588
|
+
allIssues?: () => RemoteFormIssue[] | undefined;
|
|
589
|
+
[key: string]: unknown;
|
|
590
|
+
};
|
|
591
|
+
}
|
|
530
592
|
/**
|
|
531
593
|
* Props for button input component
|
|
594
|
+
* @template T - The type of the form result (inferred from form prop)
|
|
532
595
|
*/
|
|
533
|
-
export interface ButtonInputProps {
|
|
534
|
-
/**
|
|
596
|
+
export interface ButtonInputProps<T = unknown> {
|
|
597
|
+
/** The remote form - used to infer the result type T */
|
|
598
|
+
form: ButtonFormLike<T>;
|
|
599
|
+
/** Button text (used if no children snippet provided) */
|
|
535
600
|
label?: string;
|
|
536
601
|
/** Button type */
|
|
537
602
|
buttonType?: 'submit' | 'reset' | 'button';
|
|
@@ -539,16 +604,21 @@ export interface ButtonInputProps {
|
|
|
539
604
|
class?: string;
|
|
540
605
|
/** Whether button is disabled */
|
|
541
606
|
disabled?: boolean;
|
|
542
|
-
/**
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
607
|
+
/** Children snippet receives ButtonState<T> for custom rendering with typed result */
|
|
608
|
+
children?: Snippet<[ButtonState<T>]>;
|
|
609
|
+
/** Callback that runs before validation/submission (can be async) */
|
|
610
|
+
onsubmit?: () => void | Promise<void>;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Props for SIssues component
|
|
614
|
+
*/
|
|
615
|
+
export interface SIssuesProps {
|
|
616
|
+
/** General message shown when there are any issues */
|
|
617
|
+
message?: string | Snippet;
|
|
618
|
+
/** CSS class for the wrapper */
|
|
619
|
+
class?: string;
|
|
620
|
+
/** CSS class for the issues list */
|
|
621
|
+
listClass?: string;
|
|
552
622
|
}
|
|
553
623
|
/**
|
|
554
624
|
* Props for range input component
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Sform, Sfield, Sbutton } from './Sform/index.js';
|
|
1
|
+
export { Sform, Sfield, Sbutton, SIssues, SResult } from './Sform/index.js';
|
|
2
2
|
export type { ValidateOn, FieldState, SfieldClasses, InputType, SelectOption, SformContext, SformProps, ButtonFormState, ButtonInputProps, InputAffixProps, SfieldTypeMap, AllowedSfieldType, TypedSfieldProps, SfieldBaseProps, TypedBaseSfieldProps, SfieldTextProps, SfieldPasswordProps, SfieldNumberProps, SfieldTextareaProps, SfieldSelectProps, SfieldCheckboxProps, SfieldCheckboxGroupProps, SfieldRadioProps, SfieldRangeProps, SfieldToggleProps, SfieldToggleOptionsProps, SfieldMaskedProps, RemoteForm, RemoteFormField, RemoteFormFields, RemoteFormFieldValue, RemoteFormInput, RemoteFormIssue } from './Sform/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Sform, Sfield, Sbutton } from './Sform/index.js';
|
|
1
|
+
export { Sform, Sfield, Sbutton, SIssues, SResult } from './Sform/index.js';
|