React components for Rjv (Reactive JSON Validator)
npm install rjv-reactReact components for creating forms powered by Rjv (Reactive JSON Validator)
- works with any type of data
- uses the JSON Schema to validate data
- customizable validation messages
- supports message localization
- works in web and native environments
- suitable for any UI components library
#### Contents
- Install
- Overview
- Guide
- 3rd Party Bindings
- Examples
- API
- Components
- Hooks
yarn add rjv rjv-react
`Overview
This library is a set of components for creating simple or complex forms in React JS applications.
It uses the Rjv JSON validator,
which provides simplified standard JSON schema keywords adopted for the front-end needs,
as well as some additional keywords like validate or resolveSchema that allow you to create validation rules at runtime.Guide
- Sign up form
- Higher-Order Fields (HOF)$3
This example shows how to create a user registration form using the Material UI component library.
> The rjv-react can be used with any component libraries, the Material UI library is selected for example only.`typescript jsx
import React, { useState } from "react";
import { Button, TextField } from "@material-ui/core";
import { FormProvider, Field, Submit, Watch } from "rjv-react";export default function SignUpForm() {
const [initialData] = useState({});
return (
Sign Up Form
{/ create form and provide initial data /}
{/ create a field and attach validation rules for the "email" property /}
path={"email"}
schema={{ default: "", presence: true, format: "email" }}
render={({ field, state, inputRef }) => {
const hasError = state.isValidated && !state.isValid;
const errorDescription = hasError
? field.messageDescription
: undefined;
return (
inputRef={inputRef}
label="Email"
onChange={(e) => {
field.value = e.target.value;
}}
onBlur={() => field.validate()}
value={field.value}
required={state.isRequired}
helperText={errorDescription}
error={hasError}
/>
);
}}
/>
{/ create a field and attach validation rules for the "password" property /}
path={"password"}
schema={{ default: "", presence: true, minLength: 6 }}
render={({ field, state, inputRef }) => {
const hasError = state.isValidated && !state.isValid;
const errorDescription = hasError
? field.messageDescription
: undefined;
return (
inputRef={inputRef}
label="Password"
onChange={(e) => {
field.value = e.target.value;
}}
onBlur={() => field.validate()}
value={field.value}
required={state.isRequired}
helperText={errorDescription}
error={hasError}
type="password"
/>
);
}}
/>
{/ watch for changes in the "password" field /}
props={["password"]}
render={(password: string) => {
// create a field and attach validation rules for the "confirmPassword" property
return (
path={"confirmPassword"}
schema={{
default: "",
presence: !!password,
const: password,
error: "Confirm your password"
}}
// notice that the validation rules above depends on the "password" field value
dependencies={[password]}
render={({ field, state, inputRef }) => {
const hasError = state.isValidated && !state.isValid;
const errorDescription = hasError
? field.messageDescription
: undefined;
return (
inputRef={inputRef}
label="Confirm password"
onChange={(e) => {
field.value = e.target.value;
}}
onBlur={() => field.validate()}
value={field.value}
required={state.isRequired}
helperText={errorDescription}
error={hasError}
type="password"
/>
);
}}
/>
)
}}
/>
{/ Submit form /}
onSuccess={(data) => console.log("Form data:", data)}
render={({ handleSubmit }) => (
)}
/>
);
}
`Checkout this example in CodeSandBox
$3
As the rjv-react library works in any environment (web/native) and with any 3d party UI
components framework, it would be better to create a set of higher-order field components to simplify form development.
For example, we can create a couple of higher order components such as Form and TextField that combines the rjv-react and Material UI components together.components/Form.js
`typescript jsx
import React, { useCallback } from "react";
import { FormProvider, useFormApi } from "rjv-react";type FormProps = {
// initial form data
data: any;
// note that onSuccess function could be async
onSuccess: (data: any) => void | Promise;
children: React.ReactNode;
// focus first error field after the form submitting
focusFirstError?: boolean;
};
function Form(props: FormProps) {
const { data, onSuccess, focusFirstError = true, ...restProps } = props;
const { submit } = useFormApi();
const handleSubmit = useCallback(
(e) => {
e.preventDefault();
submit(onSuccess, (firstErrorField) => {
if (focusFirstError) {
firstErrorField.focus();
}
});
},
[submit, onSuccess, focusFirstError]
);
return
;
}export default function FormWithProvider(props: FormProps) {
return (
);
}
` components/TextField.js
`typescript jsx
import React from "react";
import { types } from "rjv";
import { useField } from "rjv-react";
import {
TextField as MuiTextField,
TextFieldProps as MuiTextFieldProps
} from "@material-ui/core";export interface TextFieldProps
extends Omit<
MuiTextFieldProps,
"name" | "value" | "onFocus" | "onChange" | "onBlur"
> {
// path to the data property
path: string;
// validation schema
schema: types.ISchema;
// specify when the field should be validated
validateTrigger?: "onBlur" | "onChange" | "none";
// remove error when the field's value is being changed
invalidateOnChange?: boolean;
}
export default function TextField(props: TextFieldProps) {
const {
path,
schema,
helperText,
required,
error,
children,
validateTrigger = "onBlur",
invalidateOnChange = true,
...restProps
} = props;
const { field, state, inputRef } = useField(path, schema);
const hasError = error ?? (state.isValidated && !state.isValid);
const errorDescription = hasError ? field.messageDescription : undefined;
return (
{...restProps}
inputRef={inputRef}
onFocus={() => field.touched()}
onChange={(e) => {
field.dirty().value = e.target.value;
if (
invalidateOnChange &&
validateTrigger !== "onChange" &&
state.isValidated
) {
field.invalidated();
}
if (validateTrigger === "onChange") field.validate();
}}
onBlur={() => {
if (validateTrigger === "onBlur") field.validate();
}}
value={field.value}
required={required ?? state.isRequired}
error={hasError}
helperText={errorDescription ?? helperText}
>
{children}
);
}
`
Now the "Sign Up" form might look like:components/SignUpForm.js
`typescript jsx
import React, { useState } from "react";
import { Button } from "@material-ui/core";
import { Watch } from "rjv-react";
import Form from "./Form";
import TextField from "./TextField";export default function SignUpForm() {
const [initialData] = useState({})
return (
)
}
`Checkout this example in CodeSandBox
3rd Party Bindings
- Ant DesignExamples
- Conditional form
- Dynamic items form
- Nested form
- Multi-step form
- Reset form data and state
- Inline validation
- Customize error messages
- Multi-language formAPI
- FormProvider - create a form.
- Field / useField - create a field with some validation rules and get access to the state and API objects of the field.
- FieldArray / useFieldArray - render the list and get the API to work with it.
- Form / useForm - get access to the state and API objects of the form.
- FormStateUpdater - update the
isValid state of the form when data changes
- OptionsProvider - setup default form options like error messages, intl, validation options
- Watch / useWatch - watch for property / properties change
- Property / useProperty - watch for the particular property change and get the API to change that value
- Scope - change the scope against which the relative paths are resolved
- VisibleWhen - show some content when the provided schema is valid
- Submit - submit form and get access to the state and api objects of the form
- Errors / useErrors - get error messagesComponents
- OptionsProvider
- FormProvider
- CatchErrors
- Errors
- Form
- FormStateUpdater
- Field
- FieldArray
- Scope
- Submit
- Watch
- Property
- VisibleWhen
$3
Provides default form options. This options will be used as default at the FormProvider component level.
One of the main purposes of the OptionsProvider component is setting up localized validation messages of the application.Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
children* | ReactNode | undefined | content
coerceTypes | boolean | false | enable coerce types feature
removeAdditional | boolean | false | enable remove additional properties feature
validateFirst | boolean | true | stop validating schema keywords on the first error acquired
errors | { [keywordName: string]: string } | {} | custom error messages
warnings | { [keywordName: string]: string } | {} | custom warning messages
keywords | IKeyword[] | [] | additional validation keywords
descriptionResolver | (message: ValidationMessage) => string | (message) => message.toString() | function resolving validation message descriptions$3
Creates a form and provides initial form data.
There are some tips to notice:
- The initial data can be of any type, so it doesn't necessarily have to be objects.
- When your initial data is changed, the state of the form is reset, so you have to memoize your initial data objects or arrays to control the form resetting.
- FormProvider doesn't affect the provided data, it uses the cloned instance.
- FormProvider can be nested.Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
children* | ReactNode | undefined | content
data | any | undefined | the initial form data
ref | React.RefObject | undefined | returns an object containing the form API$3
Provides validation errors context. This component collects errors from all fields enclosed within.> Note that the
FormProvider component already provides an CatchErrors for the entire form,
> but you are able to wrap some particular fields with CatchErrors to get errors only for that fields.Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
children* | ReactNode | undefined | content$3
Passes errors caught by the closest CatchErrors component to the render function.Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
render* | (errors: ValidationErrors) => ReactNode | undefined | a function rendering errors. See ValidationErrors$3
Passes FormInfo object to the render function.Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
render* | (formInfo: FormInfo) => ReactElement | undefined | a function rendering a form related UI.#### FormInfo
`typescript
type FormInfo = {
form: FormApi
state: FormState
}
`#### FormApi
`typescript
type FormApi = {
// submits form
submit: (
onSuccess?: (data: any) => void | Promise,
onError?: (firstErrorField: FirstErrorField) => void | Promise
) => void
// validates form data and sets isValid state of the form
sync: () => void
// validates specified field / fields
validateFields: (path: types.Path | types.Path[]) => Promise
// same as validateFields,
// but not affects isValidating and isValidated states of the field
syncFields: (path: types.Path | types.Path[]) => Promise
}
`#### FirstErrorField
`typescript
type FirstErrorField = {
// path of the field where the first error has been acquired
path: string,
// focuses the input control if possible
focus: () => void,
// input control element if it was provided,
// see render function of the Field component
inputEl?: React.ReactElement
}
`#### FormState
`typescript
type FormState = {
isValid: boolean
isSubmitting: boolean
submitted: number
isTouched: boolean
isDirty: boolean
isChanged: boolean
}
`$3
Recalculates the isValid state of the whole form when data changes.
> Should be used only once per form.Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
debounce | number | 300 ms | debounce updates, 0 - without debounce
dependecies | any[] | [] | any external values (react state / store / etc) that affect validation rules of the form$3
The Field component is responsible for rendering the specific data property.
It takes the render function of the field control and invokes it each time the value or validation/UI state of the data property changed.
The field control render function gets a fieldInfo object, using this object it is able to handle these issues:
- Changing value
- Getting/Setting UI state (pristine/touched/dirty/valid etc)
- Validating value
- Rendering the field control with actual validation/UI stateProperties:
Name | Type | Default | Description
--- | :---: | :---: | ---
path* | string| undefined | path to the data property
schema* | Object | undefined | JSON schema is used to validate data property
render* | (fieldInfo: FieldInfo) => ReactNode | undefined | a function rendering the UI of the field. See FieldInfo.
dependecies | any[] | [] | any values that affect validation schema or are used in the validate or resolveSchema keywords, when dependencies are changed the field applies a new validation schema and recalculates the isValid state
ref | React.RefObject | undefined | returns an object containing the field API#### Path
The
rjv-react uses a path to point to a specific data property.
Path is a simple string working like a file system path,
it can be absolute - /a/b/c or relative - ../b/c, b/c.
The numeric parts of the path are treated as an array index, the rest as an object key.
By default, all relative paths are resolved against the root path /.
The Scope component can be used to change the resolving path.
`typescript
type Path = string
`#### FieldInfo
`typescript
type FieldInfo = {
// the current state of the field
state: FieldState
// an API to interact with field
field: FieldApi
// a reference to the input component, later this ref can be used to focus an invalid input component
inputRef: React.RefObject
}
`#### FieldApi
`typescript
type FieldApi = {
// get / set value of the field
value: any
// get data ref to the property
ref: types.IRef
// normalized message of the FieldState.message
messageDescription: string | undefined
// validate field
validate: () => Promise
// focus input element if it was provided
focus: () => void
// mark field as dirty
dirty: () => this
// mark field as touched
touched: () => this
// mark field as pristine
pristine: () => this
// mark field as invalidated
invalidated: () => this
}
`
See IRef, IValidationResult#### FieldState
`typescript
type FieldState = {
isValid: boolean
isValidating: boolean
isValidated: boolean
isPristine: boolean
isTouched: boolean
isDirty: boolean
isChanged: boolean
isRequired: boolean
isReadonly: boolean
message?: ValidationMessage
}
`
See ValidationMessage> Note that you can create multiple fields for the same data property, and these fields will act independently
$3
A useful component to deal with an array of fields. It provides an api for adding / removing fields
and also manages the generation of unique keys for each field.> This component works with arrays of any types - strings, numbers, booleans, objects, arrays.
> As a consequence of the above, this component doesn't need to store generated unique keys in the form data.
Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
path* | string| undefined | specifies data property
render* | (fieldsInfo: FieldArrayInfo) => React.ReactElement | undefined | a function rendering fields of the array. See FieldArrayInfo.
ref | React.RefObject | undefined | returns an object containing the API to add / remove fields#### FieldArrayInfo
`typescript
type FieldArrayInfo = {
items: FieldItem[]
fields: FieldArrayApi
}
`#### FieldItem
The description of the field being rendered.
`typescript
type FieldItem = {
key: string, // unique key of teh field
path: types.Path // the property path of the field
}
`#### FieldArrayApi
An API for adding / removing fields
`typescript
type FieldArrayApi = {
// append data to the end of the array
append: (value: any) => void
// prepend data to the start of the array
prepend: (value: any) => void
// remove data at particular position
remove: (index: number) => void
// clear array
clear: () => void
// remove data at particular position
insert: (index: number, value: any) => void
// swap data items
swap: (indexA: number, indexB: number) => void
// move data item to another position
move: (from: number, to: number) => void
}
`$3
Sets the data scope of the form - all relative paths will be resolved against the nearest scope up the component tree.
> By default FormProvider component sets the root / scope.Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
path* | string | undefined | specifies scope$3
The component based form submitting.Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
render* | (submitInfo: SubmitInfo) => ReactNode | undefined | a function rendering the UI of the submit button, it gets a SubmitInfo object containing a handleSubmit function which should be called to submit a form.
onSubmit | (data: any) => void | undefined | a callback function to be called when the form submission process begins
onSuccess | (data: any) => void | undefined | a callback function to be called after onSubmit if the form data is valid
onError | (firstErrorField: FieldApi) => void | undefined | a callback function to be called after onSubmit if the form data is invalid. See FieldApi
focusFirstError | boolean | true | if "true" tries to focus first invalid input control after the form submission#### SubmitInfo
`typescript
type SubmitInfo = {
handleSubmit: () => void
form: FormApi
state: FormState
}
`
See FormApi, FormState
$3
Re-renders content when the certain fields are changedProperties:
Name | Type | Default | Description
--- | :---: | :---: | ---
render | (...values: any[]) => ReactNode | undefined | a function rendering some UI content when changes occur. The render function gets a list of data values for each observed property. > Note: values of props with wildcards or ** cannot be resolved, they will be undefined
props | Path[] | [] | a list of properties to watch, each property path can be relative or absolute and contain wildcards or *$3
Subscribes to a property changes and passes the property value and value setter function to the render function.Properties:
Name | Type | Default | Description
--- | :---: | :---: | ---
render* | (value: any, setValue: (value: any) => void) => ReactElement | undefined | a function rendering a property related UI.$3
Shows children content when the data is correctProperties:
Name | Type | Default | Description
--- | :---: | :---: | ---
schema* | Object | undefined | schema used to check data property
path | string| '/' | absolute or relative path to data property
useVisibilityStyle | boolean | false | use css visibility style and do not unmount children components
visibleStyles | CSSProperties | undefined | css styles for the visible content
hiddenStyles | CSSProperties | undefined | css styles for the hidden contentHooks
- useForm
- useFormApi
- useFormState
- useField
- useFieldArray
- useProperty
- useErrors
- useWatch
- usePath
- useDateRef
- useValidate$3
This hook combines useFormApi and useFormState hooks together and returns a form info object.
> This hook is used by Form component.$3
Returns a form api object.$3
Returns a form state object.$3
Creates a field with provided schema and returns a field info object.
> This hook is used by Field component.$3
Returns a field array info object.
> This hook is used by FieldArray component.$3
Subscribes to the property changes. Behaves like the useState hook.$3
Returns a list of errors caught by CatchErrors component
> This hook is used by Errors component.#### ValidationErrors
`typescript
type ValidationErrors = { path: string, message: string }[]
``[LICENSE file]: https://github.com/gromver/rjv-react/blob/master/LICENSE