It can be tricky to handle elaborate form features such as repeatable sections and context-sensitive hiding or showing. Here is one approach using React.


React’s form documentation focuses on basic properties and outlines the differences between controlled and uncontrolled components within forms, but composing more advanced forms is left up to the imagination of the developer. What’s more is that React is all about one-way data flow down the component heirarchy, which can be challenging to reconcile in form management. There is form creation to deal with, as well as form submission. This article will focus on form creation, an will not address handling form submissions in React. The majority of one-off contact forms could be handled as a single component —<Form />, for instance.

View Entire Project on CodePen

var React = require('react');

// Form component
var Form = React.createClass({
    render: function() {
        return (
            <form>
                <fieldset>
                    <legend>RSVP</legend>

                    <label htmlFor='name'>Name
                        <input id='name' name='name' type='text' />
                    </label>

                    <label htmlFor='email'>Email Address
                        <input id='email' name='email' type='email' />
                    </label>

                    <input type='submit' />
                </fieldset>
            </form>
        );
    }
});

React.render(<Form />, document.getElementById('myForm'));
Figure 1a. Forgive the lack of syntax highlighting for JSX

Here’s the output.

Figure 1b. Styles courtesy of Fuselage

Reusing Components

But moving into the realm of a form framework is not so simple. It quickly becomes necessary to abstract away elements of the form into other React components. A natural way to break up a web form is into input groups —that is, a label and an element that takes input. This covers the majority of use cases that forms must handle, as evidenced by personal experience and the forms that can be built for Foundation or Bootstrap. Here is what a component <InputGroup /> could look like.

var React = require('react');

// InputGroup component
var InputGroup = React.createClass({
    render: function() {
        return (
            <label htmlFor={this.props.id}>{this.props.title}
                <input id={this.props.id} name={this.props.id} type={this.props.type} />
            </label>
        );
    }
});

// Form component
var Form = React.createClass({
    render: function() {
        return (
            <form>
                <fieldset>
                    <legend>Basic Stuff</legend>

                    <InputGroup id={'name'} type={'text'} title={'Name'} />
                    <InputGroup id={'email'} type={'email'} title={'Email Address'} />

                    <input type='submit' />
                </fieldset>
            </form>
        );
    }
});

React.render(<Form />, document.getElementById('myForm'));
Figure 2a.

Notice the properties are passed in as simple values. This could be made simpler and more robust through the use of spread attributes and prop validation, but refining these examples into a framework is beyond the scope of this article.

The output is the same as Figure 1b, but now the <InputGroup /> component can be used repeatedly for other input groups.

Figure 2b.

Creating the <InputGroup /> component is a great first step, but it cannot yet handle textareas, selects, inline checkboxes, or inline radio buttons. It could be modified to handle these elements, or new React components could be used. The following example shows the functionality of a <select> element offloaded to a new component, called <Dropdown />, which accepts an array of options that render as the <select>’s options.

var React = require('react');

// Dropdown component
var Dropdown = React.createClass({
    render: function() {
        return (
            <label htmlFor={this.props.id}>{this.props.title}
                <select id={this.props.id} name={this.props.id}>
                    {this.props.options.map(function(option, i) {
                        return <option val={option} key={i}>{option}</option>
                    })}
                </select>
            </label>
        );
    }
});

// InputGroup component
var InputGroup = React.createClass({
    render: function() {
        return (
            <label htmlFor={this.props.id}>{this.props.title}
                <input id={this.props.id} name={this.props.id} type={this.props.type} />
            </label>
        );
    }
});

// Form component
var Form = React.createClass({
    render: function() {
        return (
            <form>
                <fieldset>
                    <legend>RSVP</legend>

                    <InputGroup id={'name'} type={'text'} title={'Name'} />
                    <InputGroup id={'email'} type={'email'} title={'Email Address'} />

                    <Dropdown
                        id={'diet'}
                        type={'select'}
                        title={'Dietary Needs'}
                        options={DIETARY_OPTIONS} />

                    <input type='submit' />
                </fieldset>
            </form>
        );
    }
});

