@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 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 '$lib';
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 | Type | Default | Description |
149
- | ------ | ------------------ | ----------- | -------------- |
150
- | `min` | `number \| string` | `undefined` | Minimum value |
151
- | `max` | `number \| string` | `undefined` | Maximum value |
152
- | `step` | `number \| string` | `undefined` | Step increment |
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 | Default | Description |
261
- | --------------------- | --------- | -------- | -------------------------------- |
262
- | `mask` | `string` | required | Mask pattern |
263
- | `maskPlaceholder` | `string` | `'_'` | Placeholder character |
264
- | `showMaskPlaceholder` | `boolean` | `false` | Show full mask with placeholders |
265
- | `storeRaw` | `boolean` | `true` | Store unmasked value |
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
- | `defaultState` | `Snippet` | `undefined` | Default state snippet |
305
- | `pendingState` | `Snippet` | `undefined` | Pending state snippet |
306
- | `successState` | `Snippet` | `undefined` | Success state snippet |
307
- | `errorState` | `Snippet` | `undefined` | Error state snippet |
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
- ```
@@ -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
  );
@@ -53,7 +53,15 @@
53
53
  form.validate({ includeUntouched: true });
54
54
  };
55
55
 
56
- const context = createSformContext(() => validateOn, getFieldNames, triggerValidation);
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
- // Get all field names from the form and mark them as touched and dirty
44
- const fieldNames = getFieldNames();
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 */
@@ -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.1",
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",