@vertz/ui 0.2.25 → 0.2.26

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.
@@ -103,6 +103,74 @@ function validate(schema, data) {
103
103
  }
104
104
  return { success: false, data: undefined, errors: { _form: "Validation failed" } };
105
105
  }
106
+ function isSchemaLike(value) {
107
+ return value !== null && typeof value === "object" && "parse" in value && typeof value.parse === "function";
108
+ }
109
+ function resolveFieldSchema(schema, fieldPath) {
110
+ const shape = schema.shape;
111
+ if (!shape || typeof shape !== "object")
112
+ return;
113
+ const segments = fieldPath.split(".");
114
+ let current = shape;
115
+ for (let i = 0;i < segments.length; i++) {
116
+ if (!current || typeof current !== "object")
117
+ return;
118
+ const segment = segments[i];
119
+ let fieldSchema = current[segment];
120
+ if (i < segments.length - 1) {
121
+ let unwrapCount = 0;
122
+ while (fieldSchema && typeof fieldSchema === "object" && "unwrap" in fieldSchema && typeof fieldSchema.unwrap === "function" && !fieldSchema.shape && unwrapCount < 10) {
123
+ fieldSchema = fieldSchema.unwrap();
124
+ unwrapCount++;
125
+ }
126
+ if (fieldSchema && typeof fieldSchema === "object" && "shape" in fieldSchema && typeof fieldSchema.shape === "object") {
127
+ current = fieldSchema.shape;
128
+ } else {
129
+ return;
130
+ }
131
+ } else {
132
+ if (isSchemaLike(fieldSchema)) {
133
+ return fieldSchema;
134
+ }
135
+ return;
136
+ }
137
+ }
138
+ return;
139
+ }
140
+ function extractErrorFromParseResult(result) {
141
+ const err = result.error;
142
+ if (err instanceof Error) {
143
+ const issues = err.issues;
144
+ if (Array.isArray(issues) && issues.length > 0) {
145
+ return issues[0].message ?? "Validation failed";
146
+ }
147
+ return err.message;
148
+ }
149
+ return "Validation failed";
150
+ }
151
+ function validateField(schema, fieldName, value, formData) {
152
+ const fieldSchema = resolveFieldSchema(schema, fieldName);
153
+ if (fieldSchema) {
154
+ const result = fieldSchema.parse(value);
155
+ if (result.ok) {
156
+ return { valid: true, error: undefined };
157
+ }
158
+ const error = extractErrorFromParseResult(result);
159
+ return { valid: false, error };
160
+ }
161
+ if (formData) {
162
+ const result = validate(schema, formData);
163
+ if (result.success) {
164
+ return { valid: true, error: undefined };
165
+ }
166
+ const fieldError = result.errors[fieldName];
167
+ if (fieldError) {
168
+ return { valid: false, error: fieldError };
169
+ }
170
+ return { valid: true, error: undefined };
171
+ }
172
+ return { valid: true, error: undefined };
173
+ }
106
174
 
107
175
  // src/form/form.ts
108
176
  var FIELD_STATE_SIGNALS = new Set(["error", "dirty", "touched", "value"]);
