@marianmeres/stuic 3.85.0 → 3.87.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/dist/actions/validate.svelte.d.ts +24 -4
- package/dist/actions/validate.svelte.js +18 -5
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +21 -0
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte.d.ts +4 -1
- package/dist/components/Input/FieldAssets.svelte +48 -3
- package/dist/components/Input/FieldAssets.svelte.d.ts +8 -2
- package/dist/components/Input/FieldCheckbox.svelte +34 -3
- package/dist/components/Input/FieldCheckbox.svelte.d.ts +8 -1
- package/dist/components/Input/FieldCountry.svelte +68 -8
- package/dist/components/Input/FieldCountry.svelte.d.ts +8 -1
- package/dist/components/Input/FieldFile.svelte +34 -3
- package/dist/components/Input/FieldFile.svelte.d.ts +8 -1
- package/dist/components/Input/FieldInput.svelte +43 -3
- package/dist/components/Input/FieldInput.svelte.d.ts +8 -1
- package/dist/components/Input/FieldInputLocalized.svelte +41 -2
- package/dist/components/Input/FieldInputLocalized.svelte.d.ts +8 -2
- package/dist/components/Input/FieldKeyValues.svelte +37 -2
- package/dist/components/Input/FieldKeyValues.svelte.d.ts +8 -2
- package/dist/components/Input/FieldLikeButton.svelte +41 -4
- package/dist/components/Input/FieldLikeButton.svelte.d.ts +8 -1
- package/dist/components/Input/FieldObject.svelte +64 -6
- package/dist/components/Input/FieldObject.svelte.d.ts +8 -2
- package/dist/components/Input/FieldOptions.svelte +36 -3
- package/dist/components/Input/FieldOptions.svelte.d.ts +8 -2
- package/dist/components/Input/FieldPhoneNumber.svelte +51 -6
- package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +8 -1
- package/dist/components/Input/FieldRadios.svelte +36 -2
- package/dist/components/Input/FieldRadios.svelte.d.ts +8 -1
- package/dist/components/Input/FieldSelect.svelte +34 -3
- package/dist/components/Input/FieldSelect.svelte.d.ts +8 -1
- package/dist/components/Input/FieldSwitch.svelte +41 -2
- package/dist/components/Input/FieldSwitch.svelte.d.ts +8 -1
- package/dist/components/Input/FieldTextarea.svelte +34 -3
- package/dist/components/Input/FieldTextarea.svelte.d.ts +8 -1
- package/dist/components/Input/_internal/FieldRadioInternal.svelte +34 -3
- package/dist/components/Input/_internal/FieldRadioInternal.svelte.d.ts +7 -1
- package/dist/components/Input/_internal/countries.js +3 -0
- package/dist/components/Input/index.css +21 -0
- package/dist/components/LoginForm/LoginForm.svelte +35 -0
- package/dist/components/LoginForm/LoginForm.svelte.d.ts +5 -1
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +40 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +5 -1
- package/dist/components/RegisterForm/RegisterForm.svelte +46 -2
- package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +5 -1
- package/dist/components/Switch/Switch.svelte +42 -4
- package/dist/components/Switch/Switch.svelte.d.ts +7 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/validate-fields.d.ts +72 -0
- package/dist/utils/validate-fields.js +73 -0
- package/docs/domains/actions.md +74 -0
- package/docs/domains/components.md +131 -0
- package/docs/domains/utils.md +38 -0
- package/package.json +1 -1
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
off,
|
|
59
59
|
onclick,
|
|
60
60
|
preHook,
|
|
61
|
-
validate
|
|
61
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
62
|
+
validate: validateProp,
|
|
62
63
|
setValidationResult,
|
|
63
64
|
...rest
|
|
64
65
|
}: Props = $props();
|
|
@@ -89,6 +90,39 @@
|
|
|
89
90
|
checkbox.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
|
90
91
|
wrap.focus();
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
//
|
|
95
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
96
|
+
// Local copy of the last validation result so getValidation() works even
|
|
97
|
+
// when no external setValidationResult was provided.
|
|
98
|
+
let _validation: ValidationResult | undefined = $state();
|
|
99
|
+
|
|
100
|
+
/** Trigger validation now. Reaches the parent via `setValidationResult`. */
|
|
101
|
+
export function validate(): ValidationResult | undefined {
|
|
102
|
+
_doValidate?.();
|
|
103
|
+
return _validation;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
107
|
+
export function clearValidation(): void {
|
|
108
|
+
_validation = undefined;
|
|
109
|
+
checkbox?.setCustomValidity?.("");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Current validation state. */
|
|
113
|
+
export function getValidation(): ValidationResult | undefined {
|
|
114
|
+
return _validation;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Focus the visual switch wrapper. */
|
|
118
|
+
export function focus(): void {
|
|
119
|
+
wrap?.focus?.();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Scroll the switch into view. */
|
|
123
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
124
|
+
wrap?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
|
|
125
|
+
}
|
|
92
126
|
</script>
|
|
93
127
|
|
|
94
128
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
@@ -140,9 +174,13 @@
|
|
|
140
174
|
{required}
|
|
141
175
|
{name}
|
|
142
176
|
use:validateAction={() => ({
|
|
143
|
-
enabled:
|
|
144
|
-
...(typeof
|
|
145
|
-
setValidationResult
|
|
177
|
+
enabled: validateProp !== false,
|
|
178
|
+
...(typeof validateProp === "boolean" ? {} : validateProp),
|
|
179
|
+
setValidationResult: (res) => {
|
|
180
|
+
_validation = res;
|
|
181
|
+
setValidationResult?.(res);
|
|
182
|
+
},
|
|
183
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
146
184
|
})}
|
|
147
185
|
tabindex="-1"
|
|
148
186
|
/>
|
|
@@ -29,6 +29,12 @@ export interface Props extends Omit<HTMLLabelAttributes, "children" | "onchange"
|
|
|
29
29
|
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
30
30
|
setValidationResult?: (res: ValidationResult) => void;
|
|
31
31
|
}
|
|
32
|
-
declare const Switch: import("svelte").Component<Props, {
|
|
32
|
+
declare const Switch: import("svelte").Component<Props, {
|
|
33
|
+
validate: () => ValidationResult | undefined;
|
|
34
|
+
clearValidation: () => void;
|
|
35
|
+
getValidation: () => ValidationResult | undefined;
|
|
36
|
+
focus: () => void;
|
|
37
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
38
|
+
}, "button" | "checked">;
|
|
33
39
|
type Switch = ReturnType<typeof Switch>;
|
|
34
40
|
export default Switch;
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ValidationResult } from "../actions/validate.svelte.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal shape any STUIC field component implements once it exposes the
|
|
4
|
+
* imperative validate API. Form-level helpers and consumer aggregators consume
|
|
5
|
+
* this — they don't care what specific field type they're dealing with.
|
|
6
|
+
*
|
|
7
|
+
* Every STUIC `Field*` component (FieldInput, FieldPhoneNumber, FieldCountry,
|
|
8
|
+
* FieldSelect, FieldCheckbox, FieldTextarea, FieldFile, FieldObject,
|
|
9
|
+
* FieldAssets, FieldInputLocalized, FieldKeyValues, FieldLikeButton,
|
|
10
|
+
* FieldRadios, FieldSwitch) satisfies this interface via `export function`.
|
|
11
|
+
*/
|
|
12
|
+
export interface ValidatableField {
|
|
13
|
+
/** Run the validator now. Renders the inline error if invalid. */
|
|
14
|
+
validate(): ValidationResult | undefined;
|
|
15
|
+
/** Reset the inline error and clear `el.setCustomValidity`. */
|
|
16
|
+
clearValidation?(): void;
|
|
17
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
18
|
+
getValidation?(): ValidationResult | undefined;
|
|
19
|
+
/** Focus the visible interactive element. */
|
|
20
|
+
focus?(): void;
|
|
21
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
22
|
+
scrollIntoView?(opts?: ScrollIntoViewOptions): void;
|
|
23
|
+
}
|
|
24
|
+
type FieldArg = ValidatableField | undefined | null;
|
|
25
|
+
/**
|
|
26
|
+
* Run `validate()` on every provided field. Returns `true` if all are valid.
|
|
27
|
+
*
|
|
28
|
+
* `undefined` / `null` entries are skipped so callers can spread conditional
|
|
29
|
+
* refs without filtering first.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```svelte
|
|
33
|
+
* <script>
|
|
34
|
+
* let nameField = $state();
|
|
35
|
+
* let emailField = $state();
|
|
36
|
+
*
|
|
37
|
+
* function handleSubmit() {
|
|
38
|
+
* if (!validateAllFields([nameField, emailField])) {
|
|
39
|
+
* scrollToFirstInvalidField([nameField, emailField]);
|
|
40
|
+
* return;
|
|
41
|
+
* }
|
|
42
|
+
* // ...submit
|
|
43
|
+
* }
|
|
44
|
+
* </script>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateAllFields(fields: FieldArg[]): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Return the first field whose current validation state is invalid, or
|
|
50
|
+
* `undefined` if all are valid (or never validated).
|
|
51
|
+
*
|
|
52
|
+
* Reads cached state via `getValidation()` — call `validateAllFields()` first
|
|
53
|
+
* if you need fresh results.
|
|
54
|
+
*/
|
|
55
|
+
export declare function findFirstInvalidField(fields: FieldArg[]): ValidatableField | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Scroll the first invalid field into view and (by default) focus it.
|
|
58
|
+
* Returns `true` if a field was scrolled, `false` if all were valid.
|
|
59
|
+
*
|
|
60
|
+
* Call **after** `validateAllFields()` — this reads cached validation state.
|
|
61
|
+
*
|
|
62
|
+
* @param fields - Field refs (in display order — first match wins)
|
|
63
|
+
* @param opts.focus - Whether to also call `focus()` on the field. Default true.
|
|
64
|
+
* @param opts.behavior - ScrollIntoView behavior. Default `"smooth"`.
|
|
65
|
+
* @param opts.block - ScrollIntoView block alignment. Default `"center"`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function scrollToFirstInvalidField(fields: FieldArg[], opts?: {
|
|
68
|
+
focus?: boolean;
|
|
69
|
+
behavior?: ScrollBehavior;
|
|
70
|
+
block?: ScrollLogicalPosition;
|
|
71
|
+
}): boolean;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `validate()` on every provided field. Returns `true` if all are valid.
|
|
3
|
+
*
|
|
4
|
+
* `undefined` / `null` entries are skipped so callers can spread conditional
|
|
5
|
+
* refs without filtering first.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* let nameField = $state();
|
|
11
|
+
* let emailField = $state();
|
|
12
|
+
*
|
|
13
|
+
* function handleSubmit() {
|
|
14
|
+
* if (!validateAllFields([nameField, emailField])) {
|
|
15
|
+
* scrollToFirstInvalidField([nameField, emailField]);
|
|
16
|
+
* return;
|
|
17
|
+
* }
|
|
18
|
+
* // ...submit
|
|
19
|
+
* }
|
|
20
|
+
* </script>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function validateAllFields(fields) {
|
|
24
|
+
let allValid = true;
|
|
25
|
+
for (const f of fields) {
|
|
26
|
+
if (!f)
|
|
27
|
+
continue;
|
|
28
|
+
const res = f.validate();
|
|
29
|
+
if (res && !res.valid)
|
|
30
|
+
allValid = false;
|
|
31
|
+
}
|
|
32
|
+
return allValid;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Return the first field whose current validation state is invalid, or
|
|
36
|
+
* `undefined` if all are valid (or never validated).
|
|
37
|
+
*
|
|
38
|
+
* Reads cached state via `getValidation()` — call `validateAllFields()` first
|
|
39
|
+
* if you need fresh results.
|
|
40
|
+
*/
|
|
41
|
+
export function findFirstInvalidField(fields) {
|
|
42
|
+
for (const f of fields) {
|
|
43
|
+
if (!f)
|
|
44
|
+
continue;
|
|
45
|
+
const v = f.getValidation?.();
|
|
46
|
+
if (v && !v.valid)
|
|
47
|
+
return f;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Scroll the first invalid field into view and (by default) focus it.
|
|
53
|
+
* Returns `true` if a field was scrolled, `false` if all were valid.
|
|
54
|
+
*
|
|
55
|
+
* Call **after** `validateAllFields()` — this reads cached validation state.
|
|
56
|
+
*
|
|
57
|
+
* @param fields - Field refs (in display order — first match wins)
|
|
58
|
+
* @param opts.focus - Whether to also call `focus()` on the field. Default true.
|
|
59
|
+
* @param opts.behavior - ScrollIntoView behavior. Default `"smooth"`.
|
|
60
|
+
* @param opts.block - ScrollIntoView block alignment. Default `"center"`.
|
|
61
|
+
*/
|
|
62
|
+
export function scrollToFirstInvalidField(fields, opts) {
|
|
63
|
+
const field = findFirstInvalidField(fields);
|
|
64
|
+
if (!field)
|
|
65
|
+
return false;
|
|
66
|
+
field.scrollIntoView?.({
|
|
67
|
+
behavior: opts?.behavior ?? "smooth",
|
|
68
|
+
block: opts?.block ?? "center",
|
|
69
|
+
});
|
|
70
|
+
if (opts?.focus !== false)
|
|
71
|
+
field.focus?.();
|
|
72
|
+
return true;
|
|
73
|
+
}
|
package/docs/domains/actions.md
CHANGED
|
@@ -64,6 +64,80 @@ Actions using `$effect()` accept a function returning options:
|
|
|
64
64
|
/>
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
### Imperative validate() trigger
|
|
68
|
+
|
|
69
|
+
The `validate` action only runs `_doValidate` in response to user-driven DOM
|
|
70
|
+
events (`change`, first `blur`). On a pristine, never-touched field the inline
|
|
71
|
+
validation message never mounts — which silently breaks any flow that
|
|
72
|
+
pre-populates errors via `customValidator` on a fresh form.
|
|
73
|
+
|
|
74
|
+
Pass a `setDoValidate` callback to capture a reference to the action's internal
|
|
75
|
+
validator function and trigger it imperatively (e.g., from a submit handler):
|
|
76
|
+
|
|
77
|
+
```svelte
|
|
78
|
+
<script>
|
|
79
|
+
let doValidate: (() => void) | undefined = $state();
|
|
80
|
+
|
|
81
|
+
function handleSubmit() {
|
|
82
|
+
// Force every "sleeping" field's validator to run, rendering inline
|
|
83
|
+
// errors even on fields the user never touched.
|
|
84
|
+
doValidate?.();
|
|
85
|
+
// ...check validationResult.valid here, or use validateAllFields().
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<input
|
|
90
|
+
required
|
|
91
|
+
use:validate={() => ({
|
|
92
|
+
enabled: true,
|
|
93
|
+
setValidationResult: (res) => (validationResult = res),
|
|
94
|
+
setDoValidate: (fn) => (doValidate = fn),
|
|
95
|
+
})}
|
|
96
|
+
/>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> **You generally don't write this by hand.** Every STUIC `Field*` component
|
|
100
|
+
> already wires `setDoValidate` internally and exposes the result as
|
|
101
|
+
> `export function validate()` on its component reference. See the
|
|
102
|
+
> [Components domain doc](./components.md#imperative-validate-api) for the
|
|
103
|
+
> per-field method list and the [validate-fields utility](./utils.md) for
|
|
104
|
+
> `validateAllFields()` / `scrollToFirstInvalidField()` aggregators.
|
|
105
|
+
|
|
106
|
+
### Pristine forms and external errors
|
|
107
|
+
|
|
108
|
+
A common trap: setting an `errors` prop on a brand-new form and expecting the
|
|
109
|
+
inline messages to render. They won't — the `validate` action's
|
|
110
|
+
`customValidator` is only invoked on user events. Two fixes:
|
|
111
|
+
|
|
112
|
+
1. **Wrap the form in `<form use:onSubmitValidityCheck>`** and listen for
|
|
113
|
+
`submit_valid` / `submit_invalid` (works for native submit flows).
|
|
114
|
+
2. **Call the field component's imperative `validate()` from your submit
|
|
115
|
+
handler** (works for any flow — wizards, multi-step, custom CTAs).
|
|
116
|
+
|
|
117
|
+
### Hidden inputs and `required`
|
|
118
|
+
|
|
119
|
+
Per the HTML spec, `<input type="hidden">` is *barred from constraint
|
|
120
|
+
validation* — `validity.valueMissing` stays `false` regardless of the
|
|
121
|
+
`required` attribute, and native browser submit blocking is skipped. Several
|
|
122
|
+
STUIC field components (`FieldPhoneNumber`, `FieldCountry`, `FieldObject`,
|
|
123
|
+
`FieldAssets`, `FieldInputLocalized`, `FieldKeyValues`, `FieldLikeButton`)
|
|
124
|
+
use a hidden input to participate in `FormData`, so they each enforce
|
|
125
|
+
`required` themselves inside their `customValidator`:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
customValidator(val, ctx, el) {
|
|
129
|
+
if (required && (val == null || val === "")) {
|
|
130
|
+
return "This field requires attention. Please review and try again.";
|
|
131
|
+
}
|
|
132
|
+
return userValidator?.(val, ctx, el) || "";
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This means `<FieldCountry required />` and `<FieldPhoneNumber required />`
|
|
137
|
+
correctly fail validation when empty — both through the imperative
|
|
138
|
+
`validate()` path and via `use:onSubmitValidityCheck`. Without this wrap
|
|
139
|
+
they'd silently accept empty values.
|
|
140
|
+
|
|
67
141
|
### File Dropzone
|
|
68
142
|
|
|
69
143
|
```svelte
|
|
@@ -102,6 +102,137 @@
|
|
|
102
102
|
|
|
103
103
|
---
|
|
104
104
|
|
|
105
|
+
## Imperative validate API
|
|
106
|
+
|
|
107
|
+
Every `Field*` component that uses the `validate` action exposes a small
|
|
108
|
+
imperative API on its component reference, accessed via `bind:this`. Form
|
|
109
|
+
components (`LoginForm`, `RegisterForm`, `LoginOrRegisterForm`,
|
|
110
|
+
`EmailVerifyForm`) compose those into form-level `validate()` /
|
|
111
|
+
`scrollToFirstError()`.
|
|
112
|
+
|
|
113
|
+
### Why
|
|
114
|
+
|
|
115
|
+
The `validate` action only runs on user-driven DOM events (`change`, first
|
|
116
|
+
`blur`). On a pristine, never-touched field the inline `validation-box` never
|
|
117
|
+
mounts — which silently breaks any flow that pre-populates errors via
|
|
118
|
+
`customValidator` on a fresh form. The imperative API lets a submit handler
|
|
119
|
+
force every "sleeping" field's validator to run, rendering inline errors all
|
|
120
|
+
at once — no synthetic `change` events, no DOM lookups, no id-format coupling.
|
|
121
|
+
|
|
122
|
+
### The `validate` prop — defaults & opt-out
|
|
123
|
+
|
|
124
|
+
All field components that use the `validate` action treat the prop with one
|
|
125
|
+
consistent rule:
|
|
126
|
+
|
|
127
|
+
| `validate` value | Action |
|
|
128
|
+
| ------------------------- | ---------------------------------------------------------- |
|
|
129
|
+
| (omitted) / `undefined` | **Enabled** with default options (the common case) |
|
|
130
|
+
| `true` | Enabled with default options (explicit; same as omitting) |
|
|
131
|
+
| `false` | **Disabled** — no validation, `validate()` becomes a no-op |
|
|
132
|
+
| `{ customValidator, ... }`| Enabled, with `ValidateOptions` overrides applied |
|
|
133
|
+
|
|
134
|
+
So `<FieldInput required />` works as expected — required is enforced, and a
|
|
135
|
+
failed `validate()` (imperative or event-driven) renders the inline error.
|
|
136
|
+
Use `validate={false}` to bypass stuic's validation entirely.
|
|
137
|
+
|
|
138
|
+
> **Why default-on?** Hidden-input field components (`FieldPhoneNumber`,
|
|
139
|
+
> `FieldCountry`, `FieldObject`, `FieldAssets`, `FieldInputLocalized`,
|
|
140
|
+
> `FieldKeyValues`, `FieldLikeButton`) *must* be default-on because hidden
|
|
141
|
+
> inputs are excluded from native browser constraint validation — without the
|
|
142
|
+
> stuic action enforcing `required` in a `customValidator`, the attribute is a
|
|
143
|
+
> silent no-op. Plain-input field components were harmonized to the same
|
|
144
|
+
> default to keep the rule uniform: "`required` means required."
|
|
145
|
+
|
|
146
|
+
### Per-field methods
|
|
147
|
+
|
|
148
|
+
Available on `FieldInput`, `FieldTextarea`, `FieldCheckbox`, `FieldSelect`,
|
|
149
|
+
`FieldFile`, `FieldObject`, `FieldAssets`, `FieldInputLocalized`,
|
|
150
|
+
`FieldKeyValues`, `FieldPhoneNumber`, `FieldCountry`, `FieldLikeButton`,
|
|
151
|
+
`FieldRadios`, `FieldSwitch`, `FieldOptions`, and `Switch`:
|
|
152
|
+
|
|
153
|
+
| Method | Returns | Purpose |
|
|
154
|
+
| --------------------------------------------- | -------------------------------- | ------------------------------------------------------------------ |
|
|
155
|
+
| `validate()` | `ValidationResult \| undefined` | Run the validator now. Renders the inline message if invalid. |
|
|
156
|
+
| `clearValidation()` | `void` | Clear the inline message and `setCustomValidity`. |
|
|
157
|
+
| `getValidation()` | `ValidationResult \| undefined` | Read cached state (no re-run). |
|
|
158
|
+
| `focus()` | `void` | Focus the visible interactive element. |
|
|
159
|
+
| `scrollIntoView(opts?)` | `void` | Scroll the field into view. Defaults to `smooth` + `center`. |
|
|
160
|
+
|
|
161
|
+
```svelte
|
|
162
|
+
<script>
|
|
163
|
+
let nameField = $state<FieldInput>();
|
|
164
|
+
|
|
165
|
+
function checkName() {
|
|
166
|
+
const result = nameField?.validate();
|
|
167
|
+
if (result && !result.valid) {
|
|
168
|
+
console.warn("Name invalid:", result.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<FieldInput bind:this={nameField} bind:value={name} label="Name" required />
|
|
174
|
+
<Button onclick={checkName}>Check now</Button>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Form-level methods
|
|
178
|
+
|
|
179
|
+
`LoginForm`, `RegisterForm`, `LoginOrRegisterForm`, and `EmailVerifyForm` each
|
|
180
|
+
expose:
|
|
181
|
+
|
|
182
|
+
| Method | Returns | Purpose |
|
|
183
|
+
| ----------------------------------- | --------- | -------------------------------------------------------------------- |
|
|
184
|
+
| `validate()` | `boolean` | Run every inner field's validator. `true` if all valid. |
|
|
185
|
+
| `scrollToFirstError(opts?)` | `boolean` | Scroll the first invalid field into view + focus it. Call after `validate()`. |
|
|
186
|
+
|
|
187
|
+
```svelte
|
|
188
|
+
<script>
|
|
189
|
+
let loginForm = $state<LoginForm>();
|
|
190
|
+
|
|
191
|
+
async function handleCustomSubmit() {
|
|
192
|
+
if (!loginForm?.validate()) {
|
|
193
|
+
loginForm?.scrollToFirstError();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
await api.login(/* ... */);
|
|
197
|
+
}
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<LoginForm bind:this={loginForm} onSubmit={handleCustomSubmit} />
|
|
201
|
+
<Button onclick={handleCustomSubmit}>Submit from outside</Button>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Pristine-form errors pattern
|
|
205
|
+
|
|
206
|
+
The trap: an external `errors` prop wired into each field's `customValidator`
|
|
207
|
+
won't render until the user touches the field. Pair the existing prop with
|
|
208
|
+
an imperative `validate()` call from your submit handler:
|
|
209
|
+
|
|
210
|
+
```svelte
|
|
211
|
+
<FieldInput
|
|
212
|
+
bind:this={nameField}
|
|
213
|
+
bind:value={address.name}
|
|
214
|
+
required
|
|
215
|
+
validate={{
|
|
216
|
+
customValidator() {
|
|
217
|
+
return externalErrors.find((e) => e.field === "name")?.message || "";
|
|
218
|
+
},
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
|
|
222
|
+
<!-- in your submit handler -->
|
|
223
|
+
<script>
|
|
224
|
+
function handleContinue() {
|
|
225
|
+
if (!validateAllFields([nameField, /* ... */])) return;
|
|
226
|
+
// ...submit
|
|
227
|
+
}
|
|
228
|
+
</script>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
For aggregation across many fields, see
|
|
232
|
+
[validate-fields utilities](./utils.md#field-validation-aggregators).
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
105
236
|
## LoginForm
|
|
106
237
|
|
|
107
238
|
Standalone login form with optional modal variant. Supports social logins, forgot password, remember me, client+server validation, i18n, and notifications integration.
|
package/docs/domains/utils.md
CHANGED
|
@@ -173,6 +173,43 @@ storage.get("user"); // { name: 'John' }
|
|
|
173
173
|
| `generateCssTokens` | Convert token schema to CSS |
|
|
174
174
|
| `toCssString` | Format tokens as CSS string |
|
|
175
175
|
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Field Validation Aggregators
|
|
179
|
+
|
|
180
|
+
Helpers for orchestrating `validate()` across multiple `Field*` components.
|
|
181
|
+
Pair with the per-field imperative API documented in the
|
|
182
|
+
[Components domain](./components.md#imperative-validate-api).
|
|
183
|
+
|
|
184
|
+
| Util | Purpose |
|
|
185
|
+
| ----------------------------- | ------------------------------------------------------------ |
|
|
186
|
+
| `validateAllFields` | Run `validate()` on every provided field ref. Returns `true` if all valid. |
|
|
187
|
+
| `findFirstInvalidField` | Return the first ref whose cached `getValidation()` is invalid. |
|
|
188
|
+
| `scrollToFirstInvalidField` | Scroll the first invalid field into view + focus (call after `validateAllFields`). |
|
|
189
|
+
| `ValidatableField` (interface)| Minimal shape every STUIC `Field*` satisfies — your own components can too. |
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import {
|
|
193
|
+
scrollToFirstInvalidField,
|
|
194
|
+
validateAllFields,
|
|
195
|
+
} from "@marianmeres/stuic";
|
|
196
|
+
|
|
197
|
+
let nameField = $state<FieldInput>();
|
|
198
|
+
let emailField = $state<FieldInput>();
|
|
199
|
+
let countryField = $state<FieldCountry>();
|
|
200
|
+
|
|
201
|
+
function handleContinue() {
|
|
202
|
+
const allValid = validateAllFields([nameField, emailField, countryField]);
|
|
203
|
+
if (!allValid) {
|
|
204
|
+
scrollToFirstInvalidField([nameField, emailField, countryField]);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// ...submit
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`undefined` / `null` entries are skipped so callers can spread conditional refs
|
|
212
|
+
without filtering first.
|
|
176
213
|
|
|
177
214
|
---
|
|
178
215
|
|
|
@@ -184,4 +221,5 @@ storage.get("user"); // { name: 'John' }
|
|
|
184
221
|
| src/lib/utils/tw-merge.ts | Critical for class merging |
|
|
185
222
|
| src/lib/utils/persistent-state.svelte.ts | Reactive storage pattern (runes-based) |
|
|
186
223
|
| src/lib/utils/storage-abstraction.ts | Non-reactive storage (localStorage, etc.) |
|
|
224
|
+
| src/lib/utils/validate-fields.ts | Form-level validation aggregators |
|
|
187
225
|
| src/lib/utils/design-tokens.ts | Re-exports from `@marianmeres/design-tokens` |
|