Create a FormBuilder component in React Native (Part 4)
Vasile Stefirta π²π© βοΈ πΊπΈ
Posted on March 18, 2019
This series contents:
- Part 1: Create a new React Native app
- Part 2: Create a simple Salary Calculator Form
- Part 3: Create custom form input and button components
- Part 4: Work on the
FormBuilder
component (current) - Part 5: Enable/disable form buttons on-the-fly
- Part 6: Create a Sign Up form
- Part 7: Add support for Boolean field type
Part 4: Work on the FormBuilder
component
We have reached the point where we have a fully functional form. Now let's try to build out a separate component which can render that form for us. We'll just need to instruct it what fields to render out and how should it handle the form submission.
Define the form's configuration
Let's start by generating a JavaScript array which will be our FormBuilder
's component configuration
. In our App.js
file we'll define a class property as an arrow function which returns our desired configuration like so:
getFormFields = () => {
const inputProps = {
placeholder: '0',
autoCapitalize: 'none',
autoCorrect: false,
keyboardType: 'numeric',
returnKeyType: 'done',
};
const formFields = [
[
{
name: 'hourlyRate',
label: 'Hourly Rate',
type: 'text',
inputProps,
},
{
name: 'hoursPerWeek',
label: 'Hours / Week',
type: 'text',
inputProps,
},
],
[
{
name: 'daysPerWeek',
label: 'Days / Week',
type: 'text',
inputProps,
},
],
];
return formFields;
};
Basically, our configuration is a list of rows with objects that describe each form field to be rendered. Why do we need those rows? Simply to allow us to control the form's layout. Each form field within a row will be rendered inline, and then a new row will start from a new line. In our example, hourlyRate
and hoursPerWeek
will be rendered inline, but daysPerWeek
will be rendered in a new line and will take the full width of the row.
Modify our handleSubmit
class property
We'll make a couple of changes to our function that handles the form submission like so:
- don't use the
App.js
component's state anymore. Because ourFormBuilder
component will take care of collecting data from our form fields, we don't need a local state anymore in ourApp.js
component. Instead, we'll have a local state within ourFormBuilder
component, and that state will be passed to ourhandleSubmit
function; - remove validation for required fields. We'll delegate that job to our
FormBuilder
component as well; - add support for our new
daysPerWeek
form field (which wasn't used in our previous blog posts); - include
weeklyIncome
as part of the result.
The modified version should look like this:
handleSubmit = (state) => {
// using Javascript object destructuring to
// get user's input data from the state.
const { hourlyRate, hoursPerWeek, daysPerWeek } = state;
const weeksPerYear = 52;
const hoursPerDay = Math.ceil(parseFloat(hoursPerWeek) / parseFloat(daysPerWeek));
const weeklyIncome = Math.abs(
parseFloat(hourlyRate) * hoursPerDay * parseFloat(daysPerWeek),
);
const annualIncome = Math.abs(
parseFloat(hourlyRate) * parseFloat(hoursPerWeek) * weeksPerYear,
);
// show results
Alert.alert(
'Results',
`Weekly Income: $${weeklyIncome}, \n Annual Income: $${annualIncome}`,
);
};
Modify our render()
function
Let's modify our render()
function by removing all of the form inputs and buttons, and then use the FormBuilder
component instead (which we'll create in just a just a moment).
render() {
return (
<KeyboardAvoidingView behavior="padding" style={styles.container}>
<Text style={styles.screenTitle}>Salary Calculator</Text>
<FormBuilder
formFieldsRows={this.getFormFields()}
handleSubmit={this.handleSubmit}
submitBtnTitle="Calculate"
/>
</KeyboardAvoidingView>
);
}
As you can see our FormBuilder
component accepts 3 props:
-
formFieldsRows
- our desired list of form fields; -
handleSubmit
- the function to be run when the user submits the form; -
submitBtnTitle
- the submit button's title. We'll make this optional and have a default value within the form component itself.
Create the FormBuilder
component
The time has come! ππ Let's create our helper component π. For now, we'll take a look at how the final code looks like, and then we'll walk through it and talk about functionality. Ladies and gentlemen, may I present you the 1st version of our FormBuilder
ππ(I'm sorry, I just got too excited here for a second π):
import React from 'react';
import PropTypes from 'prop-types';
import {
View, StyleSheet, Keyboard, Alert,
} from 'react-native';
import FormTextInput from './FormTextInput';
import FormButton from './FormButton';
/**
* A component which renders a form based on a given list of fields.
*/
class FormBuilder extends React.Component {
/* eslint-disable no-param-reassign */
constructor(props) {
super(props);
const formFields = this.getFormFields();
// dynamically construct our initial state by using
// each form field's name as an object property.
const formFieldNames = formFields.reduce((obj, field) => {
obj[field.name] = '';
return obj;
}, {});
// define the initial state, so we can use it later on
// when we'll need to reset the form
this.initialState = {
...formFieldNames,
};
this.state = this.initialState;
}
/* eslint-enable no-param-reassign */
/**
* Extract our form fields from each row
* and compose one big list of field objects.
*/
getFormFields = () => {
const { formFieldsRows } = this.props;
const formFields = [];
formFieldsRows.forEach((formFieldsRow) => {
formFields.push(...formFieldsRow);
});
return formFields;
};
/**
* Check if all fields have been filled out.
*/
/* eslint-disable react/destructuring-assignment */
hasValidFormData = () => {
const formFields = this.getFormFields();
const isFilled = formFields.every(field => !!this.state[field.name]);
return isFilled;
};
/* eslint-enable react/destructuring-assignment */
/**
* Attempt to submit the form if all fields have been
* properly filled out.
*/
attemptFormSubmission = () => {
const { handleSubmit } = this.props;
if (!this.hasValidFormData()) {
return Alert.alert('Input error', 'Please input all required fields.');
}
return handleSubmit(this.state);
};
/**
* Reset the form and hide the keyboard.
*/
resetForm = () => {
Keyboard.dismiss();
this.setState(this.initialState);
};
/* eslint-disable react/destructuring-assignment */
renderTextInput = ({ name, label, inputProps }) => (
<FormTextInput
{...inputProps}
value={this.state[name].toString()}
onChangeText={(value) => {
this.setState({ [name]: value });
}}
labelText={label}
key={name}
/>
);
/* eslint-enable react/destructuring-assignment */
render() {
const { submitBtnTitle, formFieldsRows } = this.props;
return (
<View>
{/* eslint-disable react/no-array-index-key */}
{formFieldsRows.map((formFieldsRow, i) => (
<View style={styles.row} key={`r-${i}`}>
{formFieldsRow.map(field => this.renderTextInput(field))}
</View>
))}
{/* eslint-enable react/no-array-index-key */}
<FormButton onPress={this.attemptFormSubmission}>{submitBtnTitle}</FormButton>
<FormButton onPress={this.resetForm}>Reset</FormButton>
</View>
);
}
}
FormBuilder.propTypes = {
handleSubmit: PropTypes.func.isRequired,
submitBtnTitle: PropTypes.string,
formFieldsRows: PropTypes.arrayOf(
PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
label: PropTypes.string,
type: PropTypes.string,
inputProps: PropTypes.object,
}),
),
).isRequired,
};
FormBuilder.defaultProps = {
submitBtnTitle: 'Submit',
};
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
},
});
export default FormBuilder;
As for any other components we've created before, we'll define the propTypes
our component accepts. Two of those are required - handleSubmit
and formFieldsRows
(pay attention on how we require a very specific format for our configuration array), and one is optional - submitBtnTitle
- with a default value of Submit
.
Within our component, we'll be using some helper functions. Let's list them out and briefly described their purpose:
-
getFormFields
- because our component receives a list forform field rows
and not just a simple list ofform fields
, we'll define this helper function which will make that conversion for us. You can see how this gets handy when we use it in our classconstructor
when we're trying to dynamically define our initial state, as well as when we need to validate our form data within ourhasValidFormData
helper function. -
hasValidFormData
- this is a very simple implementation of form validation. We're basically checking if the user filled out all our form fields. JavaScript array method every() is a perfect fit for our use-case. -
attemptFormSubmission
- a helper function which describes the action that needs to happen when the user submits the form. Here we make use of our validation function we described above, and if the validation passes, then we'll call ourhandleSubmit
function passed down as a prop fromApp.js
. -
resetForm
- a helper function which describes the action that needs to happen when the user taps theReset
button. Basically, this will reset our state (which leads to resetting all form fields) and hide the keyboard. -
renderTextInput
- this helper function simply renders out aFormTextInput
component by accepting a form field object as a parameter. Note how we unpack/destructure our form field object right away within the function's parameters list -({ name, label, inputProps }) => (
. This is just a very handy ES6 feature. The main reason we moved this logic into a separate function is to keep things nice and tidy within our component'srender()
function, especially if we'll add support for other field types down the road (e.g., textarea, checkboxes etc.)
Just like that, we have defined a good list of helper functions which are being used within our component.
I think it's worth mentioning the usage of JavaScript Array reduce() method within our constructor
which basically loops through every single form field and produces a final object with the field name as property and an empty string as a value. That's basically our component's initial state.
const formFieldNames = formFields.reduce((obj, field) => {
obj[field.name] = '';
return obj;
}, {});
The last piece we need to look at is the implementation of the render()
function. The JSX content here looks pretty nice and tidy. We simply loop through our formFieldsRows
list and render out all form inputs, as well as including the submit and reset buttons.
After all these changes, the final version of our form should look like this:
For a full list of changes, check out this commit on GitHub.
Great job if you've made it this far π. Let's have some more fun by playing around and add some more functionality to our component. π¨βπ»π Do you like this idea? Then check out Part 5 of this series.
Posted on March 18, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.