@marianmeres/stuic 3.76.2 → 3.76.4
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/on-submit-validity-check.svelte.js +40 -3
- package/dist/actions/validate.svelte.js +21 -0
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +13 -1
- package/dist/components/LoginForm/LoginForm.svelte +53 -22
- package/dist/components/LoginForm/LoginForm.svelte.d.ts +1 -1
- package/dist/components/RegisterForm/RegisterForm.svelte +13 -3
- package/package.json +1 -1
|
@@ -47,6 +47,12 @@ export function onSubmitValidityCheck(node) {
|
|
|
47
47
|
e.preventDefault();
|
|
48
48
|
// this will disable all other onsubmit listeners...
|
|
49
49
|
e.stopImmediatePropagation();
|
|
50
|
+
// // [debug] kept commented for the next time Issue A regresses
|
|
51
|
+
// // eslint-disable-next-line no-console
|
|
52
|
+
// console.log(
|
|
53
|
+
// "[onSubmitValidityCheck] submit intercepted. element count:",
|
|
54
|
+
// node.elements?.length
|
|
55
|
+
// );
|
|
50
56
|
const invalid = [];
|
|
51
57
|
for (let i = 0; i < node.elements?.length; i++) {
|
|
52
58
|
const el = node.elements[i];
|
|
@@ -55,6 +61,15 @@ export function onSubmitValidityCheck(node) {
|
|
|
55
61
|
// input (last radio input), which is not desired
|
|
56
62
|
if (el.type === "radio")
|
|
57
63
|
continue;
|
|
64
|
+
// // [debug] kept commented for the next time Issue A regresses
|
|
65
|
+
// // eslint-disable-next-line no-console
|
|
66
|
+
// console.log(`[onSubmitValidityCheck] el#${i} ${el.name || el.type} BEFORE`, {
|
|
67
|
+
// value: el.value,
|
|
68
|
+
// valid: el.validity.valid,
|
|
69
|
+
// customError: el.validity.customError,
|
|
70
|
+
// valueMissing: el.validity.valueMissing,
|
|
71
|
+
// validationMessage: el.validationMessage,
|
|
72
|
+
// });
|
|
58
73
|
// Clear any stale `customError` flag from a prior submit attempt before
|
|
59
74
|
// re-dispatching the validate listeners. Without this, if the field's
|
|
60
75
|
// per-field validate $effect was torn down/re-mounted in a way that
|
|
@@ -69,9 +84,15 @@ export function onSubmitValidityCheck(node) {
|
|
|
69
84
|
el.setCustomValidity("");
|
|
70
85
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
71
86
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
87
|
+
// // [debug] kept commented for the next time Issue A regresses
|
|
88
|
+
// // eslint-disable-next-line no-console
|
|
89
|
+
// console.log(`[onSubmitValidityCheck] el#${i} ${el.name || el.type} AFTER `, {
|
|
90
|
+
// value: el.value,
|
|
91
|
+
// valid: el.validity.valid,
|
|
92
|
+
// customError: el.validity.customError,
|
|
93
|
+
// valueMissing: el.validity.valueMissing,
|
|
94
|
+
// validationMessage: el.validationMessage,
|
|
95
|
+
// });
|
|
75
96
|
if (!el.validity.valid) {
|
|
76
97
|
invalid.push(el);
|
|
77
98
|
}
|
|
@@ -79,12 +100,28 @@ export function onSubmitValidityCheck(node) {
|
|
|
79
100
|
}
|
|
80
101
|
// none invalid
|
|
81
102
|
if (!invalid.length) {
|
|
103
|
+
// // [debug] kept commented for the next time Issue A regresses
|
|
104
|
+
// // eslint-disable-next-line no-console
|
|
105
|
+
// console.log("[onSubmitValidityCheck] → dispatching submit_valid");
|
|
82
106
|
node.dispatchEvent(new CustomEvent(SUBMIT_VALID_EVENT_NAME, {
|
|
83
107
|
bubbles: true,
|
|
84
108
|
detail: { formData: new FormData(node) },
|
|
85
109
|
}));
|
|
86
110
|
}
|
|
87
111
|
else {
|
|
112
|
+
// // [debug] kept commented for the next time Issue A regresses
|
|
113
|
+
// // eslint-disable-next-line no-console
|
|
114
|
+
// console.warn(
|
|
115
|
+
// "[onSubmitValidityCheck] → dispatching submit_invalid; invalid =",
|
|
116
|
+
// invalid.map((el) => ({
|
|
117
|
+
// name: el.name,
|
|
118
|
+
// type: el.type,
|
|
119
|
+
// value: el.value,
|
|
120
|
+
// customError: el.validity.customError,
|
|
121
|
+
// valueMissing: el.validity.valueMissing,
|
|
122
|
+
// validationMessage: el.validationMessage,
|
|
123
|
+
// }))
|
|
124
|
+
// );
|
|
88
125
|
node.dispatchEvent(new CustomEvent(SUBMIT_INVALID_EVENT_NAME, {
|
|
89
126
|
bubbles: true,
|
|
90
127
|
detail: { invalid },
|
|
@@ -154,6 +154,14 @@ export function validate(el, fn) {
|
|
|
154
154
|
}
|
|
155
155
|
return fallback;
|
|
156
156
|
};
|
|
157
|
+
// // [debug] kept commented for the next time Issue A regresses
|
|
158
|
+
// // eslint-disable-next-line no-console
|
|
159
|
+
// console.log(
|
|
160
|
+
// `[validate $effect] (re)mount listener on <${el.tagName.toLowerCase()} name="${
|
|
161
|
+
// (el as HTMLInputElement).name || ""
|
|
162
|
+
// }">`,
|
|
163
|
+
// { enabled, on, hasCustomValidator: typeof customValidator === "function" }
|
|
164
|
+
// );
|
|
157
165
|
const _doValidate = () => {
|
|
158
166
|
if (!enabled)
|
|
159
167
|
return;
|
|
@@ -172,6 +180,19 @@ export function validate(el, fn) {
|
|
|
172
180
|
// being removed across re-renders would leave the previous message
|
|
173
181
|
// stuck on the element forever.
|
|
174
182
|
el.setCustomValidity(customValidatorMessage);
|
|
183
|
+
// // [debug] kept commented for the next time Issue A regresses
|
|
184
|
+
// // eslint-disable-next-line no-console
|
|
185
|
+
// console.log(
|
|
186
|
+
// `[validate _doValidate] ran on <${el.tagName.toLowerCase()} name="${
|
|
187
|
+
// (el as HTMLInputElement).name || ""
|
|
188
|
+
// }">`,
|
|
189
|
+
// {
|
|
190
|
+
// value: el.value,
|
|
191
|
+
// customValidatorMessage,
|
|
192
|
+
// customError: el.validity.customError,
|
|
193
|
+
// valid: el.validity.valid,
|
|
194
|
+
// }
|
|
195
|
+
// );
|
|
175
196
|
// this triggers the bubble, which is not what we want
|
|
176
197
|
// el.reportValidity();
|
|
177
198
|
const validityState = el.validity;
|
|
@@ -89,6 +89,18 @@
|
|
|
89
89
|
|
|
90
90
|
let t = $derived(tProp ?? t_default);
|
|
91
91
|
|
|
92
|
+
// Mirror the form ref into local $state so it survives prop re-application
|
|
93
|
+
// when the parent re-renders without binding `el`. See LoginForm for the full
|
|
94
|
+
// rationale — same Svelte 5 `$bindable` + `bind:this` gotcha applies here.
|
|
95
|
+
// EmailVerifyForm uses `onsubmit={handleFormSubmit}` (declarative attribute)
|
|
96
|
+
// so the listener wouldn't get torn down the way LoginForm/RegisterForm did,
|
|
97
|
+
// but the public `el` prop is still unsafe for consumers that bind it after
|
|
98
|
+
// any parent re-render — same fix for consistency.
|
|
99
|
+
let formEl = $state<HTMLFormElement | undefined>();
|
|
100
|
+
$effect(() => {
|
|
101
|
+
el = formEl;
|
|
102
|
+
});
|
|
103
|
+
|
|
92
104
|
let code = $state("");
|
|
93
105
|
let cooldownRemaining = $state(0);
|
|
94
106
|
let resentFlash = $state(false);
|
|
@@ -182,7 +194,7 @@
|
|
|
182
194
|
let submitDisabled = $derived(code.length !== codeLength || isSubmitting);
|
|
183
195
|
</script>
|
|
184
196
|
|
|
185
|
-
<form bind:this={
|
|
197
|
+
<form bind:this={formEl} class={_class} onsubmit={handleFormSubmit} {...rest}>
|
|
186
198
|
<!-- Heading -->
|
|
187
199
|
<H level={2} class={unstyled ? undefined : "stuic-email-verify-form-heading"}>
|
|
188
200
|
{t("email_verify_form.heading")}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import type { HTMLAttributes } from "svelte/elements";
|
|
4
4
|
import type { TranslateFn } from "../../types.js";
|
|
5
|
+
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
5
6
|
import type {
|
|
6
7
|
LoginFormData,
|
|
7
8
|
LoginFormValidationError,
|
|
8
9
|
} from "./_internal/login-form-types.js";
|
|
9
|
-
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
10
10
|
|
|
11
11
|
export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
|
|
12
12
|
/** Bindable login data. Default: createEmptyLoginFormData() */
|
|
@@ -78,18 +78,18 @@
|
|
|
78
78
|
|
|
79
79
|
<script lang="ts">
|
|
80
80
|
import { untrack } from "svelte";
|
|
81
|
+
import { onSubmitValidityCheck } from "../../actions/on-submit-validity-check.svelte.js";
|
|
82
|
+
import { tooltip } from "../../actions/tooltip/tooltip.svelte.js";
|
|
81
83
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
84
|
+
import Button from "../Button/Button.svelte";
|
|
85
|
+
import DismissibleMessage from "../DismissibleMessage/DismissibleMessage.svelte";
|
|
86
|
+
import FieldCheckbox from "../Input/FieldCheckbox.svelte";
|
|
87
|
+
import FieldInput from "../Input/FieldInput.svelte";
|
|
82
88
|
import { t_default } from "./_internal/login-form-i18n-defaults.js";
|
|
83
89
|
import {
|
|
84
90
|
createEmptyLoginFormData,
|
|
85
91
|
validateLoginForm,
|
|
86
92
|
} from "./_internal/login-form-utils.js";
|
|
87
|
-
import { tooltip } from "../../actions/tooltip/tooltip.svelte.js";
|
|
88
|
-
import Button from "../Button/Button.svelte";
|
|
89
|
-
import DismissibleMessage from "../DismissibleMessage/DismissibleMessage.svelte";
|
|
90
|
-
import FieldCheckbox from "../Input/FieldCheckbox.svelte";
|
|
91
|
-
import FieldInput from "../Input/FieldInput.svelte";
|
|
92
|
-
import { onSubmitValidityCheck } from "../../actions/on-submit-validity-check.svelte.js";
|
|
93
93
|
|
|
94
94
|
let {
|
|
95
95
|
formData = $bindable(createEmptyLoginFormData()),
|
|
@@ -115,6 +115,19 @@
|
|
|
115
115
|
|
|
116
116
|
let t = $derived(tProp ?? t_default);
|
|
117
117
|
|
|
118
|
+
// Mirror the form ref into local $state so it survives prop re-application
|
|
119
|
+
// when the parent re-renders without binding `el`. Otherwise the bindable
|
|
120
|
+
// prop reverts to its default (undefined) on the next parent re-render, the
|
|
121
|
+
// `$effect` tracking it cleans up the `submit_valid` listener, and `bind:this`
|
|
122
|
+
// does NOT re-fire (the form element wasn't unmounted) — leaving the form
|
|
123
|
+
// alive in the DOM with no submit handler. Next click silently goes nowhere.
|
|
124
|
+
// `el` stays the public API: consumers can still bind it; we mirror formEl
|
|
125
|
+
// into it so the binding sees a value.
|
|
126
|
+
let formEl = $state<HTMLFormElement | undefined>();
|
|
127
|
+
$effect(() => {
|
|
128
|
+
el = formEl;
|
|
129
|
+
});
|
|
130
|
+
|
|
118
131
|
// Internal validation errors (set on submit)
|
|
119
132
|
let internalErrors = $state<LoginFormValidationError[]>([]);
|
|
120
133
|
|
|
@@ -131,15 +144,22 @@
|
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
function handleSubmitValid() {
|
|
147
|
+
// // [debug] kept commented for the next time Issue A regresses
|
|
148
|
+
// // eslint-disable-next-line no-console
|
|
149
|
+
// console.log("[LoginForm handleSubmitValid] entered", {
|
|
150
|
+
// formData: { ...formData },
|
|
151
|
+
// internalErrors: $state.snapshot(internalErrors),
|
|
152
|
+
// externalErrors: [...externalErrors],
|
|
153
|
+
// error,
|
|
154
|
+
// });
|
|
155
|
+
|
|
134
156
|
// Defensively clear any stale customValidity left on form fields by a prior
|
|
135
|
-
// validation pass. The
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (el) {
|
|
142
|
-
for (const node of Array.from(el.elements) as HTMLInputElement[]) {
|
|
157
|
+
// validation pass. The canonical fix lives in `onSubmitValidityCheck` (which
|
|
158
|
+
// pre-clears before the per-field re-dispatch), but doing it here too is
|
|
159
|
+
// cheap insurance against any future regression that lets a stale flag slip
|
|
160
|
+
// past the action.
|
|
161
|
+
if (formEl) {
|
|
162
|
+
for (const node of Array.from(formEl.elements) as HTMLInputElement[]) {
|
|
143
163
|
if (typeof node.setCustomValidity === "function") node.setCustomValidity("");
|
|
144
164
|
}
|
|
145
165
|
}
|
|
@@ -147,6 +167,15 @@
|
|
|
147
167
|
const validationErrors = validateLoginForm(formData, t);
|
|
148
168
|
internalErrors = validationErrors;
|
|
149
169
|
|
|
170
|
+
// // [debug] kept commented for the next time Issue A regresses
|
|
171
|
+
// // eslint-disable-next-line no-console
|
|
172
|
+
// console.log("[LoginForm handleSubmitValid] post-validate", {
|
|
173
|
+
// validationErrors,
|
|
174
|
+
// externalErrorsLen: externalErrors.length,
|
|
175
|
+
// willCallOnSubmit:
|
|
176
|
+
// validationErrors.length === 0 && externalErrors.length === 0,
|
|
177
|
+
// });
|
|
178
|
+
|
|
150
179
|
if (validationErrors.length === 0 && externalErrors.length === 0) {
|
|
151
180
|
onSubmit(formData);
|
|
152
181
|
}
|
|
@@ -172,9 +201,11 @@
|
|
|
172
201
|
|
|
173
202
|
// The onSubmitValidityCheck action intercepts native submit (capture phase,
|
|
174
203
|
// stopImmediatePropagation) and dispatches a custom "submit_valid" event.
|
|
175
|
-
// Listen for it on the form element as a fallback.
|
|
204
|
+
// Listen for it on the form element as a fallback. Reads `formEl` (local state)
|
|
205
|
+
// — NOT the `el` prop, which can revert to undefined on parent re-render and
|
|
206
|
+
// would cause this $effect's cleanup to silently detach the listener.
|
|
176
207
|
$effect(() => {
|
|
177
|
-
const node =
|
|
208
|
+
const node = formEl;
|
|
178
209
|
if (!node) return;
|
|
179
210
|
node.addEventListener("submit_valid", handleSubmitValid);
|
|
180
211
|
return () => node.removeEventListener("submit_valid", handleSubmitValid);
|
|
@@ -183,15 +214,15 @@
|
|
|
183
214
|
let _class = $derived(unstyled ? classProp : twMerge("stuic-login-form", classProp));
|
|
184
215
|
</script>
|
|
185
216
|
|
|
186
|
-
<form bind:this={
|
|
217
|
+
<form bind:this={formEl} class={_class} use:onSubmitValidityCheck {...rest}>
|
|
187
218
|
<!-- General error alert -->
|
|
188
219
|
<DismissibleMessage message={error} intent="destructive" />
|
|
189
220
|
|
|
190
221
|
<!--
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
222
|
+
svelte-ignore binding_property_non_reactive:
|
|
223
|
+
formData is a $bindable prop — deep reactivity depends on the consumer
|
|
224
|
+
passing a $state() object. The bindings work correctly regardless.
|
|
225
|
+
-->
|
|
195
226
|
<!-- Email -->
|
|
196
227
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
197
228
|
<FieldInput
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
import type { HTMLAttributes } from "svelte/elements";
|
|
3
3
|
import type { TranslateFn } from "../../types.js";
|
|
4
|
-
import type { LoginFormData, LoginFormValidationError } from "./_internal/login-form-types.js";
|
|
5
4
|
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
5
|
+
import type { LoginFormData, LoginFormValidationError } from "./_internal/login-form-types.js";
|
|
6
6
|
export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
|
|
7
7
|
/** Bindable login data. Default: createEmptyLoginFormData() */
|
|
8
8
|
formData?: LoginFormData;
|
|
@@ -132,6 +132,14 @@
|
|
|
132
132
|
|
|
133
133
|
let t = $derived(tProp ?? t_default);
|
|
134
134
|
|
|
135
|
+
// Mirror the form ref into local $state so it survives prop re-application
|
|
136
|
+
// when the parent re-renders without binding `el`. See LoginForm for the full
|
|
137
|
+
// rationale — same Svelte 5 `$bindable` + `bind:this` gotcha applies here.
|
|
138
|
+
let formEl = $state<HTMLFormElement | undefined>();
|
|
139
|
+
$effect(() => {
|
|
140
|
+
el = formEl;
|
|
141
|
+
});
|
|
142
|
+
|
|
135
143
|
let topFields = $derived(extraFields.filter((f) => f.position === "top"));
|
|
136
144
|
let bottomFields = $derived(extraFields.filter((f) => f.position !== "top"));
|
|
137
145
|
|
|
@@ -180,9 +188,11 @@
|
|
|
180
188
|
|
|
181
189
|
// The onSubmitValidityCheck action intercepts native submit (capture phase,
|
|
182
190
|
// stopImmediatePropagation) and dispatches a custom "submit_valid" event.
|
|
183
|
-
// Listen for it on the form element as a fallback.
|
|
191
|
+
// Listen for it on the form element as a fallback. Reads `formEl` (local state)
|
|
192
|
+
// — NOT the `el` prop, which can revert to undefined on parent re-render and
|
|
193
|
+
// would cause this $effect's cleanup to silently detach the listener.
|
|
184
194
|
$effect(() => {
|
|
185
|
-
const node =
|
|
195
|
+
const node = formEl;
|
|
186
196
|
if (!node) return;
|
|
187
197
|
node.addEventListener("submit_valid", handleSubmitValid);
|
|
188
198
|
return () => node.removeEventListener("submit_valid", handleSubmitValid);
|
|
@@ -191,7 +201,7 @@
|
|
|
191
201
|
let _class = $derived(unstyled ? classProp : twMerge("stuic-register-form", classProp));
|
|
192
202
|
</script>
|
|
193
203
|
|
|
194
|
-
<form bind:this={
|
|
204
|
+
<form bind:this={formEl} class={_class} use:onSubmitValidityCheck {...rest}>
|
|
195
205
|
<!-- General error alert -->
|
|
196
206
|
<DismissibleMessage message={error} intent="destructive" />
|
|
197
207
|
|