loading...
Cover image for Create a FormBuilder component in React Native (Part 4)

Create a FormBuilder component in React Native (Part 4)

dev_nope profile image Vasile Stefirta πŸ‡²πŸ‡© ✈️ πŸ‡ΊπŸ‡Έ ・7 min read

This series contents:

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 our FormBuilder component will take care of collecting data from our form fields, we don't need a local state anymore in our App.js component. Instead, we'll have a local state within our FormBuilder component, and that state will be passed to our handleSubmit 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 for form field rows and not just a simple list of form 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 class constructor when we're trying to dynamically define our initial state, as well as when we need to validate our form data within our hasValidFormData 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 our handleSubmit function passed down as a prop from App.js.
  • resetForm - a helper function which describes the action that needs to happen when the user taps the Reset 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 a FormTextInput 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's render() 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:

Salary Calculator V2

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.

Discussion

pic
Editor guide