react-form-binder

4.0.2 • Public • Published

A React data entry form binder. Automatically controls component state, validates and translates data to and from internal data objects. Works with both React and React Native.

When building data entry forms with React there are a bunch of things that you will want to do in pretty much all cases:

  • Move data to and from an internal transmission or storage object, such as the payload of an API call or a database record.
  • Track whether the data has been modified and filter out only the modified properties
  • Validate that the React components on the form contain valid data
  • Set the enabled, read-only or visible state of components on the form based on the value of other components.
  • Transform data from and to the transmission/storage format, e.g. date/times from ISO 8601 to local time.
  • Be able to easily detect the validity and modification state of the entire form.

Glossary

Binding - an object that defines how to move data back and forth from the internal data format to the React form components used to edit that data.

Binding Definition - a set of properties that define the binding.

Binder - an object that manages a collection of bindings.

Bound Component - a React component that uses a binder to control it's state. There are two types of bound components. Those that maintain a value as part of their state, e.g. text input, checkbox, dropdown, and those that maintain no value, e.g. 'OK' and 'Cancel' buttons. Non-value components can an still change their state based on the value of other bound components on the form.

Step-by-Step

Here are the step-by-step instructions to use the library for a React application:

  1. Add the bindings definition object as a static property of the class, ensuring an isValid at a minimum for each binding. isValid can be any truthy value or a function returning a truthy value. Mark a binding as noValue if it does not map to a property in the data object.

  2. Construct the FormBinder object in the form component constructor() and assign it to either this.binder (or this.state.binder if the bindings will be changing)

  3. Set the binder property of each bound React component in the form. Typically, you'll want to remove any existing onChange handlers if converting from a non-bound component and ensure a name proprty. See below for how to create bound components.

  4. For HTML, in the onSubmit event handler method add e.preventDefault() (to avoid automatic form processing by HTML)

  5. Call this.binder.getDeltaObject() to get an object with just the modified values at any time.

Class: Binding

Bound forms work using an array of Binding objects that should be declared as a static property of the form component. Here is an example:

class MyFormComponent extends React.Component {
  ...
  static bindings = {
  	email: {
  	  isValid: (b, v) => (regExpPattern.email.test(v)),
  	  isDisabled: (b) => (!!b._id)
  	},
  	changeEmail: {
  	  nonValue: true,
  	  isDisabled: (b) => (!!b._id === false)
  	},
  	...
  }

  render() {
    return (
      <form ...>...</form>
    )
  }
}

Each property of the bindings object must correspond to the name property of a React component in the form.

You can use .'s in the name of the binding property, but you must quote the name. This defines a path to a nested object within the internal data object (similar to paths in MongooseJS, for example).

The following are the various properties you can set for a binding definition.

Property: noValue: bool

If true, indicates the binding does not track a value. Only the state.disabled, state.readOnly and state.visible properties of the binding change in response to the value of other bindings. Default is false.

Property: isDisabled: bool | (binder: FormBinder, value: any) => bool

A truthy value, or function returning a truthy value, that indicates determines state.disabled for the binding. It's up to the bound component to implement what disable looks like, e.g. graying out the component.

Property: isReadOnly: bool | (binder: FormBinder, value: any) => bool

A truthy value, or function returning a truthy value, that determines state.readOnly for the binding. It's up to the bound component to implement what read-only looks & behaves like, e.g. prevent mouse clicks on the component.

Property: alwaysGet: bool

Setting this on a binding means this value will always be returned in a call to getDeltaObject even if it has not changed.

Function: isVisible: bool | (binder: FormBinder, value: any) => bool

A truthy value, or function returning a truthy value, that determines state.visible for the binding. It's up to the bound component to implement looks like, but it should generally correspond to being hidden or not.

Function: isValid: bool | (binder: FormBinder, value: any) => bool

A truthy value, or function returning a truthy value, that determines state.valid for the binding. It's up to the bound component to implement looks like, but generally you want to display some kind of error message when this is false.

Function: pre (binder: FormBinder, value: any) => any

A function used to pre-process the original object property for use as the binding value in the bound component. The binder looks through the binding names to find values in the original object. If a value is undefined the binder will use "" as the binding value by default. You can use this function to override that behavior.

For example, the original object may have a boolean property. If you want to use this value to set a checkbox INPUT control, you might set pre: v => v || false so that the checkbox has a valid value, even if the field is not initially set.

Function: post: (binder: FormBinder, value: any) => any

A function use to post-process the binding value to a format used in the original object.

Class: FormBinder(obj: object, bindings: Bindings, onAnyModified: () => ())

The FormBinder class is responsible for holding a collection of bindings. Here's an example of how to construct the class:

    this.state = {
      binder: new FormBinder(
      	this.props.myObject,
      	MyForm.validations,
      	this.props.onAnyModifiedChanged)
    }

Constructor: constructor(obj: Object, bindings: Bindings, onAnyModified: bool)

The constructor takes the original object that is being modified by the form, the Bindings object, and a callback for when the overall form modification state changes.

You must pass the FormBinder instance to each bound component as a property binder={this.state.binder} so save it in this or this.state.

Assigning the binder to the form state allows you to change it if the the object that the form displays changes. You can use the componentWillReceiveProps method to update the FormBinder, like so:

  componentWillReceiveProps(nextProps) {
    if (nextProps.obj !== this.props.obj) {
      this.setState({
        binder: new FormBinder(nextProps.obj, MyForm.bindings, nextProps. nextProps.onAnyModifiedChanged)
      })
    }
  }

