@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 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
- <Sfield name="username" type="text" label="Username" />
73
- <Sfield name="_password" type="password" label="Password" />
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
- <Sbutton label="Login" />
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
- <!-- Sfield components here -->
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
- | `name` | `string` | required | Field name (must match schema) |
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 name="email" type="email" label="Email" placeholder="you@example.com" />
124
- <Sfield name="search" type="search" label="Search" />
125
- <Sfield name="phone" type="tel" label="Phone" />
126
- <Sfield name="website" type="url" label="Website" prefix="https://" />
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`, `hidden`
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 name="_password" type="password" label="Password" />
140
- <Sfield name="_password" type="password" label="Password" showToggle={false} />
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 | Type | Default | Description |
144
- | ------------ | --------- | ------- | ---------------------------------- |
145
- | `showToggle` | `boolean` | `true` | Show eye icon to toggle visibility |
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 name="age" type="number" label="Age" min={0} max={150} step={1} />
151
- <Sfield name="price" type="number" label="Price" prefix="$" suffix="USD" align="end" />
152
- <Sfield name="quantity" type="number" label="Qty" showControls={false} maxDecimals={0} />
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 name="bio" type="textarea" label="Bio" placeholder="Tell us about yourself" />
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
- name="country"
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 name="subscribe" type="checkbox" label="Subscribe to newsletter" />
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
- name="plan"
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 name="volume" type="range" label="Volume" min={0} max={100} step={5} showValue />
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 name="notifications" type="toggle" label="Enable Notifications" />
234
- <Sfield name="darkMode" type="toggle" label="Theme" onLabel="Dark" offLabel="Light" />
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
- name="theme"
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 name="phone" type="masked" label="Phone" mask="(###) ###-####" />
268
- <Sfield name="creditCard" type="masked" label="Credit Card" mask="#### #### #### ####" />
269
- <Sfield name="ssn" type="masked" label="SSN" mask="###-##-####" />
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 | Default | Description |
273
- | --------------------- | ------------------- | ----------- | -------------------------------- |
274
- | `mask` | `string` | required | Mask pattern |
275
- | `maskPlaceholder` | `string` | `'_'` | Placeholder character |
276
- | `showMaskPlaceholder` | `boolean` | `false` | Show full mask with placeholders |
277
- | `unmaskValue` | `boolean` | `true` | Store unmasked value |
278
- | `prefix` | `string \| Snippet` | `undefined` | Content before input |
279
- | `suffix` | `string \| Snippet` | `undefined` | Content after input |
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
- <Sbutton label="Submit" class="my-button" />
439
+ <script lang="ts">
440
+ import { login } from './auth.remote'; // Returns { success: boolean; message: string }
441
+ </script>
294
442
 
295
- <!-- With custom state snippets -->
296
- <Sbutton class="submit-btn">
297
- {#snippet defaultState(state)}
298
- Submit Form
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
- {#snippet pendingState(state)}
301
- Submitting...
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
- {#snippet successState(state)}
304
- ✓ Success!
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
- {#snippet errorState(state)}
307
- Fix Errors
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
- </Sbutton>
518
+ </SResult>
310
519
  ```
311
520
 
312
- | Prop | Type | Default | Description |
313
- | -------------- | --------------------------------- | ----------- | --------------------------------- |
314
- | `label` | `string` | `'Submit'` | Button text |
315
- | `buttonType` | `'submit' \| 'reset' \| 'button'` | `'submit'` | Button type |
316
- | `class` | `string` | `undefined` | CSS class |
317
- | `disabled` | `boolean` | `false` | Disable button |
318
- | `onsubmit` | `() => void \| Promise<void>` | `undefined` | Callback before validation/submit |
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 name="email" type="email" class="my-field" />
545
+ <Sfield field={fields.email} type="email" class="my-field" />
341
546
 
342
547
  <!-- Object for granular control -->
343
548
  <Sfield
344
- name="email"
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 name="_password" type="password" showToggle={false} />
587
+ <Sfield field={fields._password} type="password" showToggle={false} />
383
588
 
384
589
  // ✅ TypeScript knows 'options' is required for select type
385
- <Sfield name="country" type="select" options={countries} />
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 name="age" type="number" min={0} max={150} />
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 name="username" type="text" showToggle />
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;