var DIETARY_OPTIONS = ['None', 'Gluten-free', 'Nut-free', 'Vegan', 'Other'];

React.render(<Form />, document.getElementById('myForm'));
Figure 3a.

The output is below. Like many other snippets in this article, the <Dropdown /> component could be made more powerful with a few enhancements. Options, for instance could be contained in <optgroup>s, and carry information about other attributes, such as selected or disabled. The component would need to be adjusted to accommodate these.

Figure 3b.

Hiding and Showing

In the context of this example, selecting “Other” from the dropdown should reveal a new text input in which the user could specify a dietary consideration. Subscribing to the React line of thinking, this should be managed using the <Form /> component’s state. The change will be handled in a new method, handleChangeDiet, which is passed to <Dropdown /> as a property. Notice how the dropdown’s options are passed in as an array, and looped through in the render method. This can be enforced using prop validation and helps to separate concerns amongst components.

var React = require('react');

// Dropdown component
var Dropdown = React.createClass({
    render: function() {
        return (
            <label htmlFor={this.props.id}>{this.props.title}
                {/* Set 'onChange' to the passed-in change handler */}
                <select id={this.props.id} name={this.props.id} onChange={this.props.handleChange}>
                    {this.props.options.map(function(option, i) {
                        return <option val={option} key={i}>{option}</option>
                    })}
                </select>
            </label>
        );
    }
});

// InputGroup component
var InputGroup = React.createClass({
    render: function() {
        return (
            <label htmlFor={this.props.id}>{this.props.title}
                <input id={this.props.id} name={this.props.id} type={this.props.type} />
            </label>
        );
    }
});

// Form component
var Form = React.createClass({
    getInitialState: function() {
        return {
            otherDiet: false
        };
    },
    handleChangeDiet: function(event) {
        this.setState({
            otherDiet: event.target.value == 'Other'
        });
    },
    render: function() {
        return (
            <form>
                <fieldset>
                    <legend>RSVP</legend>

                    <InputGroup id={'name'} type={'text'} title={'Name'} />
                    <InputGroup id={'email'} type={'email'} title={'Email Address'} />

                    <Dropdown
                        id={'diet'}
                        type={'select'}
                        title={'Dietary Considerations'}
                        options={DIETARY_OPTIONS}
                        handleChange={this.handleChangeDiet} /> {/* Set up the change handler */}

                    {this.state.otherDiet
                        ? <InputGroup id={'otherDiet'} type={'text'} title={'Please Specify'} />
                        : null
                    }

                    <input type='submit' />
                </fieldset>
            </form>
        );
    }
});

var DIETARY_OPTIONS = ['None', 'Gluten-free', 'Nut-free', 'Vegan', 'Other'];

React.render(<Form />, document.getElementById('myForm'));
Figure 4a.

Output.

Figure 4b.

Repeatable Sections

Another feasible expectation for this example form is a way to share it with multiple people, in this case it will be as few as one or as many as five. A repeatable section can be used to to meet this need, so it seems worthwhile to explore creating a new component —<Repeatable />. But handling this in React seems to require some interpretation regarding how state should be used in React. It is clear that state should include data used in triggering a UI update, but it should not include React components or duplicated data from the props.

State Concerns ( React docs )

State should contain data that a component’s event handlers may change to trigger a UI update.

Try to use props as the source of truth where possible. One valid use to store props in state is to be able to know its previous values, because props can change over time.

While reasoning about implementing a repeatable component, it becomes clear that keeping track of the UI that needs to be rendered would involve storing an array of the children elements. In this scenario that is any <InputGroup /> components (or other elements) that are passed to <Repeatable />. Adding elements to or removing elements from this array should trigger a UI update, making it stateful. This line of thinking works against the notion that state should not include other components or duplicated data from props. It isn’t cut and dry, but this is one way to implement a <Repeatable /> component.

var React = require('react');

