Reusable form-inputs in React. Simple and flexible!

This is of course never enough.

If we want the form to be controlled, each input field need to receive an onChange property.

Then we want a label above the input to tell what field it is.

We also want the design unified with the same amount if air between and so on, so we wrap each field in another DOM-element to be able to style everything.

<div className="profile-form"> <div className="field"> <label> Name <input type="text" value={ user.

name } onChange={ e => this.

handleChange('name', e.

target.

value) } /> </label> </div> <div className="field"> <label> E-mail <input type="email" value={ user.

email } onChange={ e => this.

handleChange('email', e.

target.

value) } /> </label> </div> <div className="field"> <label> Phone <input type="text" value={ user.

phone } onChange={ e => this.

handleChange('phone', e.

target.

value) } /> </label> </div></div>Notice how we need to repeat the pattern of reading the target value from the inputs.

We also have to repeat the property keys twice per field (in the value reference, and in the argument for the handleChange method).

This also leads to the problem that the handler- arrow function is created/replaced every time the render function is called, so everything that receives these functions will re-render even if the value for that field was not changed.

So, listen very carefully, for we will create these functions only once:// ProfileForm.

jsxclass ProfileForm extends React.

PureComponent { constructor(props) { super(props); this.

handleNameChange = this.

handleNameChange.

bind(this); this.

handleEmailChange = this.

handleEmailChange.

bind(this); this.

handlePhoneChange = this.

handlePhoneChange.

bind(this); } handleNameChange(e) { this.

props.

onChange('name', e.

target.

value); } handleEmailChange(e) { this.

props.

onChange('email', e.

target.

value); } handlePhoneChange(e) { this.

props.

onChange('phone', e.

target.

value); } render() { const { user } = this.

props; return ( <div className="profile-form"> <div className="field"> <label> Name <input type="text" value={ user.

name } onChange={ this.

handleNameChange } /> </label> </div> <div className="field"> <label> E-mail <input type="email" value={ user.

email } onChange={ this.

handleEmailChange } /> </label> </div> <div className="field"> <label> Phone <input type="text" value={ user.

phone } onChange={ this.

handlePhoneChange} /> </label> </div> </div> ); }}Now we have a complete form component.

The problem is that the pattern of tags and logic surrounding handling of input change contains a lot of repeated code.

It should be possible to render three form fields with less than 60 lines of code.

Here is the solution I implemented for this in Moment.

1.

Input componentsWe make our own “API” for how we want to interact with inputs in our web views.

In 99.

9% of the cases, you don’t need anything other than the value of the input from the event object.

So no point in sending out the whole event object all the time.

Let’s just send the value.

Then we can add other reusable logic that we don’t want to repeat, like showing a text length counter, handling enter press (with an event instead of adding key-code comparison everywhere) and similar things.

// StringInput.

jsxexport default class StringInput extends React.

PureComponent { constructor(props) { super(props); this.

handleChange = this.

handleChange.

bind(this); } handleChange(e) { this.

props.

onChange && this.

props.

onChange(e.

target.

value); } render() { const { className, value } = this.

props; return ( <input className={ className } type="text" value={ value } onChange={ this.

handleChange } /> ); }}Now we have turned this:<input type="text" value={ user.

name } onChange={ e => this.

handleChange(e.

target.

value) } />into this:<StringInput value={ user.

name } onChange={ this.

handleChange } />2.

Field componentsIn addition to labels and wrapper divs, we will probably need more view logic for each field.

Like showing validation errors.

Every element adds more boilerplate and when making large forms with a high number of fields (not just three simple user profile fields), this leads to some long and ugly render methods.

Let’s wrap our inputs in some Field components.

// StringField.

jsxfunction StringField({ label, .

inputProps }) { return ( <div className="string-field field"> { label && <label>{ label }</label> } <StringInput { .

inputProps } /> </div> );}Now, every field can be rendered with label and wrapping field-div with just one line:<StringField label="Name" value={ user.

name } />Why not just have field-components instead of one input and one field?.For flexibility!.We want to be able to put inputs straight up without labels, error messages and so on in components where that is needed, but we don’t want to repeat all the logic inside the input components.

We want our defined input component API everywhere.

3.

