@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.
@@ -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
 
@@ -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
- // Register for issue display tracking (all types except hidden display their issues)
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(context.shouldDisplayIssues(name, props.validateOn));
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
- // Props that Sfield manages internally - these are set by Sfield, not passed from parent
68
- const internalPropKeys = [
69
- 'field',
70
- 'name',
71
- 'showIssues',
72
- 'onblur',
73
- 'oninput',
74
- 'labelClass',
75
- 'class'
76
- ];
77
- // Props that are Sfield-specific and not passed to components
78
- const sfieldOnlyPropKeys = ['validateOn', 'hint', 'type'];
79
-
80
- // Passthrough props: everything from parent except internal and sfield-only props
81
- // This allows new component props to automatically flow through without updating Sfield
82
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
- const passthroughProps = $derived((): any => {
84
- const result: Record<string, unknown> = {};
85
- for (const [key, value] of Object.entries(props)) {
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
- return result;
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
- {...passthroughProps()}
128
- {...internalProps}
129
- type={props.type as import('./types.js').TextInputType}
130
- />
131
- {:else if props.type === 'password'}
132
- <PasswordInput {...passthroughProps()} {...internalProps} />
133
- {:else if props.type === 'number'}
134
- <NumberInput {...passthroughProps()} {...internalProps} />
135
- {:else if props.type === 'textarea'}
136
- <TextareaInput {...passthroughProps()} {...internalProps} />
137
- {:else if props.type === 'select'}
138
- <SelectInput {...passthroughProps()} {...internalProps} />
139
- {:else if props.type === 'checkbox'}
140
- <CheckboxInput {...passthroughProps()} {...internalProps} />
141
- {:else if props.type === 'checkbox-group'}
142
- <CheckboxGroupInput {...passthroughProps()} {...internalProps} />
143
- {:else if props.type === 'radio'}
144
- <RadioInput {...passthroughProps()} {...internalProps} />
145
- {:else if props.type === 'range'}
146
- <RangeInput {...passthroughProps()} {...internalProps} />
147
- {:else if props.type === 'toggle'}
148
- <ToggleInput {...passthroughProps()} {...internalProps} />
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}
@@ -1,5 +1,10 @@
1
1
  <script lang="ts" generics="Input extends import('@sveltejs/kit').RemoteFormInput, Output">
2
- import type { RemoteFormInstance, ValidateOn, EnhanceCallback } from './types.js';
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
- children
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
- form.validate({ includeUntouched: true, preflightOnly });
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(enhance ? formWithSchema.enhance(enhance) : formWithSchema);
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
- // Submission just completed successfully (was pending, now not, has result, no issues)
84
- if (wasPending && !isPending && hasResult && hasNoIssues) {
85
- context.resetFieldStates();
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
- triggerValidation();
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, submitForm: () => void, getForm: () => FormLike): SformContext;
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();
@@ -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, 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, 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';