@samuel-charpentier/sform 1.0.0 → 1.1.0
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 +697 -587
- package/dist/Sform/SIssues.svelte +1 -1
- package/dist/Sform/Sfield.svelte +226 -56
- package/dist/Sform/Sform.svelte +79 -11
- package/dist/Sform/Sform.svelte.d.ts +6 -1
- package/dist/Sform/context.svelte.d.ts +1 -1
- package/dist/Sform/context.svelte.js +41 -1
- package/dist/Sform/index.d.ts +1 -1
- package/dist/Sform/inputs/ButtonInput.svelte +26 -34
- package/dist/Sform/inputs/ButtonInput.svelte.d.ts +2 -23
- package/dist/Sform/inputs/HiddenInput.svelte +10 -10
- package/dist/Sform/inputs/MaskedInput.svelte +1 -1
- package/dist/Sform/sform.css +0 -41
- package/dist/Sform/types.d.ts +59 -36
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
const formState = $derived(context.getFormState());
|
|
23
23
|
|
|
24
24
|
// Show issues only after form submission and when there are issues
|
|
25
|
-
const shouldShow = $derived(context.submitted && formState.hasIssues);
|
|
25
|
+
const shouldShow = $derived(!context.disabled && context.submitted && formState.hasIssues);
|
|
26
26
|
const hasUnhandledIssues = $derived(unhandledIssues.length > 0);
|
|
27
27
|
</script>
|
|
28
28
|
|
package/dist/Sform/Sfield.svelte
CHANGED
|
@@ -3,7 +3,19 @@
|
|
|
3
3
|
SfieldClasses,
|
|
4
4
|
RemoteFormField,
|
|
5
5
|
RemoteFormFieldValue,
|
|
6
|
-
TypedSfieldProps
|
|
6
|
+
TypedSfieldProps,
|
|
7
|
+
SfieldTextProps,
|
|
8
|
+
SfieldPasswordProps,
|
|
9
|
+
SfieldNumberProps,
|
|
10
|
+
SfieldTextareaProps,
|
|
11
|
+
SfieldSelectProps,
|
|
12
|
+
SfieldCheckboxProps,
|
|
13
|
+
SfieldCheckboxGroupProps,
|
|
14
|
+
SfieldRadioProps,
|
|
15
|
+
SfieldRangeProps,
|
|
16
|
+
SfieldToggleProps,
|
|
17
|
+
SfieldMaskedProps,
|
|
18
|
+
SfieldHiddenProps
|
|
7
19
|
} from './types.js';
|
|
8
20
|
import { getSformContext } from './context.svelte.js';
|
|
9
21
|
import TextInput from './inputs/TextInput.svelte';
|
|
@@ -37,58 +49,219 @@
|
|
|
37
49
|
|
|
38
50
|
// Derive name from the field - all field types include name in their .as() output
|
|
39
51
|
const name = $derived(field.as('text').name);
|
|
52
|
+
const issueDisplay = $derived(props.issueDisplay ?? 'auto');
|
|
53
|
+
const shouldRenderFieldIssues = $derived(
|
|
54
|
+
issueDisplay === 'field' || (issueDisplay === 'auto' && props.type !== 'hidden')
|
|
55
|
+
);
|
|
56
|
+
const shouldMarkIssuesHandled = $derived(issueDisplay === 'none' || shouldRenderFieldIssues);
|
|
57
|
+
|
|
58
|
+
// Form-level disabled always wins over a field's own disabled prop (OR semantics)
|
|
59
|
+
const effectiveDisabled = $derived(context.disabled || props.disabled === true);
|
|
40
60
|
|
|
41
61
|
// Register this field with the context on mount
|
|
42
62
|
$effect(() => {
|
|
43
63
|
context.registerField(name);
|
|
44
|
-
|
|
45
|
-
if (props.type !== 'hidden') {
|
|
64
|
+
if (shouldMarkIssuesHandled) {
|
|
46
65
|
context.registerFieldWithIssueDisplay(name);
|
|
47
66
|
}
|
|
48
67
|
});
|
|
49
68
|
|
|
69
|
+
$effect(() => {
|
|
70
|
+
if (!props.lifecycle) return;
|
|
71
|
+
return context.registerLifecycleHooks(props.lifecycle);
|
|
72
|
+
});
|
|
73
|
+
|
|
50
74
|
const classes: SfieldClasses = $derived(
|
|
51
75
|
typeof props.class === 'string' ? { wrapper: props.class } : (props.class ?? {})
|
|
52
76
|
);
|
|
53
77
|
|
|
54
|
-
const showIssues = $derived(
|
|
78
|
+
const showIssues = $derived(
|
|
79
|
+
shouldRenderFieldIssues && context.shouldDisplayIssues(name, props.validateOn)
|
|
80
|
+
);
|
|
55
81
|
const issues = $derived(showIssues ? field.issues() : []);
|
|
56
82
|
const hasIssues = $derived(issues && issues instanceof Array && issues.length > 0);
|
|
57
83
|
|
|
58
84
|
async function handleBlur() {
|
|
85
|
+
if (effectiveDisabled) return;
|
|
59
86
|
context.markTouched(name);
|
|
60
87
|
context.triggerValidation();
|
|
61
88
|
}
|
|
62
89
|
|
|
63
90
|
function handleInput() {
|
|
91
|
+
if (effectiveDisabled) return;
|
|
64
92
|
context.markDirty(name);
|
|
65
93
|
}
|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (!internalPropKeys.includes(key) && !sfieldOnlyPropKeys.includes(key)) {
|
|
87
|
-
result[key] = value;
|
|
88
|
-
}
|
|
95
|
+
function isTextSfieldProps(
|
|
96
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
97
|
+
): input is SfieldTextProps {
|
|
98
|
+
switch (input.type) {
|
|
99
|
+
case 'text':
|
|
100
|
+
case 'email':
|
|
101
|
+
case 'tel':
|
|
102
|
+
case 'url':
|
|
103
|
+
case 'search':
|
|
104
|
+
case 'date':
|
|
105
|
+
case 'datetime-local':
|
|
106
|
+
case 'time':
|
|
107
|
+
case 'month':
|
|
108
|
+
case 'week':
|
|
109
|
+
case 'color':
|
|
110
|
+
case 'file':
|
|
111
|
+
return true;
|
|
112
|
+
default:
|
|
113
|
+
return false;
|
|
89
114
|
}
|
|
90
|
-
|
|
91
|
-
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isPasswordSfieldProps(
|
|
118
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
119
|
+
): input is SfieldPasswordProps {
|
|
120
|
+
return input.type === 'password';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isNumberSfieldProps(
|
|
124
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
125
|
+
): input is SfieldNumberProps {
|
|
126
|
+
return input.type === 'number';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isTextareaSfieldProps(
|
|
130
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
131
|
+
): input is SfieldTextareaProps {
|
|
132
|
+
return input.type === 'textarea';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isSelectSfieldProps(
|
|
136
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
137
|
+
): input is SfieldSelectProps {
|
|
138
|
+
return input.type === 'select';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isCheckboxSfieldProps(
|
|
142
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
143
|
+
): input is SfieldCheckboxProps {
|
|
144
|
+
return input.type === 'checkbox';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isCheckboxGroupSfieldProps(
|
|
148
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
149
|
+
): input is SfieldCheckboxGroupProps {
|
|
150
|
+
return input.type === 'checkbox-group';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isRadioSfieldProps(
|
|
154
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
155
|
+
): input is SfieldRadioProps {
|
|
156
|
+
return input.type === 'radio';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isRangeSfieldProps(
|
|
160
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
161
|
+
): input is SfieldRangeProps {
|
|
162
|
+
return input.type === 'range';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isToggleSfieldProps(
|
|
166
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
167
|
+
): input is SfieldToggleProps {
|
|
168
|
+
return input.type === 'toggle';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isMaskedSfieldProps(
|
|
172
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
173
|
+
): input is SfieldMaskedProps {
|
|
174
|
+
return input.type === 'masked';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isHiddenSfieldProps(
|
|
178
|
+
input: TypedSfieldProps<RemoteFormFieldValue>
|
|
179
|
+
): input is SfieldHiddenProps {
|
|
180
|
+
return input.type === 'hidden';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Props that Sfield manages internally - these are set by Sfield, not passed from parent
|
|
184
|
+
type SfieldManagedProps =
|
|
185
|
+
| 'field'
|
|
186
|
+
| 'validateOn'
|
|
187
|
+
| 'issueDisplay'
|
|
188
|
+
| 'hint'
|
|
189
|
+
| 'type'
|
|
190
|
+
| 'lifecycle'
|
|
191
|
+
| 'class'
|
|
192
|
+
| 'disabled';
|
|
193
|
+
|
|
194
|
+
type InputPassthrough<T extends TypedSfieldProps<RemoteFormFieldValue>> = Omit<
|
|
195
|
+
T,
|
|
196
|
+
SfieldManagedProps
|
|
197
|
+
>;
|
|
198
|
+
|
|
199
|
+
function getPassthroughProps<T extends TypedSfieldProps<RemoteFormFieldValue>>(
|
|
200
|
+
input: T
|
|
201
|
+
): InputPassthrough<T> {
|
|
202
|
+
const {
|
|
203
|
+
field,
|
|
204
|
+
validateOn,
|
|
205
|
+
issueDisplay,
|
|
206
|
+
hint,
|
|
207
|
+
type,
|
|
208
|
+
lifecycle,
|
|
209
|
+
class: className,
|
|
210
|
+
disabled: fieldDisabled,
|
|
211
|
+
...rest
|
|
212
|
+
} = input;
|
|
213
|
+
|
|
214
|
+
void field;
|
|
215
|
+
void validateOn;
|
|
216
|
+
void issueDisplay;
|
|
217
|
+
void hint;
|
|
218
|
+
void type;
|
|
219
|
+
void lifecycle;
|
|
220
|
+
void className;
|
|
221
|
+
void fieldDisabled;
|
|
222
|
+
|
|
223
|
+
return rest;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Passthrough props: everything from parent except Sfield-managed props.
|
|
227
|
+
// Keep this generic so each input branch can recover exact prop typing.
|
|
228
|
+
const textPassthrough = $derived.by(() =>
|
|
229
|
+
isTextSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
230
|
+
);
|
|
231
|
+
const passwordPassthrough = $derived.by(() =>
|
|
232
|
+
isPasswordSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
233
|
+
);
|
|
234
|
+
const numberPassthrough = $derived.by(() =>
|
|
235
|
+
isNumberSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
236
|
+
);
|
|
237
|
+
const textareaPassthrough = $derived.by(() =>
|
|
238
|
+
isTextareaSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
239
|
+
);
|
|
240
|
+
const selectPassthrough = $derived.by(() =>
|
|
241
|
+
isSelectSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
242
|
+
);
|
|
243
|
+
const checkboxPassthrough = $derived.by(() =>
|
|
244
|
+
isCheckboxSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
245
|
+
);
|
|
246
|
+
const checkboxGroupPassthrough = $derived.by(() =>
|
|
247
|
+
isCheckboxGroupSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
248
|
+
);
|
|
249
|
+
const radioPassthrough = $derived.by(() =>
|
|
250
|
+
isRadioSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
251
|
+
);
|
|
252
|
+
const rangePassthrough = $derived.by(() =>
|
|
253
|
+
isRangeSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
254
|
+
);
|
|
255
|
+
const togglePassthrough = $derived.by(() =>
|
|
256
|
+
isToggleSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
257
|
+
);
|
|
258
|
+
const maskedPassthrough = $derived.by(() =>
|
|
259
|
+
isMaskedSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
260
|
+
);
|
|
261
|
+
const hiddenPassthrough = $derived.by(() =>
|
|
262
|
+
isHiddenSfieldProps(props) ? getPassthroughProps(props) : undefined
|
|
263
|
+
);
|
|
264
|
+
const textInputType = $derived.by(() => (isTextSfieldProps(props) ? props.type : undefined));
|
|
92
265
|
|
|
93
266
|
// Internal props that Sfield computes/manages
|
|
94
267
|
const internalProps = $derived({
|
|
@@ -98,6 +271,7 @@
|
|
|
98
271
|
labelClass: classes.label,
|
|
99
272
|
wrapperClass: classes.inputWrapper,
|
|
100
273
|
showIssues,
|
|
274
|
+
disabled: effectiveDisabled,
|
|
101
275
|
onblur: handleBlur,
|
|
102
276
|
oninput: handleInput
|
|
103
277
|
});
|
|
@@ -122,34 +296,30 @@
|
|
|
122
296
|
</script>
|
|
123
297
|
|
|
124
298
|
<div class={classes.wrapper}>
|
|
125
|
-
{#if isTextType}
|
|
126
|
-
<TextInput
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/>
|
|
131
|
-
{:else if
|
|
132
|
-
<
|
|
133
|
-
{:else if
|
|
134
|
-
<
|
|
135
|
-
{:else if
|
|
136
|
-
<
|
|
137
|
-
{:else if
|
|
138
|
-
<
|
|
139
|
-
{:else if
|
|
140
|
-
<
|
|
141
|
-
{:else if
|
|
142
|
-
<
|
|
143
|
-
{:else if
|
|
144
|
-
<
|
|
145
|
-
{:else if
|
|
146
|
-
<
|
|
147
|
-
{:else if
|
|
148
|
-
<
|
|
149
|
-
{:else if props.type === 'masked'}
|
|
150
|
-
<MaskedInput {...passthroughProps()} {...internalProps} />
|
|
151
|
-
{:else if props.type === 'hidden'}
|
|
152
|
-
<HiddenInput {...passthroughProps()} {...internalProps} />
|
|
299
|
+
{#if isTextType && textPassthrough && textInputType}
|
|
300
|
+
<TextInput {...textPassthrough} {...internalProps} type={textInputType} />
|
|
301
|
+
{:else if passwordPassthrough}
|
|
302
|
+
<PasswordInput {...passwordPassthrough} {...internalProps} />
|
|
303
|
+
{:else if numberPassthrough}
|
|
304
|
+
<NumberInput {...numberPassthrough} {...internalProps} />
|
|
305
|
+
{:else if textareaPassthrough}
|
|
306
|
+
<TextareaInput {...textareaPassthrough} {...internalProps} />
|
|
307
|
+
{:else if selectPassthrough}
|
|
308
|
+
<SelectInput {...selectPassthrough} {...internalProps} />
|
|
309
|
+
{:else if checkboxPassthrough}
|
|
310
|
+
<CheckboxInput {...checkboxPassthrough} {...internalProps} />
|
|
311
|
+
{:else if checkboxGroupPassthrough}
|
|
312
|
+
<CheckboxGroupInput {...checkboxGroupPassthrough} {...internalProps} />
|
|
313
|
+
{:else if radioPassthrough}
|
|
314
|
+
<RadioInput {...radioPassthrough} {...internalProps} />
|
|
315
|
+
{:else if rangePassthrough}
|
|
316
|
+
<RangeInput {...rangePassthrough} {...internalProps} />
|
|
317
|
+
{:else if togglePassthrough}
|
|
318
|
+
<ToggleInput {...togglePassthrough} {...internalProps} />
|
|
319
|
+
{:else if maskedPassthrough}
|
|
320
|
+
<MaskedInput {...maskedPassthrough} {...internalProps} />
|
|
321
|
+
{:else if hiddenPassthrough}
|
|
322
|
+
<HiddenInput {...hiddenPassthrough} {...internalProps} />
|
|
153
323
|
{/if}
|
|
154
324
|
|
|
155
325
|
{#if props.hint}
|
package/dist/Sform/Sform.svelte
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<script lang="ts" generics="Input extends import('@sveltejs/kit').RemoteFormInput, Output">
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
RemoteFormInstance,
|
|
4
|
+
ValidateOn,
|
|
5
|
+
EnhanceCallback,
|
|
6
|
+
SformLifecycleHooks
|
|
7
|
+
} from './types.js';
|
|
3
8
|
import type { RemoteFormFields } from '@sveltejs/kit';
|
|
4
9
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
5
10
|
import { createSformContext } from './context.svelte.js';
|
|
@@ -18,7 +23,10 @@
|
|
|
18
23
|
validateOn = 'blur',
|
|
19
24
|
class: className,
|
|
20
25
|
preflightOnly = false,
|
|
21
|
-
|
|
26
|
+
resetOnSuccess = true,
|
|
27
|
+
lifecycle,
|
|
28
|
+
children,
|
|
29
|
+
disabled
|
|
22
30
|
}: {
|
|
23
31
|
/** Remote form object from form() API, or the result of form.for(id) */
|
|
24
32
|
form: RemoteFormInstance<Input, Output>;
|
|
@@ -30,6 +38,10 @@
|
|
|
30
38
|
validateOn?: ValidateOn;
|
|
31
39
|
/** If true, only run preflight validation (no submission) */
|
|
32
40
|
preflightOnly?: boolean;
|
|
41
|
+
/** If true, reset touched/dirty/submitted state after successful submit response */
|
|
42
|
+
resetOnSuccess?: boolean;
|
|
43
|
+
/** Lifecycle hooks for submit/validate phases */
|
|
44
|
+
lifecycle?: SformLifecycleHooks;
|
|
33
45
|
/** Form element class */
|
|
34
46
|
class?: string;
|
|
35
47
|
/**
|
|
@@ -44,6 +56,7 @@
|
|
|
44
56
|
* ```
|
|
45
57
|
*/
|
|
46
58
|
children: Snippet<[FormFields]>;
|
|
59
|
+
disabled?: boolean;
|
|
47
60
|
} = $props();
|
|
48
61
|
|
|
49
62
|
// Get field names for marking all dirty on submit
|
|
@@ -51,9 +64,32 @@
|
|
|
51
64
|
return Object.keys(form.fields).filter((key) => !['value', 'set', 'allIssues'].includes(key));
|
|
52
65
|
};
|
|
53
66
|
|
|
67
|
+
const isDisabled = $derived(disabled === true);
|
|
68
|
+
|
|
54
69
|
// Trigger validation including untouched fields (for blur mode)
|
|
55
|
-
const triggerValidation = () => {
|
|
56
|
-
|
|
70
|
+
const triggerValidation = async () => {
|
|
71
|
+
if (isDisabled) return;
|
|
72
|
+
|
|
73
|
+
await context.runLifecycleHooks('beforeValidate');
|
|
74
|
+
|
|
75
|
+
const validateRequest = form.validate({ includeUntouched: true, preflightOnly });
|
|
76
|
+
let afterCalledError: unknown;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await context.runLifecycleHooks('afterValidateCalled');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
afterCalledError = error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await validateRequest;
|
|
86
|
+
} finally {
|
|
87
|
+
await context.runLifecycleHooks('afterValidateSettled');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (afterCalledError) {
|
|
91
|
+
throw afterCalledError;
|
|
92
|
+
}
|
|
57
93
|
};
|
|
58
94
|
|
|
59
95
|
const context = createSformContext(
|
|
@@ -61,14 +97,33 @@
|
|
|
61
97
|
getFieldNames,
|
|
62
98
|
triggerValidation,
|
|
63
99
|
() => formElement?.requestSubmit(),
|
|
64
|
-
() => form
|
|
100
|
+
() => form,
|
|
101
|
+
() => isDisabled
|
|
65
102
|
);
|
|
66
103
|
|
|
104
|
+
$effect(() => {
|
|
105
|
+
if (!lifecycle) return;
|
|
106
|
+
return context.registerLifecycleHooks(lifecycle);
|
|
107
|
+
});
|
|
108
|
+
|
|
67
109
|
// Apply preflight schema if provided
|
|
68
110
|
const formWithSchema = $derived(schema ? form.preflight(schema) : form);
|
|
69
111
|
|
|
112
|
+
// Guard the consumer-provided enhance callback so a disabled form never submits,
|
|
113
|
+
// even if the native submit-blocking guards below are bypassed.
|
|
114
|
+
const guardedEnhance = $derived(
|
|
115
|
+
enhance
|
|
116
|
+
? (opts: Parameters<EnhanceCallback<Input>>[0]) => {
|
|
117
|
+
if (isDisabled) return;
|
|
118
|
+
return enhance(opts);
|
|
119
|
+
}
|
|
120
|
+
: undefined
|
|
121
|
+
);
|
|
122
|
+
|
|
70
123
|
// Apply enhance if provided - returns a minimal object for spreading onto form element
|
|
71
|
-
const formProps = $derived(
|
|
124
|
+
const formProps = $derived(
|
|
125
|
+
guardedEnhance ? formWithSchema.enhance(guardedEnhance) : formWithSchema
|
|
126
|
+
);
|
|
72
127
|
|
|
73
128
|
// Track previous pending state to detect submission completion
|
|
74
129
|
let wasPending = $state(false);
|
|
@@ -80,19 +135,29 @@
|
|
|
80
135
|
(form.fields as { allIssues?: () => unknown[] | undefined }).allIssues?.() ?? [];
|
|
81
136
|
const hasNoIssues = allIssues.length === 0;
|
|
82
137
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
138
|
+
if (wasPending && !isPending) {
|
|
139
|
+
void context.runLifecycleHooks('afterSubmitResponse');
|
|
140
|
+
|
|
141
|
+
// Submission just completed successfully (was pending, now not, has result, no issues)
|
|
142
|
+
if (resetOnSuccess && hasResult && hasNoIssues) {
|
|
143
|
+
context.resetFieldStates();
|
|
144
|
+
}
|
|
86
145
|
}
|
|
87
146
|
|
|
88
147
|
wasPending = isPending;
|
|
89
148
|
});
|
|
90
149
|
|
|
91
150
|
function handleInput() {
|
|
92
|
-
|
|
151
|
+
if (isDisabled) return;
|
|
152
|
+
void triggerValidation();
|
|
93
153
|
}
|
|
94
154
|
|
|
95
|
-
function handleSubmit() {
|
|
155
|
+
function handleSubmit(event: SubmitEvent) {
|
|
156
|
+
if (isDisabled) {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
event.stopImmediatePropagation();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
96
161
|
context.markSubmitted();
|
|
97
162
|
context.markAllFieldsDirty();
|
|
98
163
|
}
|
|
@@ -100,6 +165,7 @@
|
|
|
100
165
|
let formElement: HTMLFormElement | undefined = $state();
|
|
101
166
|
</script>
|
|
102
167
|
|
|
168
|
+
<!-- svelte-ignore a11y_role_supports_aria_props_implicit -->
|
|
103
169
|
<form
|
|
104
170
|
bind:this={formElement}
|
|
105
171
|
{...formProps as unknown as HTMLFormAttributes}
|
|
@@ -107,6 +173,8 @@
|
|
|
107
173
|
novalidate
|
|
108
174
|
oninput={handleInput}
|
|
109
175
|
onsubmit={handleSubmit}
|
|
176
|
+
aria-disabled={isDisabled ? 'true' : undefined}
|
|
177
|
+
data-disabled={isDisabled ? '' : undefined}
|
|
110
178
|
>
|
|
111
179
|
{@render (children as Snippet<[FormFields]>)(form.fields)}
|
|
112
180
|
</form>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RemoteFormInstance, ValidateOn, EnhanceCallback } from './types.js';
|
|
1
|
+
import type { RemoteFormInstance, ValidateOn, EnhanceCallback, SformLifecycleHooks } from './types.js';
|
|
2
2
|
import type { RemoteFormFields } from '@sveltejs/kit';
|
|
3
3
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
4
4
|
import type { Snippet } from 'svelte';
|
|
@@ -14,6 +14,10 @@ declare function $$render<Input extends import('@sveltejs/kit').RemoteFormInput,
|
|
|
14
14
|
validateOn?: ValidateOn;
|
|
15
15
|
/** If true, only run preflight validation (no submission) */
|
|
16
16
|
preflightOnly?: boolean;
|
|
17
|
+
/** If true, reset touched/dirty/submitted state after successful submit response */
|
|
18
|
+
resetOnSuccess?: boolean;
|
|
19
|
+
/** Lifecycle hooks for submit/validate phases */
|
|
20
|
+
lifecycle?: SformLifecycleHooks;
|
|
17
21
|
/** Form element class */
|
|
18
22
|
class?: string;
|
|
19
23
|
/**
|
|
@@ -28,6 +32,7 @@ declare function $$render<Input extends import('@sveltejs/kit').RemoteFormInput,
|
|
|
28
32
|
* ```
|
|
29
33
|
*/
|
|
30
34
|
children: Snippet<[RemoteFormFields<Input>]>;
|
|
35
|
+
disabled?: boolean;
|
|
31
36
|
};
|
|
32
37
|
exports: {};
|
|
33
38
|
bindings: "";
|
|
@@ -7,6 +7,6 @@ interface FormLike {
|
|
|
7
7
|
[key: string]: unknown;
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
-
export declare function createSformContext(getValidateOn: () => ValidateOn, getFieldNames: () => string[], triggerValidation: () => void
|
|
10
|
+
export declare function createSformContext(getValidateOn: () => ValidateOn, getFieldNames: () => string[], triggerValidation: () => void | Promise<void>, submitForm: () => void, getForm: () => FormLike, getDisabled?: () => boolean): SformContext;
|
|
11
11
|
export declare function getSformContext(): SformContext;
|
|
12
12
|
export {};
|
|
@@ -1,11 +1,19 @@
|
|
|
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, getForm) {
|
|
4
|
+
export function createSformContext(getValidateOn, getFieldNames, triggerValidation, submitForm, getForm, getDisabled = () => false) {
|
|
5
5
|
const touched = new SvelteSet();
|
|
6
6
|
const dirty = new SvelteSet();
|
|
7
7
|
const registeredFields = new SvelteSet();
|
|
8
8
|
const fieldsWithIssueDisplay = new SvelteSet();
|
|
9
|
+
const lifecycleHooks = {
|
|
10
|
+
beforeSubmit: new SvelteSet(),
|
|
11
|
+
afterSubmitTriggered: new SvelteSet(),
|
|
12
|
+
afterSubmitResponse: new SvelteSet(),
|
|
13
|
+
beforeValidate: new SvelteSet(),
|
|
14
|
+
afterValidateCalled: new SvelteSet(),
|
|
15
|
+
afterValidateSettled: new SvelteSet()
|
|
16
|
+
};
|
|
9
17
|
let submitted = $state(false);
|
|
10
18
|
const context = {
|
|
11
19
|
get validateOn() {
|
|
@@ -14,18 +22,27 @@ export function createSformContext(getValidateOn, getFieldNames, triggerValidati
|
|
|
14
22
|
get submitted() {
|
|
15
23
|
return submitted;
|
|
16
24
|
},
|
|
25
|
+
get disabled() {
|
|
26
|
+
return getDisabled();
|
|
27
|
+
},
|
|
17
28
|
triggerValidation,
|
|
18
29
|
getFieldState: (name) => ({
|
|
19
30
|
touched: touched.has(name),
|
|
20
31
|
dirty: dirty.has(name)
|
|
21
32
|
}),
|
|
22
33
|
markTouched: (name) => {
|
|
34
|
+
if (getDisabled())
|
|
35
|
+
return;
|
|
23
36
|
touched.add(name);
|
|
24
37
|
},
|
|
25
38
|
markDirty: (name) => {
|
|
39
|
+
if (getDisabled())
|
|
40
|
+
return;
|
|
26
41
|
dirty.add(name);
|
|
27
42
|
},
|
|
28
43
|
shouldDisplayIssues: (name, fieldValidateOn) => {
|
|
44
|
+
if (getDisabled())
|
|
45
|
+
return false;
|
|
29
46
|
const effectiveValidateOn = fieldValidateOn ?? getValidateOn();
|
|
30
47
|
switch (effectiveValidateOn) {
|
|
31
48
|
case 'blur':
|
|
@@ -39,9 +56,13 @@ export function createSformContext(getValidateOn, getFieldNames, triggerValidati
|
|
|
39
56
|
}
|
|
40
57
|
},
|
|
41
58
|
markSubmitted: () => {
|
|
59
|
+
if (getDisabled())
|
|
60
|
+
return;
|
|
42
61
|
submitted = true;
|
|
43
62
|
},
|
|
44
63
|
markAllFieldsDirty: () => {
|
|
64
|
+
if (getDisabled())
|
|
65
|
+
return;
|
|
45
66
|
// Use registered fields from Sfield components
|
|
46
67
|
const fieldNames = [...registeredFields];
|
|
47
68
|
for (const name of fieldNames) {
|
|
@@ -55,6 +76,25 @@ export function createSformContext(getValidateOn, getFieldNames, triggerValidati
|
|
|
55
76
|
registerFieldWithIssueDisplay: (name) => {
|
|
56
77
|
fieldsWithIssueDisplay.add(name);
|
|
57
78
|
},
|
|
79
|
+
registerLifecycleHooks: (hooks) => {
|
|
80
|
+
const registrations = [];
|
|
81
|
+
for (const event of Object.keys(lifecycleHooks)) {
|
|
82
|
+
const hook = hooks[event];
|
|
83
|
+
if (!hook)
|
|
84
|
+
continue;
|
|
85
|
+
lifecycleHooks[event].add(hook);
|
|
86
|
+
registrations.push([event, hook]);
|
|
87
|
+
}
|
|
88
|
+
return () => {
|
|
89
|
+
for (const [event, hook] of registrations) {
|
|
90
|
+
lifecycleHooks[event].delete(hook);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
runLifecycleHooks: async (event) => {
|
|
95
|
+
const hooks = [...lifecycleHooks[event]];
|
|
96
|
+
await Promise.all(hooks.map(async (hook) => hook()));
|
|
97
|
+
},
|
|
58
98
|
resetFieldStates: () => {
|
|
59
99
|
touched.clear();
|
|
60
100
|
dirty.clear();
|
package/dist/Sform/index.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { default as Sbutton } from './inputs/ButtonInput.svelte';
|
|
|
4
4
|
export { default as SIssues } from './SIssues.svelte';
|
|
5
5
|
export { default as SResult } from './SResult.svelte';
|
|
6
6
|
export { applyMask, unmask, MASK_PATTERNS, DEFAULT_TOKENS, type MaskOptions, type MaskResult, type MaskToken, type MaskPattern } from './utils/mask.js';
|
|
7
|
-
export type { ValidateOn, FieldState, SfieldClasses, InputType, SelectOption, SformContext, SformProps, ButtonState, ButtonFormState, ButtonInputProps, SIssuesProps, InputAffixProps, RangeInputProps, ToggleInputProps,
|
|
7
|
+
export type { ValidateOn, SformLifecycleEvent, SformLifecycleHook, SformLifecycleHooks, FieldState, SfieldClasses, InputType, SelectOption, SformContext, SformProps, ButtonState, ButtonFormState, ButtonInputProps, SIssuesProps, InputAffixProps, RangeInputProps, ToggleInputProps, SfieldTypeMap, AllowedSfieldType, TypedSfieldProps, SfieldBaseProps, TypedBaseSfieldProps, SfieldTextProps, SfieldPasswordProps, SfieldNumberProps, SfieldTextareaProps, SfieldSelectProps, SfieldCheckboxProps, SfieldCheckboxGroupProps, SfieldRadioProps, SfieldRangeProps, SfieldToggleProps, SfieldMaskedProps, RemoteForm, RemoteFormField, RemoteFormFields, RemoteFormFieldValue, RemoteFormInput, RemoteFormIssue } from './types.js';
|