Manual Reference Source

basic/form/Form.js

/**
 * Created by pposel on 23/01/2017.
 */

import PropTypes from 'prop-types';

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Form as FormSemanticUiReact, Radio as FormRadio, Button as FormButton } from 'semantic-ui-react';

import ErrorMessage from '../ErrorMessage';
import FormField from './FormField';
import FormCheckbox from './FormCheckbox';
import FormGroup from './FormGroup';
import FormDivider from './FormDivider';
import FormFile from './InputFile';
import FormUrlOrFile from './InputUrlOrFile';
import FormInputDate from './InputDate';
import FormInputTime from './InputTime';
import FormDropdown from '../Dropdown';
import FormTable from './EdiTable';
import FormColorPicker from './ColorPicker';
import FormJson from './InputJson';

import '../../styles/Form.css';

/**
 * Form is a component to present HTML forms
 *
 * Form is customized version of [Semantic UI-React's Form component](https://react.semantic-ui.com/collections/form),
 * so all properties of that component can be used here.
 *
 * errors prop can be just a string containing error message or an object with the following syntax:
 * ```
 * {
 *      field1: 'errorMessage1',
 *      field2: 'errorMessage2',
 *      ...
 * }
 * ```
 *
 * ## Access
 * `Stage.Basic.Form`
 *
 * ## Usage
 * ### Form before submission (no errors: _.isEmpty(this.state.errors))
 * ![Form](manual/asset/form/Form_0.png)
 *
 * ### Form after submission (with errors: !_.isEmpty(this.state.errors))
 * ![Form](manual/asset/form/Form_1.png)
 *
 * ```
 * <Form onSubmit={this._submitCreate.bind(this)} errors={this.state.errors} ref="createForm">
 *   <Form.Field error={this.state.errors.username}>
 *     <Form.Input name='username' placeholder="Username"
 *                 value={this.state.username} onChange={this._handleInputChange.bind(this)}/>
 *   </Form.Field>
 *
 *   <Form.Field error={this.state.errors.password}>
 *     <Form.Input name='password' placeholder="Password" type="password"
 *                 value={this.state.password} onChange={this._handleInputChange.bind(this)}/>
 *   </Form.Field>
 *
 *   <Form.Field error={this.state.errors.confirmPassword}>
 *     <Form.Input name='confirmPassword' placeholder="Confirm password" type="password"
 *                 value={this.state.confirmPassword} onChange={this._handleInputChange.bind(this)}/>
 *   </Form.Field>
 *
 *   <Form.Field error={this.state.errors.role}>
 *     <Form.Dropdown selection name='role' placeholder="Role" options={roleOptions}
 *                    value={this.state.role} onChange={this._handleInputChange.bind(this)}/>
 *   </Form.Field>
 * </Form>
 * ```
 *
 */
export default class Form extends Component {
    constructor(props) {
        super(props);

        this.submitFormBtnRef = React.createRef();
    }

    /**
     * Form field, see {@link FormField}
     */
    static Field = FormField;

    /**
     * Form group, see {@link FormGroup}
     */
    static Group = FormGroup;

    /**
     * Form divider, see {@link FormDivider}
     */
    static Divider = FormDivider;

    /**
     * Form input, see [Form.Input](https://react.semantic-ui.com/collections/form/)
     */

    static Input = FormSemanticUiReact.Input;

    /**
     * Form text area input, see [TextArea](https://react.semantic-ui.com/addons/text-area)
     */
    static TextArea = FormSemanticUiReact.TextArea;

    /**
     * Form radio button, see [Input](https://react.semantic-ui.com/addons/radio)
     */
    static Radio = FormRadio;

    /**
     * Form checkbox input, {@link FormCheckbox}
     */
    static Checkbox = FormCheckbox;

    /**
     * Form file input, see {@link InputFile}
     */
    static File = FormFile;

    /**
     * Form URL or file input, see {@link InputUrlOrFile}
     */
    static UrlOrFile = FormUrlOrFile;

    /**
     * Dropdown field, see {@link Dropdown}
     */
    static Dropdown = FormDropdown;

    /**
     * Calendar picker input, see {@link InputDate}
     */
    static InputDate = FormInputDate;

    /**
     * Time picker input, see {@link InputTime}
     */
    static InputTime = FormInputTime;

    /**
     * Form checkbox input, see [Button](https://react.semantic-ui.com/elements/button)
     */
    static Button = FormButton;

    /**
     * Form table input, see {@link EdiTable}
     */
    static Table = FormTable;

    /**
     * Form color picker input, see {@link ColorPicker}
     */
    static ColorPicker = FormColorPicker;

    /**
     * Form JSON input,
     */
    static Json = FormJson;

    /**
     * propTypes
     *
     * @property {object} [errors] string with error message or object with fields error messages (syntax described above)
     * @property {string} [errorMessageHeader] string with error message header
     * @property {Function} [onSubmit=()=>{}] function called on form submission
     * @property {Function} [onErrorsDismiss=()=>{}] function called when errors are dismissed (see {@link ErrorMessage})
     * @property {boolean} [scrollToError=false] if set, then on error change screen will be scrolled to (see {@link ErrorMessage})
     */
    static propTypes = {
        ...Form.propTypes,
        errors: PropTypes.any,
        errorMessageHeader: PropTypes.string,
        onSubmit: PropTypes.func,
        onErrorsDismiss: PropTypes.func,
        scrollToError: PropTypes.bool
    };

    static defaultProps = {
        errors: null,
        errorMessageHeader: 'Errors in the form',
        onSubmit: () => {},
        onErrorsDismiss: () => {},
        scrollToError: false
    };

    static fieldNameValue(field) {
        const { name } = field;
        let { value } = field;

        if (field.type === 'checkbox') {
            value = field.checked;
        }

        if (field.type === 'number') {
            const isFloat = n => Number(n) % 1 !== 0;
            value = isFloat(field.value) ? parseFloat(field.value) : parseInt(field.value);
        }

        if (_.isEmpty(name)) {
            console.error('Required name attribute is not provided!', field);
            throw 'Required name attribute is not provided!';
        }

        return { [name]: value };
    }

    componentDidUpdate(prevProps) {
        if (
            this.props.scrollToError &&
            !_.isEmpty(this.props.errors) &&
            !_.isEqual(prevProps.errors, this.props.errors)
        ) {
            const formElement = ReactDOM.findDOMNode(this);
            if (formElement) {
                formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
            }
        }
    }

    submit() {
        $(this.submitFormBtnRef.current).click();
    }

    _handleSubmit(e, data) {
        e.preventDefault();
        this.props.onSubmit && this.props.onSubmit(data.formData);
        return false;
    }

    render() {
        let { errors, errorMessageHeader, onErrorsDismiss, scrollToError, ...rest } = this.props;

        if (_.isString(errors)) {
            errors = [errors];
        } else if (_.isObject(errors)) {
            errors = _.valuesIn(errors);
        }

        return (
            <FormSemanticUiReact {...rest} onSubmit={this._handleSubmit.bind(this)} error={!_.isEmpty(errors)}>
                <ErrorMessage header={errorMessageHeader} error={errors} onDismiss={onErrorsDismiss} />

                {this.props.children}

                <input type="submit" name="submitFormBtn" style={{ display: 'none' }} ref={this.submitFormBtnRef} />
            </FormSemanticUiReact>
        );
    }
}