@lefapps/forms
v1.8.18
Published
This is a composition based form generator. Every composed form is also [reformed](https://github.com/davezuko/react-reformed), allowing for easy model bindings.
Downloads
59
Readme
LEF Forms (@lef/forms)
LEF/forms is a composition based form generator for react. In short, you can use it to generate complex React forms using simple JS objects and/or JSON. This in turn allows you the option define your form from anywhere, including code, a separate JSON file or a dabatabase (like MongoDB).
Every composed form is also reformed, allowing for easy model bindings.
A composed form requires a Library
that provides a component for every type of element
in your form. These components can be augmented by a DecoratorLibrary
, which consists of so called "decorators" that wrap around components, to modify their behaviour and/or look & feel.
A library of default components and one for decorators is provided. You can extend or modify these default libraries, by adding, removing or replacing components or decorators. This is how you customize behaviour for a specific application.
A form editor is also available, with which you can modify form elements.
Contents
- Modifying libraries
- Editing forms
- Building your own components
- Building your own decorators
- Integrating translations
EasyForm
The easiest way of creating forms is using the EasyForm
.
Let's assume the form is configured with a hardcoded list of elements:
import { EasyForm } from '@lefapps/forms'
const formElements = [
{ name: 'foo', type: 'textarea' },
{ name: 'bar', type: 'text' }
]
const MyForm = new EasyForm().instance()
class Example extends React.Component {
_onSubmit = model => {
// this gets called when the form is submitted
// e.preventDefault() has already been called of course
console.log(model)
}
render () {
return (
<MyForm
elements={formElements}
initialModel={{ bar: 'Example text' }}
onSubmit={this._onSubmit}
>
<button type='submit'>Submit</button>
</MyForm>
)
}
}
Props
| Prop | Required? | Notes | | :-------------------: | :-------: | ------------------------------------------------------------------------------------------------------------------------------ | | elements | yes | array of form elements | | onSubmit | yes | function to call when form gets submittedgets the form model as only parameter | | initialModel | | default form values (in the same format as the form model) | | onStateChange | | perform transormations on the modelgets the model as only parameter, expects a (modified) model to be returned again |
Configuration
The EasyForm
constructor accepts a configuration object with:
library
: a componentLibrary
, which is an extended Map object that holds all form componentsdecorators
: aDecoratorLibrary
, which is an extendedLibrary
that holds decorators
The component library defaults to DefaultComponents
, which is a simple library of "reformed" reactstrap
form components. Similarly, decorators
defaults to DefaultDecorators
.
EasyForm.prototype.instance
is then used to create a React Component
, which you can instantiate with props
. You can supply a config
object to the instance
function, with the following supported fields:
decorators
: an Array with the names of the decorators that you wish to apply. If not supplied, all decorators are applied.components
: an Array of component types that you wish to make available to the form. If not supplied, all components are available. (Note that this is more relevant in theeditor
mode below.)
For example, you might only want to apply the standard FormGroup
decorator:
const MyForm = new EasyForm().instance({ decorators: ['formgroup'] })
Note that the decorators are applied in sequence, either the "natural" sequence in the DecoratorLibrary
or the sequence in the instance
arguments (applied left to right). This is important if you need to be certain of the position of a wrapper in the hierarchy.
Note that the attributes
from the element are applied directly to the Input
component by the Textarea
component. This is an example of a convention from this specific component library. Similarly, name
and type
are applied as you would expect.
Architectural note: you (probably) only need to make one
EasyForm
instance per "type" of form in your application. You can simply reuse it as a component throughout your application.
Elements
| Property | Required? | Default | Type | Notes |
| -------------- | --------------------------------- | ------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| name | yes | | String
| defines the structure in the form model* |
| type | yes | | String
| defines the type of input (see components folder) |
| label | | | String
Object
* | input label |
| options | selectchekbox(-mc)radio | []
| [String]
[Object]
* | available values |
| required | | false
| Bool
| default validation decorator is applied when true
|
| schema | | | String
Object
* | help text when field is invalid |
| dependent | | | Object
| dynamically show or hide element, based on value of other elementneeds dependent decorator |
| layout | | | Object
| config for default layout decoratoruses bootstrap grid |
| attributes | | | Object
| passed attributes are applied directly to input elemente.g.: rows
for textarea |
| key | | | String
| only necessary if multiple elements with the same name are presente.g.: when using dependent fields (React needs different keys) |
Blueprint of an element:
{
"name": "name.supports.nesting",
"type": "text|textarea|select|radio|checkbox|checkbox-mc|divider|infobox",
"label": "LabelText",
"attributes": {
"placeholder": "Placeholder",
"size": 12,
"rows": 5
},
"required": true,
"dependent": {
"on": "dependentOn",
"operator": "in|gt|gte|lt|lte|is|isnt|…",
"values": "value or array of values"
},
"schema": {
"description": "HelpText",
"invalid": "HelpText when invalid"
},
"layout": {
"col": {
"xs": 12,
"md": 6
},
"inline": true
},
"options": ["~red", "~blue"]
}
Model
const elements = [
{ name: 'name' },
{ name: 'address.street' },
{ name: 'address.number' },
{ name: 'address.zip' },
{ name: 'address.city' }
]
const onSubmit = model => {
// model is an object which reflects the structure of the element names
const { name, address } = model
const { street, number, zip, city } = address || {}
/* model = {
* name: 'name',
* address: {
* street: 'street',
* number: 'number',
* zip: 'zip',
* city: 'city' }
* }
*/
}
Modifying libraries
If you wish to modify the standard component and decorator libraries, you can do things like this:
import { withTranslator } from '@lefapps/translations'
const MyFormConfig = new EasyForm()
MyFormConfig.addComponent(name1, component)
MyFormConfig.removeComponent(name2)
MyFormConfig.addDecorator(name3, decorator)
MyFormConfig.removeDecorator(name4)
const MyForm = MyFormConfig.instance()
const MyTranslatedForm = withTranslator(MyForm)
or
const MyDecorators = DefaultDecorators.subset(['formgroup', 'layout'])
const MyComponents = DefaultComponents.subset(['textarea', 'checkbox'])
const MyForm = new EasyForm({
library: MyComponents,
decorators: MyDecorators
}).instance()
See components and decorators for more info.
Editing forms
It's extremely easy to get a form editor for the example form above:
import { withTranslator } from '@lefapps/translations'
const MyFormEditor = new EasyForm().editor()
const MyTranslatedFormEditor = withTranslator(MyFormEditor)
class Example extends Component {
_onSubmit = formElements => {
// this gets called when the form editor is saved
// e.preventDefault() has already been called of course
console.log(formElements)
}
render () {
return (
<MyTranslatedFormEditor
initialModel={formElements}
onSubmit={this._onSubmit}
>
<Button type='submit'>Submit</Button>
</MyFormEditor>
)
}
}
Note that you only need to supply the form elements as the initial model.
Also note that both components and decorators basically carry their own configuration inside the respective libraries, which is used to lay-out the form editor.
Components
You can write components like this:
class TextComponent extends Component {
get type() {
return "text"
}
render() {
const { bindInput, element, attributes: propsAttributes } = this.props
const { name, type, attributes: elementAttributes } = element
return (
<Input type={type} {...bindInput(name)} {...elementAttributes} {...propsAttributes} />
)
}
}
const transform = (element, { translator, model }, saving) => {
// perform mutations of element properties
// when saving or retrieving (saving = true/false)
// Example: see translations for selects
return element // do not forget to return the altered element
}
const config = ({ translator, model }) = return [
{
key: 'name',
name: 'name',
type: 'text',
label: 'Field name', // or translator object { nl: '', en: '' }
attributes: {
placeholder: 'Technical name for field',
// OR
placeholders: {
en: 'Technical name for field',
}
},
required: true,
layout: { col: { xs: 12 } },
},
{
key: 'attributes.placeholder',
name: 'attributes.placeholders',
type: 'text',
label: 'Placeholder',
layout: { col: { xs: 12 } }
}
]
export default TextComponent
export { transform, config }
Note that the config
will determine what can be edited in the form editor.
When adding a component, you can for example do it like this:
const easyForm = new EasyForm()
const path = '../imports/components/TextComponent'
easyForm.addComponent('mytext', {
component: require(path).default,
config: require(path).config
})
You could also directly add the component and its configuration to a Library
.
Decorators
This is where the magic happens. Essentially what we can do is modify the component library, so that a higher order component (the decorator) is in control of the render function. The decorator can e.g. inject props, decide to render something completely different or wrap the component in something.
Let's assume for instance that we would like to wrap every form component in a FormGroup
and add a label if is present in the element configuration. It would look something like this:
const FormGroupDecorator = WrappedComponent => props => (
<FormGroup>
{props.element.label ? (
<Label for={props.element.name}>{props.element.label}</Label>
) : null}
<WrappedComponent {...props} /> // don't forget to "push down" the props into
the wrapped component
</FormGroup>
)
const transform = (element, { translator, model }, saving) => {
// perform mutations of element properties
// when saving or retrieving (saving = true/false)
// Example: see translations for selects
return element // do not forget to return the altered element
}
const config = ({ translator, model }) => [
{
key: 'label',
name: 'label',
type: 'textarea',
label: 'Field label or introduction',
layout: { col: { md: 12 } }
}
]
// Configuration of label is put in front
const combine = _.flip(_.union)
// we're only interested in certain components:
const filter = componentType => _.includes(['textarea', 'text'], componentType)
export default FormGroupDecorator
export { transform, config, combine, filter }
You also need to add it to the DecoratorLibrary
, for example like this:
const easyForm = new EasyForm()
const decorator = require('../imports/decorators/FormGroupDecorator')
easyForm.addDecorator('myformgroup', {
decorator: decorator.default,
config: isArray(decorator.config) ? decorator.config : [],
combine: isFunction(decorator.combine) ? decorator.combine : union,
filter: isFunction(decorator.filter) ? decorator.filter : stubTrue
})
Note the special (optional) configuration fields:
filter
: afunction
that returns true if supplied with the name of a component that it wishes to modify.combine
: afunction
that is supplied with two arguments: the componentconfig
(an array of form fields) and the decoratorconfig
. By default, the decorator configuration (also an array of fields) is appended to the element form, but in this case it is added first.
To make use of the new label
functionality, we can add them to the element configuration:
const formElements = [
{
key: 'foo',
name: 'foo',
label: 'Fill your foo',
type: 'textarea',
attributes: {
rows: 5
}
},
{
key: 'bar',
name: 'bar',
label: 'Add your bar',
type: 'text'
}
]
Note that the props
that are passed to the decorator include both element
configuration, as well as the model
. This means the decorator could easily respond to the current values in any part of the form.
If you are creating a large component and/or decorator library, it might be worthwhile to have a look at Components.js
and Decorators.js
for ideas on how to bring the together.
Translations
Injecting translator
When wrapping the Form instance or editor in @lefapps/translations’s withTranslator
, you have access to the translator
object inside library config fields. It is then recommended to pass translator
as a prop to each <Form />
component. You should extend this translator object with your own <Translate />
component.
Below is an example of a reusable translated form instance.
import React from 'react'
import { EasyForm } from '@lefapps/forms'
import { withTranslator, Translate } from '@lefapps/translations'
const withTranslateComponent = WrappedForm => ({ translator, ...props }) => (
<WrappedForm
{...props}
translator={Object.assign(translator, { component: Translate })}
/>
)
export default withTranslator(withTranslateComponent(new EasyForm().instance()))
If you want to use your own translator package, check our @lefapps/translations package to see how the translator object should be set up.
Getting translations
There is a helper function translatorText
available to make it easier to retrieve the correct language from placeholders
, label
and other fields.
import { translatorText } from '@lefapps/forms'
const label = {
nl: 'NL Label',
en: 'EN Label'
}
const getLabel = ({ translator }) =>
translatorText(label, translator, forceDefault) || 'fallback'
// returns 'NL Label' if translator.currentLanguage == 'nl'
// returns 'EN Label' if translator.currentLanguage is undefined, but default language == 'en'
// returns label.default if translator is undefined
// returns first item in label if translator is undefined and key 'default' is not present in label
// returns '' if label is empty, you can then project a fallback
// The last parameter forces 'default' as first key to check
Notes
MarkDown
Setting md: true
on a textarea will provide you with an experimental(!) MarkDown editor. Include the following Fontawesome Icons when using this:
import { library } from '@fortawesome/fontawesome-svg-core'
import { faBold, faGripLines, faHeading, faItalic, faLink, faList, faListOl, faPencilAlt, faQuoteRight, faStrikethrough } from '@fortawesome/free-solid-svg-icons'
library.add(faBold, faGripLines, faHeading, faItalic, faLink, faList, faListOl, faPencilAlt, faQuoteRight, faStrikethrough)
You can use the built-in help modal to help your users use the markdown syntax:
import { MarkDownHelp } from '@lefapps/forms'
const TextAreaHelp = () => <MarkDownHelp />
Options to add extra info from plugins will be added in future releases.