| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- import { pageMetadata } from "@/lib/page-metadata"
- export const metadata = pageMetadata("docs/validation")
- # Validation
- Validate form inputs with built-in and custom functions.
- ## Built-in Validators
- json-render includes common validation functions:
- - `required` — Value must be non-empty
- - `email` — Valid email format
- - `minLength` — Minimum string length (args: `{ "min": N }`)
- - `maxLength` — Maximum string length (args: `{ "max": N }`)
- - `pattern` — Match a regex pattern (args: `{ "pattern": "regex" }`)
- - `min` — Minimum numeric value (args: `{ "min": N }`)
- - `max` — Maximum numeric value (args: `{ "max": N }`)
- - `numeric` — Value must be a number
- - `url` — Valid URL format
- - `matches` — Must equal another field (args: `{ "other": { "$state": "/path" } }`)
- - `equalTo` — Alias for matches (args: `{ "other": { "$state": "/path" } }`)
- - `lessThan` — Value must be less than another field (args: `{ "other": { "$state": "/path" } }`)
- - `greaterThan` — Value must be greater than another field (args: `{ "other": { "$state": "/path" } }`)
- - `requiredIf` — Required only when another field is truthy (args: `{ "field": { "$state": "/path" } }`)
- ## Using Validation in JSON
- 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):
- ```json
- {
- "type": "TextField",
- "props": {
- "label": "Email",
- "value": { "$bindState": "/form/email" },
- "checks": [
- { "type": "required", "message": "Email is required" },
- { "type": "email", "message": "Invalid email format" }
- ],
- "validateOn": "blur"
- }
- }
- ```
- ## Validation with Parameters
- ```json
- {
- "type": "TextField",
- "props": {
- "label": "Password",
- "value": { "$bindState": "/form/password" },
- "checks": [
- { "type": "required", "message": "Password is required" },
- {
- "type": "minLength",
- "args": { "min": 8 },
- "message": "Password must be at least 8 characters"
- },
- {
- "type": "pattern",
- "args": { "pattern": "[A-Z]" },
- "message": "Must contain at least one uppercase letter"
- }
- ]
- }
- }
- ```
- ## Custom Validation Functions
- Define custom validators in your catalog's `functions` field. The catalog itself is framework-agnostic — only the `schema` import varies by platform:
- ```typescript
- import { defineCatalog } from '@json-render/core';
- import { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'
- import { z } from 'zod';
- const catalog = defineCatalog(schema, {
- components: { /* ... */ },
- functions: {
- isValidPhone: {
- description: 'Validates phone number format',
- },
- isUniqueEmail: {
- description: 'Checks if email is not already registered',
- },
- },
- });
- ```
- ## Usage with React
- In `@json-render/react`, use `ValidationProvider` to supply implementations for your custom validators:
- ```tsx
- import { ValidationProvider } from '@json-render/react';
- function App() {
- const customValidators = {
- isValidPhone: (value) => {
- const phoneRegex = /^\+?[1-9]\d{1,14}$/;
- return phoneRegex.test(value);
- },
- isUniqueEmail: async (value) => {
- const response = await fetch(`/api/check-email?email=${value}`);
- const { available } = await response.json();
- return available;
- },
- };
- return (
- <ValidationProvider customFunctions={customValidators}>
- {/* Your UI */}
- </ValidationProvider>
- );
- }
- ```
- ### Using in Components
- The `useFieldValidation` and `useBoundProp` hooks wire validation into your registry components. Validation uses the path from `bindings?.value` (the bound state path):
- ```tsx
- import { useFieldValidation, useBoundProp } from '@json-render/react';
- function TextField({ props, bindings }) {
- const [value, setValue] = useBoundProp(props.value, bindings?.value);
- const { errors, isValid, validate, touch, clear } = useFieldValidation(
- bindings?.value ?? null,
- { checks: props.checks, validateOn: props.validateOn }
- );
- return (
- <div>
- <label>{props.label}</label>
- <input
- value={value || ''}
- onChange={(e) => setValue(e.target.value)}
- onBlur={() => validate()}
- />
- {errors.map((error, i) => (
- <p key={i} className="text-red-500 text-sm">{error}</p>
- ))}
- </div>
- );
- }
- ```
- See the [@json-render/react API reference](/docs/api/react) for full `ValidationProvider` and `useFieldValidation` documentation.
- ## Cross-Field Validation
- Validation args support `{ "$state": "/path" }` references to compare against other fields. This enables cross-field rules like "confirm password must match password":
- ```json
- {
- "type": "Input",
- "props": {
- "label": "Confirm Password",
- "value": { "$bindState": "/form/confirmPassword" },
- "checks": [
- { "type": "required", "message": "Please confirm your password" },
- {
- "type": "matches",
- "args": { "other": { "$state": "/form/password" } },
- "message": "Passwords must match"
- }
- ]
- }
- }
- ```
- Other cross-field examples:
- ```json
- {
- "checks": [
- {
- "type": "greaterThan",
- "args": { "other": { "$state": "/form/startDate" } },
- "message": "End date must be after start date"
- }
- ]
- }
- ```
- ```json
- {
- "checks": [
- {
- "type": "requiredIf",
- "args": { "field": { "$state": "/form/enableNotifications" } },
- "message": "Email is required when notifications are enabled"
- }
- ]
- }
- ```
- ## Conditional Validation
- Use the `enabled` field in the validation config to only run checks when a condition is met:
- ```json
- {
- "type": "Input",
- "props": {
- "label": "Company Name",
- "value": { "$bindState": "/form/company" },
- "checks": [
- { "type": "required", "message": "Company name is required" }
- ]
- }
- }
- ```
- In the component implementation, you can pass `enabled` to `useFieldValidation`:
- ```typescript
- useFieldValidation(bindings?.value ?? "", {
- checks: props.checks ?? [],
- enabled: { "$state": "/form/accountType", eq: "business" },
- });
- ```
- This only validates the company name when the account type is "business".
- ## Validation Timing
- Control when validation runs with `validateOn`:
- - `change` — Validate on every input change
- - `blur` — Validate when field loses focus (default for Input, Textarea)
- - `submit` — Validate only on form submission
- ## Form-Level Validation
- 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:
- ```json
- {
- "type": "Button",
- "props": { "label": "Submit" },
- "on": {
- "press": [
- { "action": "validateForm", "params": { "statePath": "/formResult" } },
- { "action": "submitForm" }
- ]
- },
- "children": []
- }
- ```
- 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.
- > **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.
- ## Next
- - [Computed Values](/docs/computed-values) — derive dynamic prop values
- - [Watchers](/docs/watchers) — react to state changes
- - [Generation Modes](/docs/generation-modes) — how AI generates specs
|