Skip to content

Latest commit

 

History

History
366 lines (295 loc) · 12 KB

README.md

File metadata and controls

366 lines (295 loc) · 12 KB

React JSON Schema Form Validation

npm CI MIT

Validate forms with powerful JSON Schema and Ajv !

This library links JSON Schema, Ajv and Form to :

  • describe data model with JSON Schema
  • validate the form data with Ajv
  • display & customize error messages
  • use your own graphical components to build friendly user forms.

Why RJFV ?

  • Simplicity (no extraneous features, just what you need)
  • Performance (AJV is extremely fast ⚡)
  • Actively maintained
  • The simplest react JSON Schema validation module ever published on npm ! ✌️

Other JSON Schema validation modules published on NPM are often complex, with too much features. That's why we created react-jsonschema-form-validation. You'll just need a schema, a form, some fields, and your data. Nothing more. it's S I M P L E

Our philosophy :

  • focused on validation, not UI
  • highly customizable
  • minimal CSS (15 lines) : just a red color to show error message (can be overriden)

Installation

npm install react-jsonschema-form-validation
yarn add react-jsonschema-form-validation

Then import the packaged stylesheet — the minimal CSS mentioned above (a red color for error messages, easily overridden):

import 'react-jsonschema-form-validation/dist/react-jsonschema-form-validation.css';

A minified variant is also shipped: react-jsonschema-form-validation/dist/react-jsonschema-form-validation.min.css.

Getting started

Import modules:

import React, { useState } from 'react';
import { Field, FieldError, Form } from 'react-jsonschema-form-validation';

Define your JSON-Schema:

const demoSchema = {
	type: 'object',
	properties: {
		email: { type: 'string', format: 'email' },
	},
	required: [
		'email',
	],
};

Declare your Form, Field and FieldError components. Pass your schema to the Form props.

Example using a functional component and React hooks:

const DemoForm = (props) => {
	const [formData, setFormData] = useState({ email: '' });
	
	const handleChange = (newData) => {
		// newData is a copy of the object formData with properties (and nested properties)
		// updated using immutability pattern for each change occured in the form.
		setFormData(newData);
	}
	
	const handleSubmit = () => {
		const { doWhateverYouWant } = props;
		doWhateverYouWant(formData); // Do whatever you want with the form data
	}

	return (
		<Form
			data={formData}
			onChange={handleChange}
			onSubmit={handleSubmit}
			schema={demoSchema}
		>
			<label>Email :</label>
			<Field
				name="email"
				value={formData.email}
			/>
			<FieldError name="email" />
			<button type="submit">Submit</button>
		</Form>
	);
}

Same example using a class component:

class DemoForm extends PureComponent {
	state = {
		formData: {
			email: '',
		},
	}
	
	handleChange = (newData) => {
		// newData is a copy of the object formData with properties (and nested properties)
		// updated using immutability pattern for each change occured in the form.
		this.setState({ formData: newData });
	}
	
	handleSubmit = () => {
		const { doWhateverYouWant } = this.props;
		const { formData } = this.state;
		doWhateverYouWant(formData); // Do whatever you want with the form data
	}

	render() {
		<Form
			data={this.state.formData}
			onChange={this.handleChange}
			onSubmit={this.handleSubmit}
			schema={demoSchema}
		>
			<label>Email :</label>
			<Field
				name="email"
				value={formData.email}
			/>
			<FieldError name="email" />
			<button type="submit">Submit</button>
		</Form>
	}
}

🎵 That's all folks !

Custom error messages

Pass an errorMessages map (keyword → function) to <Form> — or to a single <FieldError> — to replace AJV's raw messages. Each function receives the AJV error, which carries the current value of the offending field as error.data, so messages can quote what the user actually typed:

<Form
	data={formData}
	errorMessages={{
		minLength: (e) => `"${e.data}" is too short (min ${e.params.limit} characters)`,
		format: (e) => `"${e.data}" is not a valid ${e.params.format}`,
		required: () => 'This field is required',
	}}
	onChange={handleChange}
	onSubmit={handleSubmit}
	schema={demoSchema}
>
	<Field name="email" value={formData.email} />
	<FieldError name="email" />
	<button type="submit">Submit</button>
</Form>

What error.data contains, per error type:

  • value-level keywords (minLength, format, enum, const, pattern, minimum, … including $data references): the current value of the field;
  • required: the parent object missing the property (AJV reports required on the parent — the missing value itself does not exist). Use e.params.missingProperty for the field name.

