@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.
@@ -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
- // typeof el.checkValidity === "function" && !el.checkValidity();
73
- // NOTE: el.checkValidity() returns true for hidden inputs event if they are invalid!
74
- // if (!el.checkValidity()) {
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={el} class={_class} onsubmit={handleFormSubmit} {...rest}>
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 `validate` action already clears it when the field's
136
- // customValidator returns an empty string, but a field can skip that path
137
- // (e.g., it was disabled, or its $effect torn down between submits) and the
138
- // stale flag would then make `el.validity.valid` return false on the next
139
- // submit, silently routing the form to `submit_invalid` and never calling
140
- // `onSubmit`. Resetting here gives every retry a clean slate.
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 = el;
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={el} class={_class} use:onSubmitValidityCheck {...rest}>
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
- svelte-ignore binding_property_non_reactive:
192
- formData is a $bindable prop — deep reactivity depends on the consumer
193
- passing a $state() object. The bindings work correctly regardless.
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 = el;
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={el} class={_class} use:onSubmitValidityCheck {...rest}>
204
+ <form bind:this={formEl} class={_class} use:onSubmitValidityCheck {...rest}>
195
205
  <!-- General error alert -->
196
206
  <DismissibleMessage message={error} intent="destructive" />
197
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.76.2",
3
+ "version": "3.76.4",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",