Skip to content
Artem Kuzko edited this page Mar 9, 2017 · 3 revisions

Input Prerequisites

react-form-base provides a Form base class which expects to work together with Input components. An Input is any component that consumes three properties: value, error and onChange. It also has to provide it's value as first argument to onChange function supplied in props. The most strait-forward Input component may look like so:

function TextField(props) {
  const { value, error, onChange, ...rest } = props;

  return (
    <div>
      <input value={value} onChange={(e) => onChange(e.target.value)} {...rest} />
      {error &&
        <div className="error">{error}</div>
      }
    </div>
  );
}

TextField.propTypes = {
  value: PropTypes.string,
  error: PropTypes.string,
  onChange: PropTypes.func
};

Basic example

Generally, you can use form's $ method when rendering your inputs. This method generates a set of properties that are consumed by input components.

class UserForm extends Form {
  render() {
    return (
      <div>
        <TextField {...this.$('firstName')} />
        <TextField {...this.$('lastName')} />

        <button onClick={this.save.bind(this)}>Save</button>
      </div>
    );
  }
}

Nested Fields

Form's $ method accepts a string as an argument. This string represents input name - a dot-separated value that specifies input's nesting and/or position in collection. For example, 'address.city', 'favoriteLanguages.1', 'items.0.amount' are all valid input names. Form example bellow deals with address object nested under address property of it's attrs.

class RegistrationForm extends Form {
  render() {
    return (
      <div>
        <TextField {...this.$('email')} />

        <Select {...this.$('address.country')} options={countries} />
        <TextField {...this.$('address.city')} />
        <TextField {...this.$('address.streetLine')} />

        <button onClick={this.save.bind(this)}>Save</button>
      </div>
    );
  }
}

Custom onChange handler

Custom onChange handler is passed in additional parenthesis after $ function call. It's first argument is handler itself, and subsequent values are bound to handler function call.

class OrderForm extends Form {
  changeItem(value) {
    this.set({
      item: value,
      amount: null
    });
  }

  render() {
    return (
      <div>
        <Select {...this.$('item')(this.changeItem)} options={['Item 1', 'Item 2']} />
        <Select {...this.$('amount')} options={['10', '50', '100']} />

        <button onClick={this.save.bind(this)}>Save</button>
      </div>
    );
  }
}

Validation

react-form-base's Form gives you a mechanism to define any validation of your choice that can be applied to input values as user changes them. First step is to define Validation Rules. If you have a base form in your application with a shared functionality, you should define static validations property there:

import Form from 'react-form-base';

// you can use third-party validation in rules:
import isEmail from 'validator/lib/isEmail';

class BaseForm extends Form {
  static validations = {
    presence: function(value) {
      if (!value) return 'cannot be blank';
    },
    email: function(value) {
      if (value && !isEmail(value)) return 'should be an email';
    },
    numericality: function(value, options) {
      const { greaterThan } = options;
      const fValue = parseFloat(value);

      if (isNaN(fValue)) return 'should be a number';
      if (greaterThan != undefined && fValue <= greaterThan) {
        return `should be greater than ${greaterThan}`;
      }
    }
  };
}

And now you can use those rules in your application forms:

import BaseForm from 'base-form';

class UserForm extends BaseForm {
  validations = {
    email: ['presence', 'email'],
    fullName: 'presence',
    age: { presence: true, numericality: { greaterThan: 18 } }
  };

  render() {
    return (
      <div>
        <TextField {...this.$('firstName')} />
        <TextField {...this.$('email')} />
        <TextField {...this.$('age')} />

        <button onClick={this.performValidation.bind(this)}>Validate</button>
      </div>
    );
  }
}

$render($) method

If you don't have extra logic based on render method (such as implementing rendering in base form and calling super.render(someContent) from child forms), and you want to make things a little bit more DRY, you may declare your form's rendering using $render method that accepts input-generation function as argument. Thus, removing the this. prefix in inputs:

class UserForm extends Form {
  $render($) {
    return (
      <div>
        <TextField {...$('firstName')} />
        <TextField {...$('lastName')} />
        <TextField {...$('email')} />
      </div>
    );
  }
}

This form of rendering declaration is also very useful when working with nested forms, since it has a special nested method that will generate onChange handler for nested form for you:

{this.map('items', (_item, i) =>
  <ItemForm key={i} {...$.nested(`items.${i}`)} />
)}

Of course, since $ is argument in this method, you may use any name for this variable that you find suitable.

Inline Forms

If your form is small enough, you might want to render it inline instead of defining separate form component. In this case you may pass renderer function as only form's child. This function takes form's $ function as argument for convenience. Note that you still need to define static validation rules for the Form to be able to use validations.

<Form {...bindState(this)} validations={{ email: ['presence', 'email'], fullName: 'presence' }}>
  {$ => (
    <div>
      <TextField {...$('email')} label="Email" />
      <TextField {...$('fullName')} label="FullName" />
      <button onClick={this.registerUser}>Register</button>
    </div>
  )}
</Form>
Clone this wiki locally