schema-components 1.5.0 → 1.6.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/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [1.6.0](https://github.com/Mearman/schema-components/compare/v1.5.1...v1.6.0) (2026-05-14)
2
+
3
+ ### Features
4
+
5
+ * add per-field onValidationError and writeOnly tests ([bc65589](https://github.com/Mearman/schema-components/commit/bc655893601b8e015dd3c717eccc9bf9dffbbb42))
6
+
7
+ ### Tests
8
+
9
+ * add writeOnly behaviour tests across all renderers ([abfb293](https://github.com/Mearman/schema-components/commit/abfb293498efc4d4095dec072fd1e5a9c3239035))
10
+
11
+ ## [1.5.1](https://github.com/Mearman/schema-components/compare/v1.5.0...v1.5.1) (2026-05-14)
12
+
13
+ ### Bug Fixes
14
+
15
+ * blank writeOnly values in boolean and number renderers ([b46ecac](https://github.com/Mearman/schema-components/commit/b46ecac8d997fd4b875d6090d5374cded1143e5d))
16
+
1
17
  ## [1.5.0](https://github.com/Mearman/schema-components/compare/v1.4.0...v1.5.0) (2026-05-14)
2
18
 
3
19
  ### Features
@@ -1,4 +1,4 @@
1
- import { l as JsonObject, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
1
+ import { l as JsonObject, m as SchemaMeta } from "../types-CnlV7bBK.mjs";
2
2
 
3
3
  //#region src/core/adapter.d.ts
4
4
  type SchemaInput = Record<string, unknown>;
@@ -1,2 +1,2 @@
1
- import { A as mergeResolvers, C as HtmlResolver, D as getHtmlRenderFn, E as RenderProps, O as getRenderFunction, S as HtmlRenderProps, T as RenderFunction, b as ComponentResolver, j as typeToKey, k as mergeHtmlResolvers, w as RESOLVER_KEYS, x as HtmlRenderFunction, y as BaseFieldProps } from "../types-DDCD6Xnx.mjs";
1
+ import { A as mergeResolvers, C as HtmlResolver, D as getHtmlRenderFn, E as RenderProps, O as getRenderFunction, S as HtmlRenderProps, T as RenderFunction, b as ComponentResolver, j as typeToKey, k as mergeHtmlResolvers, w as RESOLVER_KEYS, x as HtmlRenderFunction, y as BaseFieldProps } from "../types-CnlV7bBK.mjs";
2
2
  export { BaseFieldProps, ComponentResolver, HtmlRenderFunction, HtmlRenderProps, HtmlResolver, RESOLVER_KEYS, RenderFunction, RenderProps, getHtmlRenderFn, getRenderFunction, mergeHtmlResolvers, mergeResolvers, typeToKey };
@@ -1,3 +1,3 @@
1
- import { C as HtmlResolver, E as RenderProps, S as HtmlRenderProps, T as RenderFunction, _ as WalkedField, a as FromJSONSchema, b as ComponentResolver, c as InferResponseFields, d as OpenAPIResponseType, f as PathOfType, g as TypeAtPath, h as SchemaType, i as FieldOverrides, l as JsonObject, m as SchemaMeta, n as FieldConstraints, o as InferParameterOverrides, p as ResolveOpenAPIRef, r as FieldOverride, s as InferRequestBodyFields, t as Editability, u as OpenAPIRequestBodyType, v as resolveEditability, x as HtmlRenderFunction, y as BaseFieldProps } from "../types-DDCD6Xnx.mjs";
1
+ import { C as HtmlResolver, E as RenderProps, S as HtmlRenderProps, T as RenderFunction, _ as WalkedField, a as FromJSONSchema, b as ComponentResolver, c as InferResponseFields, d as OpenAPIResponseType, f as PathOfType, g as TypeAtPath, h as SchemaType, i as FieldOverrides, l as JsonObject, m as SchemaMeta, n as FieldConstraints, o as InferParameterOverrides, p as ResolveOpenAPIRef, r as FieldOverride, s as InferRequestBodyFields, t as Editability, u as OpenAPIRequestBodyType, v as resolveEditability, x as HtmlRenderFunction, y as BaseFieldProps } from "../types-CnlV7bBK.mjs";
2
2
  import { i as SchemaRenderError, n as SchemaFieldError, r as SchemaNormalisationError, t as SchemaError } from "../errors-DIKI2C78.mjs";
3
3
  export { BaseFieldProps, ComponentResolver, Editability, FieldConstraints, FieldOverride, FieldOverrides, FromJSONSchema, HtmlRenderFunction, HtmlRenderProps, HtmlResolver, InferParameterOverrides, InferRequestBodyFields, InferResponseFields, JsonObject, OpenAPIRequestBodyType, OpenAPIResponseType, PathOfType, RenderFunction, RenderProps, ResolveOpenAPIRef, SchemaError, SchemaFieldError, SchemaMeta, SchemaNormalisationError, SchemaRenderError, SchemaType, TypeAtPath, WalkedField, resolveEditability };
@@ -1,4 +1,4 @@
1
- import { _ as WalkedField, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
1
+ import { _ as WalkedField, m as SchemaMeta } from "../types-CnlV7bBK.mjs";
2
2
 
3
3
  //#region src/core/walker.d.ts
4
4
  interface WalkOptions {
@@ -1,4 +1,4 @@
1
- import { _ as WalkedField, n as FieldConstraints } from "../types-DDCD6Xnx.mjs";
1
+ import { _ as WalkedField, n as FieldConstraints } from "../types-CnlV7bBK.mjs";
2
2
  import { HtmlAttributes, HtmlNode } from "./html.mjs";
3
3
 
4
4
  //#region src/html/a11y.d.ts
@@ -1,4 +1,4 @@
1
- import { C as HtmlResolver, S as HtmlRenderProps, m as SchemaMeta, x as HtmlRenderFunction } from "../types-DDCD6Xnx.mjs";
1
+ import { C as HtmlResolver, S as HtmlRenderProps, m as SchemaMeta, x as HtmlRenderFunction } from "../types-CnlV7bBK.mjs";
2
2
 
3
3
  //#region src/html/renderToHtml.d.ts
4
4
  interface RenderToHtmlOptions {
@@ -133,7 +133,7 @@ function renderBooleanEditable(props) {
133
133
  type: "checkbox",
134
134
  name: id
135
135
  };
136
- if (props.value === true) attrs.checked = true;
136
+ if (!props.writeOnly && props.value === true) attrs.checked = true;
137
137
  Object.assign(attrs, ariaRequiredAttrs(props.tree));
138
138
  Object.assign(attrs, ariaLabelAttrs(props.meta.description));
139
139
  return h("input", attrs);
@@ -1,4 +1,4 @@
1
- import { c as InferResponseFields, m as SchemaMeta, o as InferParameterOverrides, r as FieldOverride, s as InferRequestBodyFields } from "../types-DDCD6Xnx.mjs";
1
+ import { c as InferResponseFields, m as SchemaMeta, o as InferParameterOverrides, r as FieldOverride, s as InferRequestBodyFields } from "../types-CnlV7bBK.mjs";
2
2
  import { WidgetMap } from "../react/SchemaComponent.mjs";
3
3
  import { ReactNode } from "react";
4
4
 
@@ -1,4 +1,4 @@
1
- import { l as JsonObject } from "../types-DDCD6Xnx.mjs";
1
+ import { l as JsonObject } from "../types-CnlV7bBK.mjs";
2
2
 
3
3
  //#region src/openapi/parser.d.ts
4
4
  interface OpenApiDocument {
@@ -1,4 +1,4 @@
1
- import { E as RenderProps, _ as WalkedField, a as FromJSONSchema, b as ComponentResolver, f as PathOfType, i as FieldOverrides, m as SchemaMeta, p as ResolveOpenAPIRef, r as FieldOverride } from "../types-DDCD6Xnx.mjs";
1
+ import { E as RenderProps, _ as WalkedField, a as FromJSONSchema, b as ComponentResolver, f as PathOfType, i as FieldOverrides, m as SchemaMeta, p as ResolveOpenAPIRef, r as FieldOverride } from "../types-CnlV7bBK.mjs";
2
2
  import { t as SchemaError } from "../errors-DIKI2C78.mjs";
3
3
  import { z } from "zod";
4
4
  import { ReactNode } from "react";
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { isObject, toRecord } from "../core/guards.mjs";
2
+ import { isObject, toRecord, toRecordOrUndefined } from "../core/guards.mjs";
3
3
  import { normaliseSchema } from "../core/adapter.mjs";
4
4
  import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
5
5
  import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
@@ -79,14 +79,21 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
79
79
  throw error;
80
80
  }
81
81
  const handleChange = useCallback((nextValue) => {
82
- if (validate) runValidation(zodSchema, jsonSchema, nextValue, onValidationError);
82
+ if (validate) {
83
+ const error = runValidation(zodSchema, jsonSchema, nextValue);
84
+ if (error !== void 0) {
85
+ onValidationError?.(error);
86
+ dispatchFieldErrors(fields, error);
87
+ }
88
+ }
83
89
  onChange?.(nextValue);
84
90
  }, [
85
91
  validate,
86
92
  zodSchema,
87
93
  jsonSchema,
88
94
  onChange,
89
- onValidationError
95
+ onValidationError,
96
+ fields
90
97
  ]);
91
98
  const tree = walk(jsonSchema, {
92
99
  componentMeta: mergedMeta,
@@ -99,15 +106,12 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
99
106
  };
100
107
  return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild, instanceWidgets, contextWidgets);
101
108
  }
102
- function runValidation(zodSchema, jsonSchema, value, onError) {
109
+ function runValidation(zodSchema, jsonSchema, value) {
103
110
  if (zodSchema !== void 0 && isObject(zodSchema)) {
104
111
  const safeParseFn = zodSchema.safeParse;
105
112
  if (isCallable(safeParseFn)) {
106
113
  const result = safeParseFn(value);
107
- if (isObject(result) && "success" in result && result.success !== true) {
108
- onError?.(result.error);
109
- return;
110
- }
114
+ if (isObject(result) && "success" in result && result.success !== true) return result.error;
111
115
  return;
112
116
  }
113
117
  }
@@ -116,7 +120,7 @@ function runValidation(zodSchema, jsonSchema, value, onError) {
116
120
  const safeParseFn = parsed.safeParse;
117
121
  if (isCallable(safeParseFn)) {
118
122
  const result = safeParseFn(value);
119
- if (isObject(result) && "success" in result && result.success !== true) onError?.(result.error);
123
+ if (isObject(result) && "success" in result && result.success !== true) return result.error;
120
124
  }
121
125
  }
122
126
  }
@@ -197,7 +201,8 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
197
201
  const handleChange = useCallback((nextFieldValue) => {
198
202
  if (validate) {
199
203
  const newRootValue = setNestedValue(value, path, nextFieldValue);
200
- runValidation(zodSchema, jsonSchema, newRootValue, onValidationError);
204
+ const error = runValidation(zodSchema, jsonSchema, newRootValue);
205
+ if (error !== void 0) onValidationError?.(error);
201
206
  }
202
207
  const newRootValue = setNestedValue(value, path, nextFieldValue);
203
208
  onChange?.(newRootValue);
@@ -280,6 +285,36 @@ function setNestedValue(root, path, leafValue) {
280
285
  }
281
286
  return result;
282
287
  }
288
+ function isFieldErrorCallback(value) {
289
+ return typeof value === "function";
290
+ }
291
+ /**
292
+ * Dispatch Zod errors to per-field onValidationError callbacks.
293
+ * Walks the fields override tree and matches errors by path prefix.
294
+ */
295
+ function dispatchFieldErrors(fields, error) {
296
+ if (fields === void 0 || !isObject(error)) return;
297
+ if (!("issues" in error)) return;
298
+ const issues = error.issues;
299
+ if (!Array.isArray(issues)) return;
300
+ const overrides = toRecordOrUndefined(fields);
301
+ if (overrides === void 0) return;
302
+ for (const [key, override] of Object.entries(overrides)) {
303
+ if (override === void 0 || typeof override !== "object") continue;
304
+ if (override === null) continue;
305
+ if (!("onValidationError" in override)) continue;
306
+ const fieldCallback = override.onValidationError;
307
+ if (typeof fieldCallback !== "function") continue;
308
+ const fieldErrors = issues.filter((issue) => {
309
+ if (!isObject(issue)) return false;
310
+ if (!("path" in issue)) return false;
311
+ const path = issue.path;
312
+ if (!Array.isArray(path)) return false;
313
+ return path[0] === key;
314
+ });
315
+ if (fieldErrors.length > 0 && isFieldErrorCallback(fieldCallback)) fieldCallback({ issues: fieldErrors });
316
+ }
317
+ }
283
318
  function isCallable(value) {
284
319
  return typeof value === "function";
285
320
  }
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
1
+ import { b as ComponentResolver, m as SchemaMeta } from "../types-CnlV7bBK.mjs";
2
2
  import { WidgetMap } from "./SchemaComponent.mjs";
3
3
  import { ReactNode } from "react";
4
4
 
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver } from "../types-DDCD6Xnx.mjs";
1
+ import { b as ComponentResolver } from "../types-CnlV7bBK.mjs";
2
2
  import { ReactNode } from "react";
3
3
 
4
4
  //#region src/react/headless.d.ts
@@ -200,7 +200,7 @@ function renderBoolean(props) {
200
200
  return /* @__PURE__ */ jsx("input", {
201
201
  id,
202
202
  type: "checkbox",
203
- checked: props.value === true,
203
+ checked: props.writeOnly ? false : props.value === true,
204
204
  onChange: (e) => {
205
205
  props.onChange(e.target.checked);
206
206
  },
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver } from "../types-DDCD6Xnx.mjs";
1
+ import { b as ComponentResolver } from "../types-CnlV7bBK.mjs";
2
2
 
3
3
  //#region src/themes/mui.d.ts
4
4
  /**
@@ -45,7 +45,7 @@ function renderNumberInput(props) {
45
45
  return /* @__PURE__ */ jsx(MuiTextField, {
46
46
  label,
47
47
  type: "number",
48
- value: typeof props.value === "number" ? props.value : "",
48
+ value: props.writeOnly ? "" : typeof props.value === "number" ? props.value : "",
49
49
  onChange: (e) => {
50
50
  props.onChange(Number(e.target.value));
51
51
  },
@@ -73,7 +73,7 @@ function renderBooleanInput(props) {
73
73
  }
74
74
  return /* @__PURE__ */ jsx(MuiFormControlLabel, {
75
75
  control: /* @__PURE__ */ jsx(MuiCheckbox, {
76
- checked: props.value === true,
76
+ checked: props.writeOnly ? false : props.value === true,
77
77
  onChange: (e) => {
78
78
  props.onChange(e.target.checked);
79
79
  }
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver } from "../types-DDCD6Xnx.mjs";
1
+ import { b as ComponentResolver } from "../types-CnlV7bBK.mjs";
2
2
 
3
3
  //#region src/themes/shadcn.d.ts
4
4
  declare const shadcnResolver: ComponentResolver;
@@ -51,7 +51,7 @@ function renderNumberInput(props) {
51
51
  return /* @__PURE__ */ jsx("input", {
52
52
  type: "number",
53
53
  className,
54
- value: typeof props.value === "number" ? props.value : "",
54
+ value: props.writeOnly ? "" : typeof props.value === "number" ? props.value : "",
55
55
  onChange: (e) => {
56
56
  props.onChange(Number(e.target.value));
57
57
  },
@@ -74,7 +74,7 @@ function renderBooleanInput(props) {
74
74
  return /* @__PURE__ */ jsx("input", {
75
75
  type: "checkbox",
76
76
  className,
77
- checked: props.value === true,
77
+ checked: props.writeOnly ? false : props.value === true,
78
78
  onChange: (e) => {
79
79
  props.onChange(e.target.checked);
80
80
  }
@@ -166,14 +166,18 @@ type Editability = "presentation" | "input" | "editable";
166
166
  declare function resolveEditability(propertyMeta: SchemaMeta | undefined, componentMeta: SchemaMeta | undefined, rootMeta: SchemaMeta | undefined): Editability;
167
167
  /**
168
168
  * Recursive mapped type that mirrors a schema's shape for per-field
169
- * meta overrides. Each leaf is `Partial<SchemaMeta>`, objects recurse
170
- * and also accept their own `SchemaMeta`.
169
+ * overrides. Each leaf accepts schema meta overrides and an optional
170
+ * per-field validation error callback. Objects recurse and also accept
171
+ * their own overrides.
171
172
  */
172
- type FieldOverrides<T> = { [K in keyof T]?: T[K] extends object ? FieldOverrides<T[K]> & Partial<SchemaMeta> : Partial<SchemaMeta> };
173
+ type FieldOverrides<T> = { [K in keyof T]?: T[K] extends object ? FieldOverrides<T[K]> & FieldOverride : FieldOverride };
173
174
  /**
174
- * Fallback type for runtime schemas (no compile-time shape).
175
+ * Per-field override. Extends SchemaMeta with a React-layer callback
176
+ * for per-field validation errors.
175
177
  */
176
- type FieldOverride = Partial<SchemaMeta>;
178
+ type FieldOverride = Partial<SchemaMeta> & {
179
+ /** Called with the ZodError when this field fails validation. */onValidationError?: (error: unknown) => void;
180
+ };
177
181
  type SchemaType = "string" | "number" | "boolean" | "null" | "enum" | "literal" | "object" | "array" | "record" | "union" | "discriminatedUnion" | "optional" | "nullable" | "default" | "readonly" | "pipe" | "lazy" | "file" | "unknown";
178
182
  interface WalkedField {
179
183
  type: SchemaType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",