Skip to content

MAIF/react-forms

Repository files navigation

@maif/react-forms

@maif/react-forms is a React form builder with batteries included. You just have to define a form flow and a JSON schema to render stylish, highly customizable and fully managed forms.

@maif/react-forms use yup and react-hook-form to build its forms

Playground

If you think that an example will explain it better, you can go right now to our react-forms playground to discover a couple of examples with an inline editor.

Install

npm install @maif/react-forms

or

yarn add @maif/react-forms

Usage

You must define a form flow (this is just a javascript array which represents the rendering order of the form fields) and a schema (that defines all fields of your form with types or constraints for example) You can import the default stylesheet or use your own style by using default classname or passing a classname in your schema.

example

import { Form, type, format, constraints } from '@maif/react-forms'
import '@maif/react-forms/lib/index.css'

export const Example = () => { 
  const schema = {
    age: {
      type: type.number,
      label: 'age',
      placeholder: 'Your age',
      help: "Just your age",
      className: "input-number",
      constraints: [
        constraints.required("your age is required"),
        constraints.min(18, 'You must be an adult'),
        constraints.integer('half years are not accepted'),
      ]
    }, 
    name: {
      type: type.string,
      disabled: true,
      label: 'name',
      placeholder: 'your name',
      constraints: [
        constraints.required('Your name is required')
      ],
    }
  }

  const flow = ['name', 'age']

  return (
    <Form
        schema={schema}
        flow={flow}
        onSubmit={item => console.log({ item })}
      />
    )
}