@@ -112,6 +180,8 @@ function form(sdkMethod, options) {
112
180
  const chainProxyCache = new Map;
113
181
  const submitting = signal(false);
114
182
  const fieldGeneration = signal(0);
183
+ const revalidateOn = options?.revalidateOn ?? "blur";
184
+ let hasSubmitted = false;
115
185
  const dirty = computed(() => {
116
186
  fieldGeneration.value;
117
187
  for (const field of fieldCache.values()) {
@@ -178,6 +248,7 @@ function form(sdkMethod, options) {
178
248
  }
179
249
  const resolvedSchema = options?.schema ?? sdkMethod.meta?.bodySchema;
180
250
  async function submitPipeline(formData) {
251
+ hasSubmitted = true;
181
252
  const data = formDataToObject(formData, { nested: true });
182
253
  if (resolvedSchema) {
183
254
  const result2 = validate(resolvedSchema, data);
@@ -209,10 +280,42 @@ function form(sdkMethod, options) {
209
280
  }
210
281
  let boundElement;
211
282
  function resetForm() {
283
+ hasSubmitted = false;
212
284
  for (const field of fieldCache.values()) {
213
285
  field.reset();
214
286
  }
215
287
  }
288
+ function assembleFormData() {
289
+ const result = {};
290
+ for (const [name, f] of fieldCache) {
291
+ const value = f.value.peek();
292
+ if (!name.includes(".")) {
293
+ result[name] = value;
294
+ continue;
295
+ }
296
+ const segments = name.split(".");
297
+ let current = result;
298
+ for (let i = 0;i < segments.length - 1; i++) {
299
+ const seg = segments[i];
300
+ if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
301
+ current[seg] = {};
302
+ }
303
+ current = current[seg];
304
+ }
305
+ current[segments[segments.length - 1]] = value;
306
+ }
307
+ return result;
308
+ }
309
+ function revalidateFieldIfNeeded(fieldName) {
310
+ if (!hasSubmitted || revalidateOn === "submit" || !resolvedSchema)
311
+ return;
312
+ const field = fieldCache.get(fieldName);
313
+ if (!field || field.error.peek() === undefined)
314
+ return;
315
+ const formData = assembleFormData();
316
+ const result = validateField(resolvedSchema, fieldName, field.value.peek(), formData);
317
+ field.error.value = result.valid ? undefined : result.error;
318
+ }
216
319
  async function submitPipelineWithReset(formData) {
217
320
  await submitPipeline(formData);
218
321
  if (options?.resetOnSuccess && !submitting.peek() && boundElement) {
@@ -228,6 +331,9 @@ function form(sdkMethod, options) {
228
331
  return;
229
332
  const field = getOrCreateField(target.name);
230
333
  field.setValue(target.value);
334
+ if (revalidateOn === "change") {
335
+ revalidateFieldIfNeeded(target.name);
336
+ }
231
337
  }
232
338
  function handleFocusout(e) {
233
339
  const target = e.target;
@@ -235,6 +341,9 @@ function form(sdkMethod, options) {
235
341
  return;
236
342
  const field = getOrCreateField(target.name);
237
343
  field.touched.value = true;
344
+ if (revalidateOn === "blur") {
345
+ revalidateFieldIfNeeded(target.name);
346
+ }
238
347
  }
239
348
  const baseProperties = {
240
349
  action: sdkMethod.url,
@@ -156,6 +156,17 @@ interface FormOptions<
156
156
  onError?: (errors: Record<string, string>) => void;
157
157
  /** When true, reset the form after a successful submission. */
158
158
  resetOnSuccess?: boolean;
159
+ /**
160
+ * Controls when fields with errors are re-validated after the first form submission.
161
+ *
162
+ * - `'blur'` (default) — Re-validates flagged fields when the user blurs them.
163
+ * - `'change'` — Re-validates flagged fields on every input/change event.
164
+ * - `'submit'` — No re-validation between submissions; errors only update on submit.
165
+ *
166
+ * Re-validation only activates after the first submit attempt. Fields without prior
167
+ * errors are never re-validated on blur/change (no premature validation).
168
+ */
169
+ revalidateOn?: "submit" | "blur" | "change";
159
170
  }
160
171
  /**
161
172
  * Create a form instance bound to an SDK method.
@@ -3,7 +3,7 @@ import {
3
3
  form,
4
4
  formDataToObject,
5
5
  validate
6
- } from "../../shared/chunk-bk7mmn92.js";
6
+ } from "../../shared/chunk-h2sjma78.js";
7
7
  import"../../shared/chunk-ppr06jgn.js";
8
8
  export {
9
9
  validate,
@@ -964,6 +964,17 @@ interface FormOptions<
964
964
  onError?: (errors: Record<string, string>) => void;
965
965
  /** When true, reset the form after a successful submission. */
966
966
  resetOnSuccess?: boolean;
967
+ /**
968
+ * Controls when fields with errors are re-validated after the first form submission.
969
+ *
970
+ * - `'blur'` (default) — Re-validates flagged fields when the user blurs them.
971
+ * - `'change'` — Re-validates flagged fields on every input/change event.
972
+ * - `'submit'` — No re-validation between submissions; errors only update on submit.
973
+ *
974
+ * Re-validation only activates after the first submit attempt. Fields without prior
975
+ * errors are never re-validated on blur/change (no premature validation).
976
+ */
977
+ revalidateOn?: "submit" | "blur" | "change";
967
978
  }
968
979
  /**
969
980
  * Create a form instance bound to an SDK method.
package/dist/src/index.js CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  form,
55
55
  formDataToObject,
56
56
  validate
57
- } from "../shared/chunk-bk7mmn92.js";
57
+ } from "../shared/chunk-h2sjma78.js";
58
58
  import {
59
59
  EntityStore,
60
60
  FieldSelectionTracker,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/ui",
3
- "version": "0.2.25",
3
+ "version": "0.2.26",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Vertz UI framework — signals, components, JSX runtime",
@@ -74,11 +74,11 @@
74
74
  "typecheck": "tsc --noEmit"
75
75
  },
76
76
  "dependencies": {
77
- "@vertz/fetch": "^0.2.24"
77
+ "@vertz/fetch": "^0.2.25"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@happy-dom/global-registrator": "^20.7.0",
81
- "@vertz/schema": "^0.2.24",
81
+ "@vertz/schema": "^0.2.25",
82
82
  "bunup": "^0.16.31",
83
83
  "happy-dom": "^20.7.0",
84
84
  "typescript": "^5.7.0"