Skip to content

blueberryapps/onion-form

Repository files navigation

Onion Form CircleCI Dependency Status

As a developer you are assigned with creating a registration form on Registration page with fields for first name, last name, e-mail and password, validate them and then send all these fields to API. Not again? This package will make your life easier by simplifying the dealing with forms.

yarn add --save onion-form

This package is only meant to be used together with Redux!

TLDR

import { Form, Field, Submit } from 'onion-form';

<Form
  name="signIn"
  onError={({ errors }) => { console.log(errors) }}
  onSubmit={({ values }) => { console.log(values) }}
  validations={{ email: (value) => [((value && !value.match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i)) ? 'wrongFormat' : null)]}}
>
  <Field name='email' type='email' />
  <Field name='password' type='password' />
  <Submit>Sign In</Submit>
</Form>

Usage

// Registration.react.js
import React, { Component } from 'react';
import { Form, Submit, connectField } from 'onion-form';

// validations
const isRequired = (value) => ((!value) ? 'required' : null);
const emailNeedsToContainName = (_, otherValues) => ((!otherValues.email || otherValues.email.indexOf(otherValues.name) === -1) ? 'invalidEmail' : null);
const needLetters = (value) => (value && !value.match(/[a-zA-Z]+/i ? 'needLetters' : null);
const needNumbers = (value) => (value && !value.match(/\d+/i) ? 'needNumbers' : null);

const validations = {
  lastName: [isRequired],
  email: [emailNeedsToContainName],
  password: [needLetters, needNumbers]
};

// You need to have a component which will receive all data by props
// error, hint, label, name, onBlur, onChange, onFocus, onionFormName, tooltip
const BasicInput = (props) => (<input type="text" {...props} />);

// Create your fields (can be used in different forms)
const FirstName = connectField('firstName')(BasicInput);
const LastName  = connectField('lastName')(BasicInput);
const Email     = connectField('email', { type: 'email' })(BasicInput);
const Password  = connectField('password', { type: 'password' })(BasicInput);

export default class RegistrationPage extends Component {

  onSubmit({ values: { firstName, lastName, email, password } }) {
    // apiCall('POST', { firstName, lastName, email, password })
  }

  onError({ errors{ firstName, lastName, email, password } }) {
    // alert, show flash message what ever you need to do when use tryies to
    // submit form and gets validation errors
  }

  render() {
    return (
      <div>
        <h1>Registration</h1>
        <Form
          name="myRegistrationForm"
          onSubmit={this.onSubmit.bind(this)}
          onError={this.onError.bind(this)}
          validations={validations}
        >
          <FirstName label="Your first name" />
          <LastName />
          <Email />
          <Password />
          <Submit>Register</Submit>
        </Form>
      </div>
    )
  }
}

Validations

There are three ways how you can add validations to your form:

  1. Pass an object with validations to the Form component as props (see examples above)
  2. Pass an array of validations to the connectField function: connectField('password', null, [isRequired(), password()])
  3. Specify the validations when the field component is being used:
export default class MyForm extends Component {
  render() {
    return (
      <Form name="myForm">
        <Email validations={[isRequired(), email()]} />
        <Password validations={[isRequired(), password()]}/>
        <Submit>Login</Submit>
      </Form>
    )
  }
}

All validations you specify will be used.

Redux

!You need to add onion form reducer to your reducers and it must be under onionForm first level key!

// store.js
import { createStore, combineReducers } from 'redux';
import { reducer as onionForm } from 'onion-form';

const store = createStore(combineReducers({ onionForm }), {})

Action Creators

We have multiple action creators for communication with reducer: setMultipleFields, setFormFieldProperty, clearForm, clearFormProperty, setFieldValue, setFieldLiveValidation, setFieldError, setFieldApiError All these actions accept formName as the first parameter which needs to match FORM_NAME in <Form name=FORM_NAME/>.

All connected fields get formName from context.

But sometimes you need to communicate with fields from your code and repeating name of the form can be exhausting, so we provide createFormActions(formName) which returns all the actions with formName set.

connectField(FIELD_NAME, DEFAULT_PROPS)(DECORATED_COMPONENT)

DEFAULT_PROPS: can be a plain {} or a function which receives props as the first parameter and needs to return {}. This function gets resolves in render on every rerender. (props) => ({ label: props.msg('key.to.label') })

FIELD_VALUES_FROM_STATE: By default we store these values in redux state:

{
  value: '',
  liveValidation: false,
  error: null,
  apiError: null
}

But you can use setMultipleFields(form, property, values) or setFormFieldProperty(form, field, property, value) to set custom properties which will be then passed to the decorated component as well.

ONION_PROPS: error, hint, label, name, onBlur, onChange, onFocus, onionFormName, tooltip

When you initialize a component in render you can pass the following PASSED_PROPS:

PASSED_PROPS label, onBlur, onFocus, onChange, tooltip, hint, defaultValue They will be transferred to the decorated component. Functions passed by props (onFocus, onChange, onBlur) will get called too, after onion form callbacks.

Passing order of props is: DEFAULT_PROPS -> FIELD_VALUES_FROM_STATE -> ONION_PROPS -> PASSED_PROPS

You can pass defaultValue to component by (PROPS or DEFAULT_PROPS) to set that value to state on componentDid mount when field has no value already set.

connectSubmit(DECORATED_COMPONENT)

You can use connectSubmit which will pass onClick, valid, hasValues, hasErrors and disabled as prop to the decorated component:

// CustomSubmit.react.js
import { connectSubmit } from 'onion-form';

const Button = ({ children, disabled, onClick }) => (
  <button disabled={disabled} onClick={onClick} type="submit">{children}</button>
);

export default const connectSubmit(Button);
  • onClick: callback function for submitting form
  • valid: returns true/false based on fields validations runned against state (errors doesn't need to be in store)
  • hasErrors: returns true if form is invalid (based on state from Redux)

Translations

You need to pass to component function msg('keypath') => string.

Implemetation is your thing but it needs to follow:

msg('key.foo') // returns translation for key.foo
msg(['foo', 'bar']) // returns translation for foo if exists else bar

We use this function to resolve translations for the error, hint, label, tooltip, placeholder props.

error is specific because we are trying to get text by:

const error = field.error || field.apiError;
const errorText = error
  ? msg([`form.${formName}.errors.${error}`, `form.errors.${error}`, `errors.${error}`])
  : '';

others are easier, for example label:

const labelText = label || defaultProps.label || msg([`form.${formName}.${fieldName}.label`, `form.${fieldName}.label`, `${fieldName}.label`]);

!For detailed documentation of all options do yarn test!

Commands

  • yarn test: runs mocha tests
  • yarn test:watch: runs mocha test with watch option
  • yarn coverage: create code coverage report

Made with love by