schema properties

  • type (required): the type of value. It provided by the imported object type. It could be just string like string, number, bool, object, date or file

  • format: Over the type you can display a special format for the field. It is provided by the imported object format or just a string

    • select: display a react-select field with provided options
    • buttonsSelect: display a buttons group, drawn with the same options than the format select. You can add the property isClearable as boolean to props to add the ability to unselect value. This format also works to display two buttons instead of a switch for type.bool (add props truelabel & falseLabel to customize buttons display).
    • code: if the type is string, display a code input (draw with react-ace) (add a props.setRef to get ace editor ref)
    • singleLineCode: renders text input with syntax highlighting (draw with react-ace) (add a props.setRef to get ace editor ref)
    • markdown: if the type is string, display a markdown input. You can add buttons into toolbar by inject a JSX.element thanks to the actions property from props taking on params the insert function.
        {
          text: {
            type: type.string,
            format: format.markdown,
            props: {
              actions: (insert) => <button type="button" onClick={() => insert("foo")}>inject foo</button>
            }
          }
        }
    • textarea: if the type is string, display a textarea
    • email: if the type is string, display an email input
    • password: if the type is string, display a password input
    • hidden: if the type is string, add an hidden input in your form
    • form: if the type is object, display a form in your form draw with given schema and flow. The form drawn is collapsable, you can choose which field will be visble or not by setting up the visibleOnCollapse props on subschema element properties. collapsable can be a boolean or a function to render JSX (with rawValues, value & getValue params)
        {
          collapsable: ({rawValue, value, getValue}) => <span>{value.firstname} {value.name}</span>
        }
  • array: boolean value to display multiple fields with "add" and "remove" buttons (false by default). You can define addabledefaultvalue to ensure the default value for the new input.

  • createOption: if select format is choosen, createOption property is to render a Creatable component

  • onCreateOption: if select format is choosen, onCreateOption property is a function called before new option creation

    {
      onCreateOption: (option) => myFunction(option)
    }
  • isMulti: if select format is choosen, isMulti property is to render a multiselect component

  • defaultKeyValue: if object format is choosen, this default key/value is set for all added entries

  • visible: a boolean or functional boolean option to hide/display form field.

  • disabled: A boolean or functional boolean option to enable/disable form field

  • label: A string or functional string to labelize of form field (you can pass null to not render a label)

  • placeholder: the placeholder of form field

  • defaultValue: A default value to fill field by default

  • help: the text display hover a help button

  • className: you can customize classnames of field,

  • style: to styling a field, you can provide a json object with css

  • onChange: a callback function to the value change.

    ({rawValues, value, setValue}) => if (value.length) { setValue('entry', false) }
  • onAfterChange: a callback function to the value change.

    ({entry, value, rawValues, previousValue, getValue, setValue, onChange, informations}) => {
      const otherEntryVal = getValue('otherEntry')
      console.debug({entry, value, rawValues, otherEntryVal})
      setValue('anotherEntry', v + 1)
      const {path, parent, index} = informations
      console.debug({path, parent, index})
    }

    where :

    • entry [string] is the updated entry of schema
    • value [any] is the actual value of your entry
    • previousValue [any] is the previous value of your entry
    • rawValues [any] is the actual value of entire form
    • getValue [function] is a function to get value of a form entry
    • setValue [function] is a function to set value of a form entry
    • onSave [function] is a function to update actual entry
    • informations [Information] is an object to get informations about the actual entry (path, index and parent informations)
  • render: a function to completely custom the rendering of form field

    ({rawValues, value, onChange, error, setValue, parent}) => <input type="text" className="is-invalid" value={value} onChange={e => onChange(e.target.value)} />

    You can use the parent field, to change a field of the current element (very useful when you have a list of forms, and you want to change a field different from your current element)

    ({ setValue, parent }) => <input onChange={e => setValue(`${parent}.anotherField`, e.target.value)}>
  • itemRender: a function to completely custom the rendering of form field items (Just in case of array is true)

    ({rawValues, value, onChange, error}) => <input type="text" className="is-invalid" value={value} onChange={e => onChange(e.target.value)} />
  • props: a json object merged with default props. For exemple, if format select is setup, you can add all props to customize react-select

  • options: An array of options for the select field (if format select is setup)

  • optionsFrom: this property is setup to get options with a promise (if format select is selected). It offer multiple possibility to get its:

    • A url to fetch array of options for the select field. The array can be transform by the transformer provided
    • A promise returning an array, which can be transform by the transformer provided
    • a function returning a promise (see up) or directly an array of value
  • transformer: A function to transform options to a valid format for react select, by default the code try to do it himself.

    {
      transformer: (value) => ({label: value.name, value: value.age})
    }
  • schema: a sub schema for the object value. this schema can contains constraints

  • conditionalSchema: an object to conditionnaly render an object value. Contains a ref to test as string and a switch as an array of object which contains default for a boolean options to set default value, condition as a function to run or just a value to test against ref value and schema and flow to draw the object value.

    {
      type: {
        type: type.string,
        format: format.select,
        defaultValue: "mammal",
        options: ["mammal", "fish"]
      },
      place: {
        type: type.object,
        format: format.form,
        conditionalSchema: {
          ref: 'type',
          switch: [
            {
              default: true,
              condition: ({rawValues, ref}) => ref === "mammal",
              schema: {
                continent: {
                  type: type.string,
                },
                country: {
                  type: type.string,
                }
              }
            },
            {
              condition: "fish",
              schema: {
                ocean: {
                  type: type.string,
                },
                deepness: {
                  type: type.number,
                }
              }
            },
          ]
        }
      }
    }
  • constraints: a JSON array of constraints. see constraints section

  • arrayConstraints: a JSON array of constraints apply to the entire array (use constraints to apply constraints to inner fileds of array). see constraints section

  • deps: In case of the entry need some form values, thestepwill be re-render after all change. To avoid some perf issues, it would be nice to declare a deps array to listen. The array must contains the root path

    const schema = {
      one: { type: type.string },
      foo: {
        type: type.object,
        format:format.form,
        schema: {
          foo: { type: type.string },
          bar: { type: type.string },
        }
      },
      two: { type: type.string },
      three: {
        type: type.string,
        deps: ['one', 'foo.bar'],
        visible: ({ rawValues }) => rawValues.one === '1' || rawValues.foo.bar === 'foo'
      },
    }
  • item: In case of the entry is an array, some properties will be applied to the array step but not to the sub-items. In this particular case, you can use item property to add following properties (with same signature than the schema properties) to sub-items:

    • disabled
    • visible
    • label
    • onChange
    • onAfterChange
    • render
    • array

