rjv-react

React components for Rjv (Reactive JSON Validator)

Usage no npm install needed!

<script type="module">
  import rjvReact from 'https://cdn.skypack.dev/rjv-react';
</script>

README

rjv-react

React 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

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

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.

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 (
    <div className="sign-up-form">
      <h3>Sign Up Form</h3>

      {/* create form and provide initial data */}
      <FormProvider data={initialData}>
        {/* create a field and attach validation rules for the "email" property */}
        <Field
          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 (
              <TextField
                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 */}
        <Field
          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 (
              <TextField
                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 */}
        <Watch
          props={["password"]}
          render={(password: string) => {
            // create a field and attach validation rules for the "confirmPassword" property
            return (
              <Field
                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 (
                    <TextField
                      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 */}
        <Submit
          onSuccess={(data) => console.log("Form data:", data)}
          render={({ handleSubmit }) => (
            <Button onClick={handleSubmit}>Submit</Button>
          )}
        />
      </FormProvider>
    </div>
  );
}

Checkout this example in CodeSandBox

Higher-Order Fields (HOF)

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

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<void>;
  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 <form {...restProps} onSubmit={handleSubmit} noValidate />;
}

export default function FormWithProvider(props: FormProps) {
  return (
    <FormProvider data={props.data}>
      <Form {...props} />
    </FormProvider>
  );
}

components/TextField.js

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 (
    <MuiTextField
      {...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}
    </MuiTextField>
  );
}

Now the "Sign Up" form might look like:

components/SignUpForm.js

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 (
    <Form data={initialData} onSuccess={(data) => console.log("Form data:", data)}>
      <TextField
        style={fieldStyles}
        path={"email"}
        schema={{ default: "", presence: true, format: "email" }}
        label="Email"
      />

      <TextField
        path={"password"}
        schema={{ default: "", presence: true, minLength: 6 }}
        label="Password"
        type="password"
      />
      <Watch
        props={["password"]}
        render={(password: string) => (
          <TextField
            style={fieldStyles}
            path={"confirmPassword"}
            schema={{
              default: "",
              presence: !!password,
              const: password,
              error: "Confirm your password"
            }}
            dependencies={[password]}
            label="Confirm password"
            type="password"
          />
        )}
      />

      <Button type="submit">Submit</Button>
    </Form>
  )
}

Checkout this example in CodeSandBox

3rd Party Bindings

Examples

API

Components

OptionsProvider

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

See ValidationMessage

FormProvider

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<FormApi> undefined returns an object containing the form API

CatchErrors

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

Errors

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

Form

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

type FormInfo = {
   form: FormApi
   state: FormState
}

FormApi

type FormApi = {
   // submits form
   submit: (
     onSuccess?: (data: any) => void | Promise<void>,
     onError?: (firstErrorField: FirstErrorField) => void | Promise<void>
   ) => void
   // validates form data and sets isValid state of the form
   sync: () => void
   // validates specified field / fields
   validateFields: (path: types.Path | types.Path[]) => Promise<void>
   // same as validateFields,
   // but not affects isValidating and isValidated states of the field
   syncFields: (path: types.Path | types.Path[]) => Promise<void>
}

FirstErrorField

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

type FormState = {
   isValid: boolean
   isSubmitting: boolean
   submitted: number
   isTouched: boolean
   isDirty: boolean
   isChanged: boolean
}

FormStateUpdater

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

Field

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 state

Properties:

Name Type Default Description
path* string undefined path to the data property
schema* Object<JSON Schema> 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<FieldApi> 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.

type Path = string

FieldInfo

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

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<types.IValidationResult>
   // 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

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

FieldArray

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<FieldArrayApi> undefined returns an object containing the API to add / remove fields

FieldArrayInfo

type FieldArrayInfo = {
   items: FieldItem[]
   fields: FieldArrayApi
}

FieldItem

The description of the field being rendered.

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

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
}

Scope

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

Submit

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

type SubmitInfo = {
  handleSubmit: () => void
  form: FormApi
  state: FormState
}

See FormApi, FormState

Watch

Re-renders content when the certain fields are changed

Properties:

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 **

Property

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.

VisibleWhen

Shows children content when the data is correct

Properties:

Name Type Default Description
schema* Object<JSON Schema> 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 content

Hooks

useForm() => FormInfo

This hook combines useFormApi and useFormState hooks together and returns a form info object.

This hook is used by Form component.

useFormApi() => FormApi

Returns a form api object.

useFormState() => FormState

Returns a form state object.

useField(path: string, schema: ISchema, dependencies?: any[]) => FieldInfo

Creates a field with provided schema and returns a field info object.

This hook is used by Field component.

useFieldArray(path: string) => FieldArrayInfo

Returns a field array info object.

This hook is used by FieldArray component.

useProperty(path: string) => [any, (value: any) => void]

Subscribes to the property changes. Behaves like the useState hook.

useErrors() => ValidationErrors

Returns a list of errors caught by CatchErrors component

This hook is used by Errors component.

ValidationErrors

type ValidationErrors = { path: string, message: string }[]

useWatch(...path: string[]) => any[]

Returns an array of values for each observed property. The property path can contain wildcards, but the value of such properties cannot be resolved and will be undefined.

This hook is used by Watch component.

usePath(path: string) => string

If the given path is relative, returns the resolved path against the closest scope, otherwise returns the path as is.

useDateRef(path: string) => IRef

Returns an IRef api object to get and set the value of the property. The path to the property can be absolute or relative.

useValidate() => (path: string | string[]) => Promise<void>

Returns a function that triggers validation of the desired properties. The path to the properties can be absolute or relative.

License

rjv-react is released under the MIT license. See the LICENSE file for license text and copyright information.