This would be useful if you allow a forms contents to be changed by some other onscreen element, such as a list box.

Property: id: string

This will be the value of any _id property on the original object. Typically, this value will be set for existing objects that were retrieved from an API, and not set if the object is being created for the first time. For this reason, it can be used in binding functions to determine if the object is being created for the first time or modified.

Property: getOriginalObject: Object

A copy of the original object passed into the constructor.

Property: getMetadata: Object

A copy of any metadata passed to the binder.

Method: getBindingState(name: string): ValueState | NoValueState

Called to get the state for a binding. Can be used by bound components, in binding callback functions and elsewhere.

Method: getBindingValue(name: string): string

Called to get just the state.value for a binding. For noValue bindings it will of course be undefined.

Method: updateBindingValue(name: string, newValue: any): State

Called to update the value of the binding by name. Typically used in bound components in response to user input. Will cause the state for all other bound components in the form to update their state also, if the change affect them.

Method: getDeltaObject(): Object

Returns an object containing just the modified binding values, the object _id and any bindings marked as alwaysGet. All the binding values are passed through any configured post function so they are in the original object data format. For example, you might call this when a submit button is clicked to get an object containing just the modified values to send to an API. You can also use a spread operator to blend this with the original values returned from getOriginalObject.

Class: NoValueState

The object returned from noValue bindings.

Property: valid: bool

Is the data valid?

Property: disable: bool

Should the bound component be disabled?

Property: visible: bool

Should the bound component be visible?

Property: readOnly: bool

Is the bound component read-only?

Class: ValueState extends NoValueState

For bindings with a value, NoValueState is extended with value and modified properties.

Property: value: any

The binding value

Property: modified: bool

Has the bound component been modified from it's original value?

Building Bound components

FormBinder enables you to easily build bound components that are automatically set from an initial data object, and allow you to easily get an object back that reflects changes to that object. This gives you the most flexibility to create components that look good in your app, show errors the way you want to show them, etc..

See the example folder in GitHub for for a wide variety of other bound components, including:

  • A full featured credit card component which automatically formats and validates different types of credit card numbers
  • A masked input component which formats input according to a mask
  • A state dropdown component
  • A check box component
  • Valueless button components
  • A container component that can shows or hides a group of components based on other bound components.

You can build and run the example project with:

git clone https://github.com/jlyonsmith/react-form-binder.git
cd react-form-binder
npm install
npm run build
cd example
npm install
npm start

On MacOS with iTerm2 installed you can run everything super easily with:

npm install -g snap-tool
git clone https://github.com/jlyonsmith/react-form-binder.git
cd react-form-binder
snap install
snap start

Lets break down the BoundInput component from the example, which happens to use SemanticUI. Of course you can use any React UI framework you like. This is the most common bound component and is basically an input component with type='text'.

First, we declare our React component, ensuring we get at least a name, binder, message and label:

export class BoundInput extends React.Component {
  static propTypes = {
    name: PropTypes.string.isRequired,
    binder: PropTypes.object.isRequired,
    message: PropTypes.string.isRequired,
    label: PropTypes.string.isRequired,
    ...
  }
  ...
}

In the constructor we bind() some methods to this and set the initial state.

We also add an event listener for the props.name event. The FormBinder will fire an event with the name of the binding if some element of this bindings state, other than value, changes because of another binding value getting updated. The only way value should change is if this component updates it, or if the binder that this component is attached to changes.

We remove the listener in the unmount callback.

  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.updateValue = this.updateValue.bind(this)
    props.binder.addListener(props.name, this.updateValue)
    this.state = props.binder.getBindingState(props.name)
  }

  updateValue(e) {
    this.setState(e.state)
  }

  componentWillUnmount() {
    this.props.binder.removeListener(this.props.name, this.updateValue)
  }

We implement the handleChange event which gets called every time the user types in the input element.

  handleChange(e, data) {
    const { binder, name } = this.props
    const state = binder.getBindingState(name)

    if (!state.readOnly && !state.disabled) {
      this.setState(binder.updateBindingValue(name, data.value))
    }
  }

Now we handle the binder property changing in the parent form:

  componentWillReceiveProps(nextProps) {
    if (nextProps.binder !== this.props.binder) {
      this.setState(nextProps.binder.getBindingState(nextProps.name))
    }
  }

Finally, the render() method to generate the DOM elements based on the binding state's.

  render() {
    return (
      <Form.Field error={!this.state.valid} width={this.props.width} disabled={this.state.disabled} className={this.props.className}>
        <label>{this.props.label}</label>
        <Popup content={this.props.message} position={this.props.position || 'bottom center'}
          hoverable={true}
          trigger={
            <Input {...this.otherProps} value={this.state.value} type='text'
              name={this.props.name} onChange={this.handleChange} />
          }
        />
      </Form.Field>
    )
  }

Release Notes

3.0.0

Follows closely on the release of 2.0.x because I decided to remove all use of the term field and instead double down on the term binding. This changes the API, which is breaking change and thus a major version number increment. In my opinion it makes the entire library more consistent and much easier to learn and understand. This release also passed the initial value to pre and uses the resulting value as the unmodified value against which to compare the binding value.

Package Sidebar

Install

npm i react-form-binder

Weekly Downloads

4

Version

4.0.2

License

MIT

Unpacked Size

40.4 kB

Total Files

7

Last publish

Collaborators

  • jlyonsmith