// Repeatable component
var Repeatable = React.createClass({
    getInitialState: function() {
        // The initial state is a copy of the passed-in children elements
        return {
            repeatedItems: [new Array().concat(this.props.children)]
        };
    },
    handleClick: function(cmd) {
        var newItems = this.state.repeatedItems;

        // The list is added to or removed from based on the user controls
        if (cmd == 'inc' && newItems.length < this.props.maxRepeat) {
            newItems.push(new Array().concat(this.props.children));
        } else if (cmd == 'dec' && newItems.length > this.props.minRepeat) {
            newItems.splice(-1,1);
        }

        // The list is updated and the UI re-rendered
        this.setState({
            repeatedItems: newItems
        });
    },
    render: function() {
        var titleRepeat = this.props.titleRepeat;
        // Since the children are an array of elements, loop through twice
        // This is brittle in handling children with nested elements
        return (
            <div>
                {this.state.repeatedItems.map(function(itemGroup, i) {
                    return (
                        <fieldset key={i + 1}>
                            <legend>{titleRepeat}</legend>

                            {itemGroup.map(function(item) {
                                return React.cloneElement(item, {id: item.props.id + '-' + (i + 1)});
                            })}
                        </fieldset>
                    );
                })}

                {/* User controls */}
                <a onClick={this.handleClick.bind(this, 'dec')}>Remove</a>
                <a onClick={this.handleClick.bind(this, 'inc')}>Add</a>
            </div>
        );
    }
});

// Dropdown component
var Dropdown = React.createClass({
    render: function() {
        return (
            <label htmlFor={this.props.id}>{this.props.title}
                <select id={this.props.id} name={this.props.id} onChange={this.props.handleChange}>
                    {this.props.options.map(function(option, i) {
                        return <option val={option} key={i}>{option}</option>
                    })}
                </select>
            </label>
        );
    }
});

// InputGroup component
var InputGroup = React.createClass({
    render: function() {
        return (
            <label htmlFor={this.props.id}>{this.props.title}
                <input id={this.props.id} name={this.props.id} type={this.props.type} />
            </label>
        );
    }
});

// Form component
var Form = React.createClass({
    getInitialState: function() {
        return {
            otherDiet: false
        };
    },
    handleChangeDiet: function(event) {
        this.setState({
            otherDiet: event.target.value == 'Other'
        });
    },
    render: function() {
        return (
            <form>
                <fieldset>
                    <legend>RSVP</legend>

                    <InputGroup id={'name'} type={'text'} title={'Name'} />
                    <InputGroup id={'email'} type={'email'} title={'Email Address'} />

                    <Dropdown
                        id={'diet'}
                        type={'select'}
                        title={'Dietary Considerations'}
                        options={DIETARY_OPTIONS}
                        handleChange={this.handleChangeDiet} />

                    {this.state.otherDiet
                        ? <InputGroup id={'otherDiet'} type={'text'} title={'Please Specify'} />
                        : null
                    }

                    {/* Pass in minimum, maximum, and title for each repeating unit */}
                    <Repeatable minRepeat={1} maxRepeat={5} titleRepeat='Invitee'>
                            <InputGroup
                                id={'inviteeName'}
                                type='text'
                                title='Invite Name'
                                key={'inviteeName'} />

                            <InputGroup
                                id={'inviteeEmail'}
                                type='email'
                                title='Invitee Email'
                                key={'inviteeEmail'} />
                    </Repeatable>

                    <input type='submit' />
                </fieldset>
            </form>
        );
    }
});

var DIETARY_OPTIONS = ['None', 'Gluten-free', 'Nut-free', 'Vegan', 'Other'];

React.render(<Form />, document.getElementById('myForm'));
Figure 5a.

Output. ( View Final on CodePen)

Figure 5b.

The <Repeatable /> component could do with quite a few optimizations to make it less brittle, some of which may involve substantial refactoring. All of the components intended to be reused would benefit from enforcing the properties.

Closing Thoughts

React is a powerful tool for building interfaces, but make sure you are using the right tool for the job. Functional and aesthetic consistency within a large number of forms may warrant a framework like what has begun to form here, but a vast number of forms can be handled on a case by case basis. React is enjoyable to play with, but can become unruly very easily. There are many ways to solve a problem, and the examples presented are one potential way.

Previous Article Next Article