page.mdx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import { pageMetadata } from "@/lib/page-metadata"
  2. export const metadata = pageMetadata("docs/validation")
  3. # Validation
  4. Validate form inputs with built-in and custom functions.
  5. ## Built-in Validators
  6. json-render includes common validation functions:
  7. - `required` — Value must be non-empty
  8. - `email` — Valid email format
  9. - `minLength` — Minimum string length (args: `{ "min": N }`)
  10. - `maxLength` — Maximum string length (args: `{ "max": N }`)
  11. - `pattern` — Match a regex pattern (args: `{ "pattern": "regex" }`)
  12. - `min` — Minimum numeric value (args: `{ "min": N }`)
  13. - `max` — Maximum numeric value (args: `{ "max": N }`)
  14. - `numeric` — Value must be a number
  15. - `url` — Valid URL format
  16. - `matches` — Must equal another field (args: `{ "other": { "$state": "/path" } }`)
  17. - `equalTo` — Alias for matches (args: `{ "other": { "$state": "/path" } }`)
  18. - `lessThan` — Value must be less than another field (args: `{ "other": { "$state": "/path" } }`)
  19. - `greaterThan` — Value must be greater than another field (args: `{ "other": { "$state": "/path" } }`)
  20. - `requiredIf` — Required only when another field is truthy (args: `{ "field": { "$state": "/path" } }`)
  21. ## Using Validation in JSON
  22. Use `{ "$bindState": "/path" }` on the value prop for two-way binding. Validation checks run against the value at the bound path (available as `bindings?.value` in components):
  23. ```json
  24. {
  25. "type": "TextField",
  26. "props": {
  27. "label": "Email",
  28. "value": { "$bindState": "/form/email" },
  29. "checks": [
  30. { "type": "required", "message": "Email is required" },
  31. { "type": "email", "message": "Invalid email format" }
  32. ],
  33. "validateOn": "blur"
  34. }
  35. }
  36. ```
  37. ## Validation with Parameters
  38. ```json
  39. {
  40. "type": "TextField",
  41. "props": {
  42. "label": "Password",
  43. "value": { "$bindState": "/form/password" },
  44. "checks": [
  45. { "type": "required", "message": "Password is required" },
  46. {
  47. "type": "minLength",
  48. "args": { "min": 8 },
  49. "message": "Password must be at least 8 characters"
  50. },
  51. {
  52. "type": "pattern",
  53. "args": { "pattern": "[A-Z]" },
  54. "message": "Must contain at least one uppercase letter"
  55. }
  56. ]
  57. }
  58. }
  59. ```
  60. ## Custom Validation Functions
  61. Define custom validators in your catalog's `functions` field. The catalog itself is framework-agnostic — only the `schema` import varies by platform:
  62. ```typescript
  63. import { defineCatalog } from '@json-render/core';
  64. import { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'
  65. import { z } from 'zod';
  66. const catalog = defineCatalog(schema, {
  67. components: { /* ... */ },
  68. functions: {
  69. isValidPhone: {
  70. description: 'Validates phone number format',
  71. },
  72. isUniqueEmail: {
  73. description: 'Checks if email is not already registered',
  74. },
  75. },
  76. });
  77. ```
  78. ## Usage with React
  79. In `@json-render/react`, use `ValidationProvider` to supply implementations for your custom validators:
  80. ```tsx
  81. import { ValidationProvider } from '@json-render/react';
  82. function App() {
  83. const customValidators = {
  84. isValidPhone: (value) => {
  85. const phoneRegex = /^\+?[1-9]\d{1,14}$/;
  86. return phoneRegex.test(value);
  87. },
  88. isUniqueEmail: async (value) => {
  89. const response = await fetch(`/api/check-email?email=${value}`);
  90. const { available } = await response.json();
  91. return available;
  92. },
  93. };
  94. return (
  95. <ValidationProvider customFunctions={customValidators}>
  96. {/* Your UI */}
  97. </ValidationProvider>
  98. );
  99. }
  100. ```
  101. ### Using in Components
  102. The `useFieldValidation` and `useBoundProp` hooks wire validation into your registry components. Validation uses the path from `bindings?.value` (the bound state path):
  103. ```tsx
  104. import { useFieldValidation, useBoundProp } from '@json-render/react';
  105. function TextField({ props, bindings }) {
  106. const [value, setValue] = useBoundProp(props.value, bindings?.value);
  107. const { errors, isValid, validate, touch, clear } = useFieldValidation(
  108. bindings?.value ?? null,
  109. { checks: props.checks, validateOn: props.validateOn }
  110. );
  111. return (
  112. <div>
  113. <label>{props.label}</label>
  114. <input
  115. value={value || ''}
  116. onChange={(e) => setValue(e.target.value)}
  117. onBlur={() => validate()}
  118. />
  119. {errors.map((error, i) => (
  120. <p key={i} className="text-red-500 text-sm">{error}</p>
  121. ))}
  122. </div>
  123. );
  124. }
  125. ```
  126. See the [@json-render/react API reference](/docs/api/react) for full `ValidationProvider` and `useFieldValidation` documentation.
  127. ## Cross-Field Validation
  128. Validation args support `{ "$state": "/path" }` references to compare against other fields. This enables cross-field rules like "confirm password must match password":
  129. ```json
  130. {
  131. "type": "Input",
  132. "props": {
  133. "label": "Confirm Password",
  134. "value": { "$bindState": "/form/confirmPassword" },
  135. "checks": [
  136. { "type": "required", "message": "Please confirm your password" },
  137. {
  138. "type": "matches",
  139. "args": { "other": { "$state": "/form/password" } },
  140. "message": "Passwords must match"
  141. }
  142. ]
  143. }
  144. }
  145. ```
  146. Other cross-field examples:
  147. ```json
  148. {
  149. "checks": [
  150. {
  151. "type": "greaterThan",
  152. "args": { "other": { "$state": "/form/startDate" } },
  153. "message": "End date must be after start date"
  154. }
  155. ]
  156. }
  157. ```
  158. ```json
  159. {
  160. "checks": [
  161. {
  162. "type": "requiredIf",
  163. "args": { "field": { "$state": "/form/enableNotifications" } },
  164. "message": "Email is required when notifications are enabled"
  165. }
  166. ]
  167. }
  168. ```
  169. ## Conditional Validation
  170. Use the `enabled` field in the validation config to only run checks when a condition is met:
  171. ```json
  172. {
  173. "type": "Input",
  174. "props": {
  175. "label": "Company Name",
  176. "value": { "$bindState": "/form/company" },
  177. "checks": [
  178. { "type": "required", "message": "Company name is required" }
  179. ]
  180. }
  181. }
  182. ```
  183. In the component implementation, you can pass `enabled` to `useFieldValidation`:
  184. ```typescript
  185. useFieldValidation(bindings?.value ?? "", {
  186. checks: props.checks ?? [],
  187. enabled: { "$state": "/form/accountType", eq: "business" },
  188. });
  189. ```
  190. This only validates the company name when the account type is "business".
  191. ## Validation Timing
  192. Control when validation runs with `validateOn`:
  193. - `change` — Validate on every input change
  194. - `blur` — Validate when field loses focus (default for Input, Textarea)
  195. - `submit` — Validate only on form submission
  196. ## Form-Level Validation
  197. Use the built-in `validateForm` action to validate all registered fields at once. This is useful for a "Submit" button that should validate the entire form before proceeding:
  198. ```json
  199. {
  200. "type": "Button",
  201. "props": { "label": "Submit" },
  202. "on": {
  203. "press": [
  204. { "action": "validateForm", "params": { "statePath": "/formResult" } },
  205. { "action": "submitForm" }
  206. ]
  207. },
  208. "children": []
  209. }
  210. ```
  211. The `validateForm` action runs `validateAll()` and writes `{ valid: boolean }` to the specified state path (defaults to `/formValidation`). Your submit handler can then check `{ "$state": "/formResult/valid" }` to decide whether to proceed.
  212. > **Note:** Actions in a list execute sequentially, but `submitForm` does not automatically gate on validation. Guard submission with a `$cond` visibility condition on the button or check `{ "$state": "/formResult/valid" }` inside your action handler to skip submission when the form is invalid.
  213. ## Next
  214. - [Computed Values](/docs/computed-values) — derive dynamic prop values
  215. - [Watchers](/docs/watchers) — react to state changes
  216. - [Generation Modes](/docs/generation-modes) — how AI generates specs