@samuel-charpentier/sform 0.0.1 → 0.0.2
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 +39 -32
- package/dist/Sform/Sfield.svelte +5 -0
- package/dist/Sform/Sform.svelte +12 -1
- package/dist/Sform/context.svelte.d.ts +1 -1
- package/dist/Sform/context.svelte.js +9 -4
- package/dist/Sform/inputs/ButtonInput.svelte +22 -1
- package/dist/Sform/inputs/ButtonInput.svelte.d.ts +2 -0
- package/dist/Sform/types.d.ts +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ A type-safe form library for **Svelte 5** with **SvelteKit remote functions**.
|
|
|
23
23
|
## Installation
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npm install
|
|
26
|
+
npm install @samuel-charpentier/sform
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Enable remote functions in `svelte.config.js`:
|
|
@@ -64,7 +64,7 @@ export const login = form(loginSchema, async ({ username, _password }) => {
|
|
|
64
64
|
|
|
65
65
|
```svelte
|
|
66
66
|
<script lang="ts">
|
|
67
|
-
import { Sform, Sfield, Sbutton } from '
|
|
67
|
+
import { Sform, Sfield, Sbutton } from '@samuel-charpentier/sform';
|
|
68
68
|
import { login } from './auth.remote.ts';
|
|
69
69
|
</script>
|
|
70
70
|
|
|
@@ -123,11 +123,16 @@ Smart field component with type-safe props based on input type.
|
|
|
123
123
|
<Sfield name="email" type="email" label="Email" placeholder="you@example.com" />
|
|
124
124
|
<Sfield name="search" type="search" label="Search" />
|
|
125
125
|
<Sfield name="phone" type="tel" label="Phone" />
|
|
126
|
-
<Sfield name="website" type="url" label="Website" />
|
|
126
|
+
<Sfield name="website" type="url" label="Website" prefix="https://" />
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime-local`, `time`, `month`, `week`, `color`, `file`, `hidden`
|
|
130
130
|
|
|
131
|
+
| Prop | Type | Default | Description |
|
|
132
|
+
| -------- | ------------------- | ----------- | -------------------- |
|
|
133
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input |
|
|
134
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input |
|
|
135
|
+
|
|
131
136
|
#### Password Input
|
|
132
137
|
|
|
133
138
|
```svelte
|
|
@@ -143,13 +148,20 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
143
148
|
|
|
144
149
|
```svelte
|
|
145
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} />
|
|
146
153
|
```
|
|
147
154
|
|
|
148
|
-
| Prop
|
|
149
|
-
|
|
|
150
|
-
| `min`
|
|
151
|
-
| `max`
|
|
152
|
-
| `step`
|
|
155
|
+
| Prop | Type | Default | Description |
|
|
156
|
+
| -------------- | ------------------- | ----------- | -------------------------------------- |
|
|
157
|
+
| `min` | `number \| string` | `undefined` | Minimum value |
|
|
158
|
+
| `max` | `number \| string` | `undefined` | Maximum value |
|
|
159
|
+
| `step` | `number \| string` | `undefined` | Step increment |
|
|
160
|
+
| `prefix` | `string \| Snippet` | `undefined` | Content before input (e.g., "$") |
|
|
161
|
+
| `suffix` | `string \| Snippet` | `undefined` | Content after input (e.g., "USD") |
|
|
162
|
+
| `showControls` | `boolean` | `true` | Show spinner controls |
|
|
163
|
+
| `align` | `'start' \| 'end'` | `'start'` | Text alignment |
|
|
164
|
+
| `maxDecimals` | `number` | `undefined` | Max decimal places (0 = integers only) |
|
|
153
165
|
|
|
154
166
|
#### Textarea
|
|
155
167
|
|
|
@@ -257,12 +269,14 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
257
269
|
<Sfield name="ssn" type="masked" label="SSN" mask="###-##-####" />
|
|
258
270
|
```
|
|
259
271
|
|
|
260
|
-
| Prop | Type
|
|
261
|
-
| --------------------- |
|
|
262
|
-
| `mask` | `string`
|
|
263
|
-
| `maskPlaceholder` | `string`
|
|
264
|
-
| `showMaskPlaceholder` | `boolean`
|
|
265
|
-
| `
|
|
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 |
|
|
266
280
|
|
|
267
281
|
**Mask Tokens:**
|
|
268
282
|
|
|
@@ -295,16 +309,17 @@ Stateful submit button that reacts to form state.
|
|
|
295
309
|
</Sbutton>
|
|
296
310
|
```
|
|
297
311
|
|
|
298
|
-
| Prop | Type | Default | Description
|
|
299
|
-
| -------------- | --------------------------------- | ----------- |
|
|
300
|
-
| `label` | `string` | `'Submit'` | Button text
|
|
301
|
-
| `buttonType` | `'submit' \| 'reset' \| 'button'` | `'submit'` | Button type
|
|
302
|
-
| `class` | `string` | `undefined` | CSS class
|
|
303
|
-
| `disabled` | `boolean` | `false` | Disable button
|
|
304
|
-
| `
|
|
305
|
-
| `
|
|
306
|
-
| `
|
|
307
|
-
| `
|
|
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 |
|
|
308
323
|
|
|
309
324
|
## Styling
|
|
310
325
|
|
|
@@ -395,11 +410,3 @@ npm run package
|
|
|
395
410
|
## License
|
|
396
411
|
|
|
397
412
|
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
|
-
```
|
package/dist/Sform/Sfield.svelte
CHANGED
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
// Derive name from the field - all field types include name in their .as() output
|
|
39
39
|
const name = $derived(field.as('text').name);
|
|
40
40
|
|
|
41
|
+
// Register this field with the context on mount
|
|
42
|
+
$effect(() => {
|
|
43
|
+
context.registerField(name);
|
|
44
|
+
});
|
|
45
|
+
|
|
41
46
|
const classes: SfieldClasses = $derived(
|
|
42
47
|
typeof props.class === 'string' ? { wrapper: props.class } : (props.class ?? {})
|
|
43
48
|
);
|
package/dist/Sform/Sform.svelte
CHANGED
|
@@ -53,7 +53,15 @@
|
|
|
53
53
|
form.validate({ includeUntouched: true });
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
const context = createSformContext(
|
|
56
|
+
const context = createSformContext(
|
|
57
|
+
() => validateOn,
|
|
58
|
+
getFieldNames,
|
|
59
|
+
triggerValidation,
|
|
60
|
+
() => {
|
|
61
|
+
// Use setTimeout to ensure we're outside the current event loop
|
|
62
|
+
setTimeout(() => formElement?.requestSubmit(), 0);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
57
65
|
|
|
58
66
|
// Apply preflight schema if provided
|
|
59
67
|
const formWithSchema = $derived(schema ? form.preflight(schema) : form);
|
|
@@ -88,9 +96,12 @@
|
|
|
88
96
|
context.markSubmitted();
|
|
89
97
|
context.markAllFieldsDirty();
|
|
90
98
|
}
|
|
99
|
+
|
|
100
|
+
let formElement: HTMLFormElement | undefined = $state();
|
|
91
101
|
</script>
|
|
92
102
|
|
|
93
103
|
<form
|
|
104
|
+
bind:this={formElement}
|
|
94
105
|
{...formProps as unknown as HTMLFormAttributes}
|
|
95
106
|
class={className}
|
|
96
107
|
novalidate
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { SformContext, ValidateOn } from './types.js';
|
|
2
|
-
export declare function createSformContext(getValidateOn: () => ValidateOn, getFieldNames: () => string[], triggerValidation: () => void): SformContext;
|
|
2
|
+
export declare function createSformContext(getValidateOn: () => ValidateOn, getFieldNames: () => string[], triggerValidation: () => void, submitForm: () => void): SformContext;
|
|
3
3
|
export declare function getSformContext(): SformContext;
|
|
@@ -1,9 +1,10 @@
|
|
|
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) {
|
|
4
|
+
export function createSformContext(getValidateOn, getFieldNames, triggerValidation, submitForm) {
|
|
5
5
|
const touched = new SvelteSet();
|
|
6
6
|
const dirty = new SvelteSet();
|
|
7
|
+
const registeredFields = new SvelteSet();
|
|
7
8
|
let submitted = $state(false);
|
|
8
9
|
const context = {
|
|
9
10
|
get validateOn() {
|
|
@@ -40,18 +41,22 @@ export function createSformContext(getValidateOn, getFieldNames, triggerValidati
|
|
|
40
41
|
submitted = true;
|
|
41
42
|
},
|
|
42
43
|
markAllFieldsDirty: () => {
|
|
43
|
-
//
|
|
44
|
-
const fieldNames =
|
|
44
|
+
// Use registered fields from Sfield components
|
|
45
|
+
const fieldNames = [...registeredFields];
|
|
45
46
|
for (const name of fieldNames) {
|
|
46
47
|
touched.add(name);
|
|
47
48
|
dirty.add(name);
|
|
48
49
|
}
|
|
49
50
|
},
|
|
51
|
+
registerField: (name) => {
|
|
52
|
+
registeredFields.add(name);
|
|
53
|
+
},
|
|
50
54
|
resetFieldStates: () => {
|
|
51
55
|
touched.clear();
|
|
52
56
|
dirty.clear();
|
|
53
57
|
submitted = false;
|
|
54
|
-
}
|
|
58
|
+
},
|
|
59
|
+
submitForm
|
|
55
60
|
};
|
|
56
61
|
setContext(SFORM_CONTEXT_KEY, context);
|
|
57
62
|
return context;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { ButtonFormState, RemoteFormIssue } from '../types.js';
|
|
4
|
+
import { getSformContext } from '../context.svelte.js';
|
|
4
5
|
|
|
5
6
|
interface FormLike {
|
|
6
7
|
pending?: number;
|
|
@@ -21,6 +22,8 @@
|
|
|
21
22
|
class?: string;
|
|
22
23
|
/** Whether button is disabled */
|
|
23
24
|
disabled?: boolean;
|
|
25
|
+
/** Callback that runs before validation/submission (can be async) */
|
|
26
|
+
onsubmit?: () => void | Promise<void>;
|
|
24
27
|
/** Snippet for default state */
|
|
25
28
|
defaultState?: Snippet<[ButtonFormState]>;
|
|
26
29
|
/** Snippet for pending state */
|
|
@@ -37,12 +40,16 @@
|
|
|
37
40
|
buttonType = 'submit',
|
|
38
41
|
class: className,
|
|
39
42
|
disabled = false,
|
|
43
|
+
onsubmit,
|
|
40
44
|
defaultState,
|
|
41
45
|
pendingState,
|
|
42
46
|
successState,
|
|
43
47
|
errorState
|
|
44
48
|
}: Props = $props();
|
|
45
49
|
|
|
50
|
+
// Get Sform context to trigger validation display
|
|
51
|
+
const sformContext = getSformContext();
|
|
52
|
+
|
|
46
53
|
const formState: ButtonFormState = $derived.by(() => {
|
|
47
54
|
const pending = (form.pending ?? 0) !== 0;
|
|
48
55
|
const hasResult = form.result !== undefined;
|
|
@@ -58,9 +65,23 @@
|
|
|
58
65
|
});
|
|
59
66
|
|
|
60
67
|
const isDisabled = $derived(disabled || formState.pending);
|
|
68
|
+
|
|
69
|
+
async function handleClick(event: MouseEvent) {
|
|
70
|
+
if (buttonType !== 'submit' || !onsubmit) return;
|
|
71
|
+
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
await onsubmit();
|
|
74
|
+
|
|
75
|
+
// Mark form as submitted and all fields dirty so issues display when server responds
|
|
76
|
+
sformContext.markSubmitted();
|
|
77
|
+
sformContext.markAllFieldsDirty();
|
|
78
|
+
|
|
79
|
+
// Submit the form via context
|
|
80
|
+
sformContext.submitForm();
|
|
81
|
+
}
|
|
61
82
|
</script>
|
|
62
83
|
|
|
63
|
-
<button type={buttonType} class={className} disabled={isDisabled}>
|
|
84
|
+
<button type={buttonType} class={className} disabled={isDisabled} onclick={handleClick}>
|
|
64
85
|
{#if formState.pending && pendingState}
|
|
65
86
|
{@render pendingState(formState)}
|
|
66
87
|
{:else if formState.success && successState}
|
|
@@ -18,6 +18,8 @@ interface Props {
|
|
|
18
18
|
class?: string;
|
|
19
19
|
/** Whether button is disabled */
|
|
20
20
|
disabled?: boolean;
|
|
21
|
+
/** Callback that runs before validation/submission (can be async) */
|
|
22
|
+
onsubmit?: () => void | Promise<void>;
|
|
21
23
|
/** Snippet for default state */
|
|
22
24
|
defaultState?: Snippet<[ButtonFormState]>;
|
|
23
25
|
/** Snippet for pending state */
|
package/dist/Sform/types.d.ts
CHANGED
|
@@ -130,6 +130,10 @@ export interface SformContext {
|
|
|
130
130
|
markAllFieldsDirty: () => void;
|
|
131
131
|
/** Reset all field states (touched, dirty, submitted) */
|
|
132
132
|
resetFieldStates: () => void;
|
|
133
|
+
/** Register a field name (called by Sfield on mount) */
|
|
134
|
+
registerField: (name: string) => void;
|
|
135
|
+
/** Programmatically submit the form */
|
|
136
|
+
submitForm: () => void;
|
|
133
137
|
}
|
|
134
138
|
/**
|
|
135
139
|
* Enhance callback options - typed based on form Input type.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@samuel-charpentier/sform",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "A Svelte 5 form library for SvelteKit remote forms",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
57
57
|
"@types/node": "^22",
|
|
58
58
|
"@vitest/browser-playwright": "^4.0.15",
|
|
59
|
+
"@vitest/coverage-v8": "4.0.16",
|
|
59
60
|
"eslint": "^9.39.1",
|
|
60
61
|
"eslint-config-prettier": "^10.1.8",
|
|
61
62
|
"eslint-plugin-svelte": "^3.13.1",
|