React Form Renderer
Rendering forms from data structures
Table of contents
Instalation
npm install --save @data-driven-forms/react-form-renderer
or
yarn add @data-driven-forms/react-form-renderer
Getting started
React form renderer is a component designed for ManageIQ and Insighs projects that takes json form definitions and renders them into react components. It uses React final form for the form state management. It is highly recommended to check their documentations first to fully understand how the data-driven-forms libraries work.
Code examples:
import FormRender from '@data-driven-forms/react-form-renderer';
const DataDrivenForm = () => (
<FormRender
formFieldsMapper={formFieldsMapper}
layoutMapper={layoutMapper}
schema={schema}
onSubmit={...}
onCancel={...}
onReset={...}
canReset
/>
);
There are several required props that must be passed to the component. Check the examples below to learn how it works.
Form schemas
There are currently 3 schema definitions you can use to define your forms. With the intention to provide additional customization in the future. Currently supported schemas are:
Default schema
This is the default schema that is used directly for rendering the form. All other schema types are parsed to this one. This gives the option to write your custom parser that transforms any of your existing definitions into the default one, and use this renderer.
The default schema is also very extensible. There is only a few requirements for the format. Most of the attributes are meta information and their shape is based upon your form components.
import { componentTypes, validatorTypes } from '@data-driven-forms/react-form-renderer';
const schema = {
title: 'My form title',
description: 'My form description',
fields: [{
component: componentTypes.TEXT_FIELD,
name: 'first-name',
label: 'First name'
validate: [{
type: validatorTypes.REQUIRED,
message: 'First name is required'
}, {
type: validatorTypes.MIN_LENGTH,
treshold: 3,
message: 'First name must be at least 3 characters long'
}]
}, {
component: componentTypes.TEXT_FIELD,
type: 'password',
name: 'password',
label: 'password',
}]
}
Example above shows definition of a very simple form with two form fields and a validation. We will now take a closer look at its attributes.
default-schema-attributes
name | data type |
---|
title? | string |
description? | string |
fields | Array of Objects |
Detailed descriptions of each attribute is below.
title?: string
Attribute defining form title.
description?: string
Attribute defining form description.
fields: Array.<Object>
Array that contains field definitions.
Fields
This is the main data structure that holds definitions of all of the form fields. It is designed to match React rendering process. It must follow this rule:
const fields = [{...}, [{...}, {...}], {...}, {...}, [[[{...}]]]]
In human language, items of field array must be either objects, where each object represents one formField (React component), or array of objects, which are form fields as well. This rule allows the component to render all the fields in one cycle with minimal code branching.
The structure of a single object is following:
field attributes
There are listed all field (items of the fields
array) attributes that are defined by the default schema. Any other attributes given to field object are automatically passed to the specified component.
Detailed descriptions of each attribute is below.
name | type |
---|
component | string |
name | string |
validate? | Array of Objects |
condition? | Object |
dataType | string |
? | bool |
SubForm only | |
fields | Array (only for SUB_FORM ) |
title? | string |
description? | string |
import { componentTypes, validatorTypes } from '@data-driven-forms/react-form-renderer';
const field = {
component: componentTypes.TEXT_FIELD,
name: 'first-name',
label: 'First name',
...
}
Note that the field structure may vary based on your component implementation. There are few required attributes and most of them do not have to match the given types. Most of them are based on used form components.
component: string
Unique identifier of the component. Final component will be picked based on this key. There are several pre-defined constants identifying the most common components for ManageIQ and Insights apps.
import { componentTypes } from '@data-driven-forms/react-form-renderer';
componentTypes = {
TEXT_FIELD: 'text-field',
TEXTAREA_FIELD: 'textarea-field',
FIELD_ARRAY: 'field-array',
SELECT_COMPONENT: 'select-field',
FIXED_LIST: 'fixed-list',
CHECKBOX: 'checkbox',
SUB_FORM: 'sub-form',
RADIO: 'radio',
TABS: 'tabs',
TAB_ITEM: 'tab-item',
DATE_PICKER: 'date-picker',
TIME_PICKER: 'time-picker',
TAG_CONTROL: 'tag-control',
SWITCH: 'switch',
TEXTAREA: 'textarea-field',
SELECT: 'select-field',
}
We are not limited by these component types. You can add your own type or use only few of them or combination of both. More detailed explanation of how this impacts the rendered form can be found here.
name: string
This is traditional html5 name attribute for input elements.
label
Label for form field. The type is based on your component definition.
validate: Array?<Object>
Array of validation definitions. These are limited by the form renderer (Might be configurable in future).
If you want to use out of the box validation, you must use this format:
const validate = [{
type: string,
message: string?,
...
}]
Each validator type has additional configuration options in addition to custom error message:
import { validatorTypes } from '@data-driven-forms/react-form-renderer';
validatorTypes = {
REQUIRED: 'required-validator',
MIN_LENGTH: 'min-length-validator',
MIN_ITEMS_VALIDATOR: 'min-items-validator',
MIN_NUMBER_VALUE: 'min-number-value',
MAX_NUMBER_VALUE: 'max-number-value',
PATTERN_VALIDATOR: 'pattern-validator',
}
const validate = [{
type: validatorTypes.REQUIRED,
message: 'This is custom error message'
}, {
type: validatorTypes.MIN_LENGTH,
treshold: integer
}, {
type: validatorTypes.MIN_ITEMS_VALIDATOR,
treshold: integer
}, {
type: validatorTypes.MIN_NUMBER_VALUE
value: integer
}, {
type: validatorTypes.MAX_NUMBER_VALUE
value: integer
}, {
type: PATTERN_VALIDATOR,
pattern: string
showPatter: bool?
}]
Validation functions are triggered only when field has a value with exception of required validator.
dataType: string?
Adds field validation based on the value data type.
import { componentTypes } from '@data-driven-forms/react-form-renderer';
const field = {
component: componentTypes.TEXT_FIELD,
name: 'number',
type: 'number',
label: 'Integer number',
dataType: 'integer',
}
There are currently four defined data types:
['integer', 'number', 'bool', 'string']
#### `condition: Object?`
Condition is used to define condition fields. For instance, field **A** should render only when field **B** has value **Foo**.
```javascript
import { componentTypes } from '@data-driven-forms/react-form-renderer';
const fields = [{
component: componentTypes.TEXT_FIELD,
name: 'Foo'
}, {
component: componentTypes.TEXT_FIELD,
name: 'Bar'
condition: {
when: 'Foo',
is: 'Show Bar field',
}
}]
Sometimes you might want to show field when it's matching multiple values:
import { componentTypes } from '@data-driven-forms/react-form-renderer';
const fields = [{
component: componentTypes.TEXT_FIELD,
name: 'Foo'
}, {
component: componentTypes.TEXT_FIELD,
name: 'Bar'
condition: {
when: 'Foo',
is: ['Show Bar field', true, 123, 'Or now'],
}
}]
In example above, field Bar
will appear when fields Foo
value is Show bar field
, true
, 123
or Or now
.
Other attributes
Any other attributes will be passed to the component matching the component
identifier.
For examples definition of select component might look something like this:
import { componentTypes } from '@data-driven-forms/react-form-renderer';
const field = {
component: componentTypes.SELECT_COMPONENT,
name: 'color'
label: 'Choose your favorite color'
options: [{
label: 'Red',
value: 'red'
}, {
label: 'Blue',
value: 'blue',
}, {
label: 'Green',
value: 'sneaky-red',
}]
}
Remember that the components define the interface. If your label is an image, pass the image source with isImage flag maybe and handle rendering in the component.
Field array and Fixed list
TO DO add documentaion here
Component Mapping
As it was already mentioned, you can define your own components for rendering. In fact, you have to define them because the Form Renderer does not know anything about them. This way, the renderer is universal and can be used with any component library or your custom components. It is also very easy to swap the look of the form without any changes to the format!
We also understand that writing form components from the scratch might not be very user friendly. ManageIQ and Insights are using patternfly style patterns, so you can get inspired with Patternfly 3 and Patternfly 4 mappers.
Form renderer requires two different mappers. Layout mapper and Form FieldsMapper.
Layout mapper
Component inside this mapper influence the layout of the form. Now compared to the Form Fields mapper, we have to be very strict because we cannot define our own elements. Layout mapper must contain these types components:
import { layoutComponents } from '@data-driven-forms/react-form-renderer';
const layoutComponents = {
[layoutComponents.FORM_WRAPPER]: 'FormWrapper',
[layoutComponents.BUTTON]: 'Button',
[layoutComponents.COL]: 'Col',
[layoutComponents.FORM_GROUP]: 'FormGroup',
[layoutComponents.BUTTON_GROUP]: 'ButtonGroup',
[layoutComponents.ICON]: 'Icon',
[layoutComponents.ARRAY_FIELD_WRAPPER]: 'ArrayFieldWrapper',
[layoutComponents.HELP_BLOCK]: 'HelpBlock'
}
LayoutMapper is just good old javascript object with keys from layoutComponents, and the values are just React components:
FormWrapper
Form wrapper is your form wrapper component. Typically it will be your react version of <form>
tag:
import FormRenderer, { layoutComponents } from '@data-driven-forms/react-form-renderer';
const FormWrapper = ({ children }) => <form>{children}</form>
const layoutMapper = {
[layoutComponents.FORM_WRAPPER]: FormWrapper,
}
const MyForm = () => (
<FormRenderer
layoutMapper={layoutMapper}
/>
)
Button
Button component will be used for your submit, reset and cancel buttons.
import layoutComponents } from '@data-driven-forms/react-form-renderer';
const Button = ({ label, bsStyle, ...props }) => (
<button
{...props}
style={{ backgroud: bsStyle === primary ? 'blue' : 'initial' }}
>
{label}
</button>
)
const layoutMapper = {
[layoutComponents.BUTTON]: FormWrapper,
}
Col
Col represents wrapper arround one Form Field (hence the name Col). It does not have to mirror bootstrap Col, which is just the name we have decided to go with. If you for instance don't need any Col (or other wrapping) component, and you are handling this inside the actual Field component, you can use <React.Fragment>
component. This way you will not create any element in your DOM. On the other hand, it might be usefull to implement it as a container for your components. Because we can't possibly create layout that suit 100% of our use cases, we can use this wrapper to pass additional styles to field components.
import './form/styles.scss';
const Col = ({ children }) => (
<div className="form-row">{children}</div>
)
FormGroup
Very similar to Col component.
import './form/styles.scss';
const FormGroup = ({ children }) => (
<div className="form-group">{children}</div>
)
ButtonGroup
Wrapper for your form buttons
import './form/styles.scss';
const ButtonGroup = ({ children }) => (
<div className="button-group">{children}</div>
)
Icon, Array Field Wrapper, Help Block
TO DO when array field docs are done
Putting it all together
import FormRenderer, { layoutComponents } from '@data-driven-forms/react-form-renderer';
const layoutMapper = {
[layoutComponents.FORM_WRAPPER]: FormWrapper,
[layoutComponents.BUTTON]: Button,
[layoutComponents.COL]: Col,
[layoutComponents.FORM_GROUP]: FormGroup,
[layoutComponents.BUTTON_GROUP]: ButtonGroup,
[layoutComponents.ICON]: Icon,
[layoutComponents.ARRAY_FIELD_WRAPPER]: ArrayFieldWrapper,
[layoutComponents.HELP_BLOCK]: HelpBlock
}
const MyForm = () => (
<FormRenderer
...
layoutMapper={layoutMapper}
...
/>
)
Form Fields mapper
Unlike the layout components, the form fields are completely customizable, and the implementation is restricted to only one rule. In order to correctly change the form state, you have to use provided input
and meta
props to your input fields. These objects provide functions like onChange
, onBlur
, error messages, valid state and more. Again you should probably read more about that in the React Final Form docs.
In an example below you can see an implementation of a simple input component using both predefined component type and a custom one.
import { componentTypes, validatorTypes } from '@data-driven-forms/react-form-renderer';
const DefaultInput = ({ input, meta, label, name, ...rest }) => {
const id = rest.id || name;
return (
<div className={`form-group ${meta.error ? 'error' : ''}`}>
<label htmlFor={id}>{label}</label>
<input id={id} name={name} {...rest} {...input} />
{meta.error && <span className="error-text">{meta.error}</span>}
</div>
)
}
const CustomInput = ({ FieldProvider, ...rest }) => (
<FieldProvider
{...rest}
component={DefaultInput}
/>
)
const formFieldsMapper = {
[componentTypes.TEXT_FIELD]: DefaultInput,
'custom-input': CustomInput,
}
const schema = {
fields:[{
component: componentTypes.TEXT_FIELD.
name: 'first-name',
label: 'First Name',
type: 'text',
validate: [{
type: validatorTypes.REQUIRED,
}]
}, {
component: 'custom-input'.
name: 'last-name',
label: 'Last Name',
type: 'text',
validate: [{
type: validatorTypes.REQUIRED,
}]
}]
}
What about nesting?
There might be a need to create something like a SubForm in your Forms. We made it possible, and there are two ways to do it.
- Use pre defined
componentTypes.SUB_FORM
component type:
import { componentTypes } from '@data-driven-forms/react-form-renderer';
const schema = {
fields: [{
component: componentTypes.TEXT_FIELD,
name: 'first-name',
...
}, {
component: componentTypes.SUB_FORM,
title: 'Address',
description: 'Please provide us with your shipping address.',
name: 'address',
fields: [{
component: componentTypes.TEXT_FIELD,
name: 'city',
label: 'City',
validate: [...],
...
}]
}]
}
This renders and bundles fields and their values inside the SUB_FORM
component. When the form is submitted, the values object will look something like this:
{
"first-name": "Bob",
"address": {
"city": "Prague"
...
}
}
- Custom component
Again, the
SUB_FORM
component type might not be the correct solution for your use case. You can always create your own. Let's use the same schema as in example above, but only with one change:
component: componentTypes.SUB_FORM, -> component: 'custom-sub-form'
If you use custom type, in addition to all the attributes in the field specification, you will also receive formOptions
props. Form options contains some global form properties, and most importantly, there is a renderForm
function. It does exactly what it says. It is the main loop that renders the form. It accepts two arguments, fields
and formOptions
:
const CustomSubForm = ({ formOptions, fields, ...rest }) => (
<SubFormWrapper>
{formOptions.renderForm(fields, {
...formOptions,
someExtendingAttribute: 'foo'
})}
</SubFormWrapper>
)
FormOptions are passed to every field (default and custom). If you extend it, the child of the sub form will receive your modified formOptions. You can even change te rendering function if you wish.