Skip to content

Lightweight React form container with validation (written in TypeScript)

License

Notifications You must be signed in to change notification settings

vitkon/form-container

Repository files navigation

Form Container

Build Status npm version Coverage Status Greenkeeper badge

Form container is a lightweight React form container with validation (written in TypeScript). It allows you to use both HTML5 form validation and custom validation functions.

built with ❤️ at Appfocused

TL;DR

It provides your child form with 2 objects of props:

  • form - all data on your form values, states, errors and warnings
  • formMethods - methods to bind your input controllers and manipulate the form model

Demos:

Installation

Form Container is available as the form-container package on npm.

Install it in your project with npm or yarn

npm install form-container

or

yarn add form-container

Getting started

connectForm([validators], [formConfig])(WrappedComponent)

form-container exposes connectForm function to connect an arbitrary form component (WrappedComponent). It does not modify the component class passed to it; instead, it returns a new, connected component class for you to use.

Arguments

  • [validators: ValidationRule[]] (Array): An array of rules can be provided to validate the form model against them. Rules are executed in a sequence that is defined in the array.

  • [formConfig: IFormConfig] (Object): An object contains initial configuration for the form

    • initialModel: Partial<T> — object provides initial values to the form fields
    • middleware: (props: T) => T & M — function transforms props passed to the wrapped component
    • onInputBlur: (e: React.ForcusEvent<any>) => void — function is called on every blur on an input field within the form. Adding a custom onBlur to the input field itself is not recommended, use this method instead

Validation

HTML5 validation example

import * as React from 'react';

// bare minimum import
import { connectForm, IFormProps } from 'form-container';

// IFormProps interface contains the props that are passed down from form-container
interface IProps extends IFormProps {}

export class Form extends React.Component<IProps, {}> {
    handleSubmit = (e: any) => {
        e.preventDefault();
        const { model } = this.props.form;
        console.log(model);
    }

    render() {
        const { validationErrors, touched } = this.props.form;
        const { bindInput, bindNativeInput } = this.props.formMethods;
        return (
            <form onSubmit={this.handleSubmit}>
                <div>
                    <label>
                        Required field
                        <input
                            {/* HTML attribute to validate required field */}
                            required={true}
                            {/* this is how you bind input to a form-container */}
                            {...bindNativeInput('test')}
                        />
                        <small>{touched.test && validationErrors.test}</small>
                    </label>
                </div>
                <div>
                    <button type="submit">Submit</button>
                </div>
            </form>
        );
    }
}

// no custom validators
const validators: any[] = [];

const formConfig = {
    initialModel: {
        test: 'foo'
    }
}

// attaching our Form to form-container with validation
export default connectForm(validators, formConfig)(Form);

Custom validation example

// components/Form.tsx
import * as React from 'react';
import { connectForm, ValidationRuleFactory, Condition, IFormProps } from 'form-container';

interface IProps extends IFormProps {}

// arbitrary form component
export class Form extends React.PureComponent<IProps, {}> {
    render() {
        const { validationErrors, touched } = this.props.form;
        const { bindInput } = this.props.formMethods;
        return (
            <form>
                <div>
                    <label>
                        Test:
                        <input
                            {/* this is how you bind input to a form-container */}
                            {...bindInput('test')}
                        />
                        <small>{touched.test && validationErrors.test}</small>
                    </label>
                </div>
            </form>
        );
    }
}

// custom validator
const hasMoreThan6Chars: Condition = value => (value ? value.length > 6 : true);
export const strongPassword = ValidationRuleFactory(
    hasMoreThan6Chars,
    'We recommend password to contain more than 6 characters for security purposes',
    'warning'
);

// all validators for the form
const validators = [
    isRequired('test'),
    strongPassword('test')
];

// attaching our Form to form-container with validation
export default connectForm(validators)(Form);

Submit validation example

Form Container exposes SubmissionError, a throwable error which can be used to set submit validation for fields in response to promise rejection because of validation errors.

A wrapped form component has access to handleSubmit via formMethods:

handleSubmit([submit])

Arguments

  • [submit: (model) => Promise]: A submit handler function which has access to the form model and returns a Promise.

Inside the provided submit function you can throw a SubmissionError which will be caught by handleSubmit, and the submission errors will be set for each key in the form state under a key submitErrors, e.g.:

throw new SubmissionError({
    test: "Don't like this..."
});
// components/Form.tsx
import * as React from 'react';
import { connectForm, ValidationRuleFactory, Condition, IFormProps } from 'form-container';

interface IProps extends IFormProps {}

interface IFormModel {
    test: string;
}

// arbitrary form component
export class Form extends React.PureComponent<IProps, {}> {
    submit = (model: IFormModel) => {
        return fetch('http://dummyurl.com', {
            method: 'POST',
            body: model
        }) // your Promise based HTTP client of choice
            .catch(error => {
                const { data: { code }, status } = error.response;
                if (status === 422 && code === 'xyz') {
                    throw new SubmissionError({
                        foo: "Back-end doesn't like this..."
                    });
                } else {
                    /// handle other errors accordingly
                }
            });
    };

    render() {
        const { validationErrors, submitErrors, touched } = this.props.form;
        const { bindInput, handleSubmit } = this.props.formMethods;
        return (
            <form>
                <div>
                    <label>
                        Test:
                        <input {...bindInput('test')} />
                        <small>
                            {(touched.test && validationErrors.test) || submitErrors.test}
                        </small>
                    </label>
                </div>
                <button onClick={handleSubmit(this.submit)} />
            </form>
        );
    }
}

export default connectForm<IFormModel>()(Form);