Functional properties

Some schema properties can be basic value or function which return basic value. This function has an object as param with these following properties :

  • rawValues: the actual value of antire form
  • value: the actual value of the actual entry
  • error: the error of the entry (undefined if there is no error)
  • informations: an object to get informations about the actual entry (path, key, index and parent informations)
  • getValue: a function to get value of a form entry

Form properties

  • schema (required): the form schema
  • flow (optional): the flow. The order of the schema fields by key (Just a javascript array of strings). Fields can be group on collapsable item, in this case, you can add an object with label, flow and a boolean collapse property. Collapse item can draw inline form thanks to boolean inline property
  • onSubmit (required): a function run on the form submission (in case if the form is valid )
  • value (optional): default value
  • inputWrapper (optional): A custom component to wrap all input of the form
  • className (optional): A custom class name for the form
  • footer (optional): a component to override the footer provided
{({ reset, valid }) => {
            return (
              <div className="d-flex justify-content-end">
                <button className="btn btn-primary m-3" onClick={reset}>reset</button>
                <button className="btn btn-success m-3" onClick={valid}>accept</button>
              </div>
            )
          }}
  • options (optional): an object to put some options for your form. see Form options for more informations.

Form options

  • httpClient: a function to override the basic fetch used by react-forms to get async values (for optionsFrom)
httpClient = {(url, method) => fetch(url, {
  method,
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'X-foo': 'bar'
  }
})} 
  • watch: a boolean to activate the automatic log of form value. A function can be set up to override the default logger.
  • autosubmit: a boolean to activate the automatic run of the onSubmit form properties on every change of values.
  • showErrorsOnStart: a boolean to display constraints messages, not in error color, when starting the form.
  • actions: an object to parameter footer buttons key. By default just submit button is displayed. Allow to change button label for the following actions:
    • reset
    • cancel
    • submit
    • add
<Form 
  options={
    autosubmit: true,
    showErrorsOnStart: true,
    actions={
        reset: {
          display: false (value by default)
          label: 'reset' (value by default)
        },
        submit: {
          display: true (value by default)
          label: "save" (value by default)
        }
      }
  }
/>

constraints

Possible constraints are provided by import constraints from @maif/react-form or can be wrotes on json By default all fields of the form are nullable (they can accept null or undefined value).

mixed

the following methods works for all type types of value.

constraints.required(message?:string)

Mark the value as required, which will not allow undefined or null as a value. The provided message (or default message) will be display under form field as an error if the value is missing.

constraints.required("this field is required")
{type: 'required', message: "this field is required"}

constraints.test(name: string, message?:string, test: (val: any) => boolean | Promise<boolean>)

Adds a test function to the validation chain. The test must provide a name, an error message and a validation function that takes in entry the current value and must return a boolean result. The test can return a promise that resolve a boolean result

constraints.test("name", 'not fifou', value => value === 'fifou')
{type: 'test', name: "name", message: 'not fifou', test: value => value === 'fifou'}

constraints.when(ref: string, test: (val: any) => boolean, then: any = [], otherwise: any = [])

Adjust field constraints based on a sibling fileds given by ref. You can provide a matcher function with test. Then provides the true constraints and otherwise the constraints for failure condition

constraints.when(
  'isBig', 
  (isBig) => !!isBig, 
  [constraint.count(5)],
  [constraint.count(1)],)
{
  type: 'when', 
  ref: 'isBig', 
  test: (isBig) => !!isBig,
  then: [constraint.count(5)],
  otherwise: [constraint.count(1)],
}

constraints.oneOf(arrayOfValues: any[], message?:string)

Whitelist a set of values and display an error under field if the provided value is not contains in this set. Values can also be a reference of value

constraints.oneOf(['foo', 'bar'], 'not foo or bar :(')
{type: 'oneOf', arrayOfValues: ['foo', 'bar'], message: 'not foo or bar :('}

constraints.ref(ref:any)

