Skip to content

Repository files navigation

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

  1. Easyform: getting started
  1. Modifying libraries
  2. Editing forms
  3. Building your own components
  4. Building your own decorators
  5. 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'constformElements=[{name: 'foo',type: 'textarea'},{name: 'bar',type: 'text'}]constMyForm=newEasyForm().instance()classExampleextendsReact.Component{_onSubmit=model=>{// this gets called when the form is submitted// e.preventDefault() has already been called of courseconsole.log(model)}render(){return(<MyFormelements={formElements}initialModel={{bar: 'Example text'}}onSubmit={this._onSubmit}><buttontype='submit'>Submit</button></MyForm>)}}

Props

PropRequired?Notes
elementsyesarray of form elements
onSubmityesfunction to call when form gets submitted
gets the form model as only parameter
initialModeldefault form values (in the same format as the form model)
onStateChangeperform transormations on the model
gets the model as only parameter, expects a (modified) model to be returned again

Configuration

The EasyForm constructor accepts a configuration object with:

  • library: a component Library, which is an extended Map object that holds all form components
  • decorators: a DecoratorLibrary, which is an extended Library 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 the editor mode below.)

For example, you might only want to apply the standard FormGroup decorator:

constMyForm=newEasyForm().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

PropertyRequired?DefaultTypeNotes
nameyesStringdefines the structure in the form model*
typeyesStringdefines the type of input (see components folder)
labelString
Object*
input label
optionsselect
chekbox(-mc)
radio
[][String]
[Object]*
available values
requiredfalseBooldefault validation decorator is applied when true
schemaString
Object*
help text when field is invalid
dependentObjectdynamically show or hide element, based on value of other element
needs dependent decorator
layoutObjectconfig for default layout decorator
uses bootstrap grid
attributesObjectpassed attributes are applied directly to input element
e.g.: rows for textarea
keyStringonly necessary if multiple elements with the same name are present
e.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

constelements=[{name: 'name'},{name: 'address.street'},{name: 'address.number'},{name: 'address.zip'},{name: 'address.city'}]constonSubmit=model=>{// model is an object which reflects the structure of the element namesconst{ name, address }=modelconst{ 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'constMyFormConfig=newEasyForm()MyFormConfig.addComponent(name1,component)MyFormConfig.removeComponent(name2)MyFormConfig.addDecorator(name3,decorator)MyFormConfig.removeDecorator(name4)constMyForm=MyFormConfig.instance()constMyTranslatedForm=withTranslator(MyForm)

or

constMyDecorators=DefaultDecorators.subset(['formgroup','layout'])constMyComponents=DefaultComponents.subset(['textarea','checkbox'])constMyForm=newEasyForm({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'constMyFormEditor=newEasyForm().editor()constMyTranslatedFormEditor=withTranslator(MyFormEditor)classExampleextendsComponent{_onSubmit=formElements=>{// this gets called when the form editor is saved// e.preventDefault() has already been called of courseconsole.log(formElements)}render(){return(<MyTranslatedFormEditorinitialModel={formElements}onSubmit={this._onSubmit}><Buttontype='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:

classTextComponentextendsComponent{gettype(){return"text"}render(){const{ bindInput, element,attributes: propsAttributes}=this.propsconst{ name, type,attributes: elementAttributes}=elementreturn(<Inputtype={type}{...bindInput(name)}{...elementAttributes}{...propsAttributes}/>)}}consttransform=(element,{ translator, model },saving)=>{// perform mutations of element properties// when saving or retrieving (saving = true/false)// Example: see translations for selectsreturnelement// do not forget to return the altered element}constconfig=({ translator, model })=return[{key: 'name',name: 'name',type: 'text',label: 'Field name',// or translator object { nl: '', en: '' }attributes: {placeholder: 'Technical name for field',// ORplaceholders: {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}}}]exportdefaultTextComponentexport{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:

consteasyForm=newEasyForm()constpath='../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:

constFormGroupDecorator=WrappedComponent=>props=>(<FormGroup>{props.element.label ? (<Labelfor={props.element.name}>{props.element.label}</Label>) : null}<WrappedComponent{...props}/> // don't forget to "push down" the props into
the wrapped component
</FormGroup>)consttransform=(element,{ translator, model },saving)=>{// perform mutations of element properties// when saving or retrieving (saving = true/false)// Example: see translations for selectsreturnelement// do not forget to return the altered element}constconfig=({ translator, model })=>[{key: 'label',name: 'label',type: 'textarea',label: 'Field label or introduction',layout: {col: {md: 12}}}]// Configuration of label is put in frontconstcombine=_.flip(_.union)// we're only interested in certain components:constfilter=componentType=>_.includes(['textarea','text'],componentType)exportdefaultFormGroupDecoratorexport{transform,config,combine,filter}

You also need to add it to the DecoratorLibrary, for example like this:

consteasyForm=newEasyForm()constdecorator=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: a function that returns true if supplied with the name of a component that it wishes to modify.
  • combine: a function that is supplied with two arguments: the component config (an array of form fields) and the decorator config. 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:

constformElements=[{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.

importReactfrom'react'import{EasyForm}from'@lefapps/forms'import{withTranslator,Translate}from'@lefapps/translations'constwithTranslateComponent=WrappedForm=>({ translator, ...props})=>(<WrappedForm{...props}translator={Object.assign(translator,{component: Translate})}/>)exportdefaultwithTranslator(withTranslateComponent(newEasyForm().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'constlabel={nl: 'NL Label',en: 'EN Label'}constgetLabel=({ 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'constTextAreaHelp=()=><MarkDownHelp/>

Options to add extra info from plugins will be added in future releases.

About

This is a composition based form generator. Every composed form is also [reformed](https://github.com/davezuko/react-reformed), allowing for easy model bindings.

Topics

Resources

Stars

0 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages