1.8.17 • Published 7 days ago

@lefapps/forms v1.8.17

Weekly downloads
2
License
MIT
Repository
github
Last release
7 days ago

Forms

This is a composition based form generator. 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'

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

PropRequired?Notes
elementsyesarray of form elements
onSubmityesfunction to call when form gets submittedgets the form model as only parameter
initialModeldefault form values (in the same format as the form model)
onStateChangeperform 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 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:

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

PropertyRequired?DefaultTypeNotes
nameyesStringdefines the structure in the form model*
typeyesStringdefines the type of input (see components folder)
labelStringObject*input label
optionsselectchekbox(-mc)radio[][String][Object]*available values
requiredfalseBooldefault validation decorator is applied when true
schemaStringObject*help text when field is invalid
dependentObjectdynamically show or hide element, based on value of other elementneeds dependent decorator
layoutObjectconfig for default layout decoratoruses bootstrap grid
attributesObjectpassed attributes are applied directly to input elemente.g.: rows for textarea
keyStringonly 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: 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:

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.

1.8.17

7 days ago

1.8.16

1 year ago

1.8.15

3 years ago

1.8.12

3 years ago

1.8.13

3 years ago

1.8.14

3 years ago

1.8.11

3 years ago

1.8.10

4 years ago

1.8.9

4 years ago

1.8.8

4 years ago

1.8.7

4 years ago

1.8.6

4 years ago

1.8.4

4 years ago

1.8.3

4 years ago

1.8.2

4 years ago

1.8.1

4 years ago

1.8.0

4 years ago

1.7.9

4 years ago

1.7.8

4 years ago

1.7.7

4 years ago

1.7.6

4 years ago

1.7.5

4 years ago

1.7.4

4 years ago

1.7.3

4 years ago

1.7.2

4 years ago

1.7.1

4 years ago

1.7.0

4 years ago

1.6.6

4 years ago

1.6.5

4 years ago

1.6.4

4 years ago

1.6.3

5 years ago

1.6.2

5 years ago

1.6.1

5 years ago

1.6.0

5 years ago

1.5.2

5 years ago

1.5.1

5 years ago

1.5.0

5 years ago

1.4.11

5 years ago

1.4.10

5 years ago

1.4.9

5 years ago

1.4.8

5 years ago

1.4.7

5 years ago

1.4.6

5 years ago

1.4.5

5 years ago

1.4.4

5 years ago

1.4.3

5 years ago

1.4.2

5 years ago

1.4.1

5 years ago

1.4.0

5 years ago

1.3.1

5 years ago

1.3.0

5 years ago