The magical EntityDataWhy should we repeat the property key for what part of the source entity to use several times per field?.The sources of data for forms like these are usually JSON-data from a server.

An object with a set of properties, possibly deep structures.

In any case, all data is located at a given path inside an object, that can be accessed using tools like lodash get.

So instead of passing values to each input by referring to the source object (like value={ user.

name }), let’s just send the path to where the data is, and a reference to the source object.

<StringInput source={ user } path="name" />And then we add the path as argument to onChange calls from the component so surrounding logic can have one function handle updates from multiple inputs:handleChange(e) { const { path, onChange } = this.

props; onChange && onChange(path, e.

target.

value);}Possibly add a separate onValueChange etc for handling direct one-argument events for functional programming chains/compilations that only need the value.

However, this will require sending the source object in as prop to every input instead.

Same as the onChange handler.

Do you see a pattern emerging here?The repeated properties is a nice opportunity for using the React Context.

The old context system in React was a bit like walking in the woods late at night.

It could contain a lot of strange stuff you had no idea why was there, and you could collide with other things.

The new context is much better.

We can wrap all our fields in an EntityData component, that has a ContextProvider, and then wrap the input components in a HOC for reading this context.

Here is a simple example:// EntityData.

jsxconst EntityDataContext = React.

createContext();export function withEntityData(Component) { return function EntityDataComponent(props) { return ( <EntityDataContext.

Consumer> { entityProps => { const source = props.

source || entityProps.

source; const path = props.

path; const sourceValue = _.

get(path, source); const value = props.

value !== undefined?.props.

value : sourceValue; const onChange = props.

onChange || entityProps.

onChange; return ( <Component { .

props } source={ source } path={ path} value={ value } /> onChange={ onChange } /> ); }} </EntityDataContext.

Consumer> ); };}export default class EntityData extends React.

PureComponent { render() { return ( <EntityDataContext.

Provider value={{ source: this.

props.

source, onChange: this.

props.

onChange }}> { this.

props.

children } </EntityDataContext.

Provider> ); }}// StringInput.

jsximport { withEntityData } from '.

/EntityData';class StringInput extends React.

PureComponent { // .

Same as before}export default withEntityData(StringInput);Now we can simplify this:<StringField label="Name" value={ user.

name } onChange={ value => this.

handleChange('name', value) } /><StringField label="E-mail" value={ user.

email } onChange={ value => this.

handleChange('email', value) } /><StringField label="Phone" value={ user.

phone } onChange={ value => this.

handleChange('phone', value) } />into this:<EntityData source={ user } onChange={ this.

handleChange }> <StringField label="Name" path="name" /> <StringField label="E-mail" path="email" /> <StringField label="Phone" path="phone" /></EntityData>The source and onChangeargument is sent to all child components of the EntityData component (that is wrapped with the withEntityData HOC).

4.

Flexibility (values etc)For full flexibility of use, we still need to be able to send values, sources/paths and onChange (plus any other events/attributes) into our input components if we are using them in a non-EntityData implementation.

So let’s have the inputs read the value-prop as first priority, then the source/path props for reading from an entity if the value is not set, and third priority is the context data:render() { const { value, path, source } = this.

props; const inputValue = (value !== undefined?.value : _.

get(path, source)) || ''; return ( <input type="text" value={ inputValue } /> );}These properties are then received through the EntityData HOC, so any source/onChange argument to the EntityData will be sent into the input components inside, but if they receive source/onChange properties directly, that will override the ones from EntityData.

And, since the onChange calls from within the inputs send the path as first argument, any function (like a redux action) that can take a path and a value can be used to handle the data from the input.

This avoids the need for separate handler functions per field, especially when it is for manipulating properties of the same entity object.

So now we can simplify the ProfileForm from above down to this:class ProfileForm extends React.

PureComponent { render() { const { user, onChange } = this.

props; return ( <div className="profile-form"> <EntityData source={ user } onChange={ onChange }> <StringField label="Name" path="name" /> <StringField label="E-mail" path="email" /> <StringField label="Phone" path="phone" /> </EntityData> </div> ); }}Our ~65 line form component is reduced to ~15 lines.

WOHOO!.Now we just need a flexible but boilerplate-limited way of handling http requests to the server and their state.

But let’s leave that for another article.

.

. More details

Leave a Reply