Errors also expose error.schema (the failing keyword's value, e.g. 5 for minLength: 5) and error.parentSchema (the enclosing subschema).

Serialization & logging — with verbose, a required error carries the entire parent object in error.data. A JSON.stringify(errors) shipped to monitoring or logs can therefore exfiltrate sensitive sibling fields (a password living next to the missing property, for instance), and cyclic data would make stringify throw. Log a projection of chosen fields (field, keyword, message) rather than raw error objects.

Note — these properties come from AJV's verbose option, enabled on the default instance. If you pass your own instance through the ajv prop of <Form>, set verbose: true yourself to benefit from error.data.

Accessibility

<Field> and <FieldError> wire the essential ARIA attributes for you — no extra props needed:

  • <FieldError> renders with role="alert" by default, so assistive technologies announce the message as soon as it appears. Pass your own role prop to override it.
  • <Field> sets aria-invalid="true" on the rendered element when the field is invalid and revealed (touched, or the form was submitted) — the same condition as the visual error styling, so nothing is announced on a pristine form. It is a plain default: an explicit aria-invalid prop wins.
  • aria-describedby is wired automatically: each <FieldError> registers its id in the parent <Form>, and the matching <Field> references those ids once the field is revealed. The default id is deterministic — jfv<N>-error-<name>, where jfv<N> identifies the <Form> instance so two forms on the same page never collide. A custom id prop on <FieldError> is followed automatically.
  • An aria-describedby you pass to <Field> yourself is merged, not replaced: your ids come first (e.g. a hint text), the error ids after.
  • On a failed submit, keyboard focus moves to the first invalid field (disable with scrollToError={false} on <Form>).
<span id="email-hint">We never share your email.</span>
<Field name="email" aria-describedby="email-hint" value={formData.email} />
<FieldError name="email" />
{/* Once the field is touched (or the form submitted) and invalid,
    the input renders:
    aria-invalid="true" aria-describedby="email-hint jfv1-error-email" */}

The default error id can be computed with the exported helper (formId is available on the form context, see useFormContext):

import { getFieldErrorId } from 'react-jsonschema-form-validation';

getFieldErrorId('jfv1', 'email'); // 'jfv1-error-email'

Known limitations:

  • Rendering several <FieldError> with the same name in one form produces duplicate default ids (invalid HTML). Override id on all but one of them.
  • A name containing spaces cannot be referenced through the generated aria-describedby (it is a space-separated id list). Give such a <FieldError> a custom id.

Reading the form state: useFormContext

Everything <Form> knows is available to its descendants through the useFormContext() hook (a legacy render-prop helper, withFormContext(cb), also exists). Both throw a descriptive error when used outside a <Form>.

import { useFormContext } from 'react-jsonschema-form-validation';

Main context values:

Name Description
errors Current validation errors (FormattedError[], each with a normalized field path)
valid true when the data matches the schema. Validation is throttled (200 ms by default, throttleDuration prop on <Form>), so valid can lag one beat behind the latest change
isSubmitted true once a submit has been attempted (reset after a successful submit, unless resetOnSubmit={false})
touchedFields Names of the fields that have been blurred
reset() Resets errors, touchedFields and isSubmitted to their initial state (called automatically after a successful submit unless resetOnSubmit={false})
getFieldErrors(names) Errors for one or several field paths (wildcards like emails.* work)
isFieldInvalid(names) true if any of the given fields has an error
isFieldTouched(names) true if any of the given fields was touched
isTouched() true if at least one field was touched
touch(names) Marks fields as touched
handleFieldChange(event) or handleFieldChange(name, value) Applies a change programmatically
formId Unique id of the <Form> instance (see Accessibility)
errorMessages The errorMessages map passed to <Form>, if any

(The remaining values — fieldErrorsVersion, getFieldErrorDescribedBy, registerFieldError, unregisterFieldError — are internal wiring between <Field> and <FieldError>.)

Disable the submit button while the form is invalid:

const SubmitButton = ({ children }) => {
	const { valid } = useFormContext();
	return (
		<button type="submit" disabled={!valid}>
			{children}
		</button>
	);
};

Show an error summary after a failed submit:

const ErrorSummary = () => {
	const { errors, isSubmitted } = useFormContext();
	if (!isSubmitted || !errors.length) return null;
	return (
		<ul>
			{errors.map((error) => (
				<li key={`${error.field}-${error.keyword}`}>
					{`${error.field}: ${error.message}`}
				</li>
			))}
		</ul>
	);
};

Use them anywhere inside the <Form>:

<Form data={formData} onChange={handleChange} onSubmit={handleSubmit} schema={demoSchema}>
	<ErrorSummary />
	<Field name="email" value={formData.email} />
	<FieldError name="email" />
	<SubmitButton>Submit</SubmitButton>
</Form>

TypeScript

The library is fully typed — .d.ts declarations ship with the package, no @types/... required.

Most relevant exported types:

import type {
    FormProps,         // generic: FormProps<T, C> — T = data shape, C = wrapper element
    FieldProps,        // generic: FieldProps<C> — C = underlying component
    FieldErrorProps,   // generic: FieldErrorProps<C>
    FormContextValue,
    FormattedError,
    ErrorMessageFn,
    ErrorMessagesMap,
} from 'react-jsonschema-form-validation';

Examples

We’ve got many examples, from the most simple to the most advanced.

Live examples are available : here

Documentation

📃 Check out our documentation : here

Changelog

See CHANGELOG.md for release notes.

Licence

MIT

About us

📬 contact : contact@53js.fr

follow us : @53jsdev

github repos : /53js

🚀 website : 53js.fr