Some constraints accepts reference as property. This methods create a reference to another field. Refs are evaluated in the proper order so that the ref value is resolved before the field using the ref (be careful of circular dependencies!).

 const schema = {
   dad_age: { type: type.number},
   son_age: {
       type: type.number,
       constraints: [
         constraints.lessThan(constraints.ref('dad_age'), 'too old')
       ]
     }
 }
const schema = {
    dad_age: { type: 'number'},
    son_age: {
        type: 'number',
        constraints: [
          {type: 'lessThan', ref: { ref: 'dad_age' }, message: 'too old !'}
        ]
      }
  }

string

the following methods works for string values. All methods for mixed are available.

constraints.url(message?:string)

Validate that the provided value matches an url pattern via regexp and display an error if the result id false.

 constraints.oneOf(['foo', 'bar'], 'not foo or bar :(')
{type: 'oneOf', arrayOfValues: ['foo', 'bar'], message: 'not foo or bar :('}

constraints.email(message?:string)

Validate that the provided value matches an email pattern via regexp and display an error if the result id false.

{type: 'email', message: 'not an email'}

constraints.uuid(message?:string)

Validate that the provided value matches an uuid pattern via regexp and display an error if the result id false.

{type: 'uuid', message: 'not an uuid'}

constraints.matches(regexp: RegExp, message?:string)

Test if value matche the provided regexp and display an error if the result id false.

constraints.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$^+=!*()@%&]).{8,1000}$/, errorMessage)
{type: 'matches', regexp: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$^+=!*()@%&]).{8,1000}$/, message: 'error'}

string or number

the following methods works for string or number values. All methods for mixed are available.

constraints.min(ref: number | Reference<number>, message: string)

Set the minimum value allowed and display an error if the provided value (for a number) or the length of the string is bigger. Provided value can be a number or a reference to a number

{type: 'min', ref: 1, message: 'too small'}

constraints.max(ref: number | Reference<number>, message: string)

Set the maximun value allowed and display an error if provided value (for a number) or the length of the string is smaller. value can be a number or a reference to a number

{type: 'max', ref: 5, message: 'too high'}

number

The following methods works for number values. All methods for mixed are available.

constraints.positive(message?:string)

The value must be a positive number and display an error if it's not.

{type: 'positive', message: 'positive please'}

constraints.negative(message?:string)

The value must be a negative number and display an error if it's not.

{type: 'negative', message: 'negative please'}

constraints.integer(message?:string)

The value must be aa integer number and display an error if it's not.

{type: 'integer', message: 'integer please'}

constraints.lessThan(ref: number | Reference<number>, message: string)

Set the maximun value allowed and display an error if provided value (for a number) or the length of the string is smaller. value can be a number or a reference to a number

{type: 'lessThan', ref: 5, message: 'less please'}

constraints.moreThan(ref: number | Reference<number>, message: string)

Set the minimum value allowed and display an error if provided value (for a number) or the length of the string is bigger. value can be a number or a reference to a number

{type: 'moreThan', ref: 5, message: 'more please'}

array

the following methods works for basic types if the format is define to array. All methods for mixed are available.

constraints.length(value: number | Reference<number>, message?:string)

Set the length of the array and display an error if it's different. value can be a number or a reference to a number

{type: 'length', ref: 5, message: 'this array must have 5 elements'}

date

the following methods works for date values. All methods for mixed are available.

file

the following methods works for file values. All methods for mixed are available.

constraints.supportedFormat(arrayOfValues: string[], message?:string)

Whitelist a set of supported format for the provided file and display an error under field if the format is not contains in this set.

{type: 'supportedFormat', arrayOfValues: ['jpg', 'jpeg', 'png'], message: 'unsupported format'}

constraints.unsupportedFormat(arrayOfValues: string[], message?:string)

Whitelist a set of unsupported format for the provided file and display an error under field if the format is contains in this set.

{type: 'unsupportedFormat', arrayOfValues: ['jpg', 'jpeg', 'png'], message: 'unsupported format'}

constraints.maxSize(value: number, message?:string)

Set the maximun value allowed for the file size and display an error if the size of provided file is bigger.

{type: 'maxSize', message: 'file size too big'}