DEV Community

loading...

Controlled Forms with Frontend Validations using React-Bootstrap

Alec Grey
Student of code. Thrower of discs. Roller of dice. Petter of cats.
・6 min read

I've been working on my capstone project for the last couple of weeks, and with it I've had a chance to learn a lot more about react-bootstrap for putting together functional, and aesthetically pleasing web pages. One place that this framework has really helped me to up my game is in creating responsive forms. Pairing with React hooks, you can very easily make forms that store input in state, keep control form values, and display invalidations when necessary. Lets create a simple form with react & react-bootstrap to see how it's done!

Link to Repo

App Setup

We're going to build a simple form with a few fields. To start, lets initialize our app with npx create-react-app form-demo. Next we're going to add react-bootstrap to our project with either npm install --save react-bootstrap or yarn add react-bootstrap.

Because React-Bootstrap comes with specific out-of-the-box styling, it is also helpful to add vanilla-bootstrap for additional customization. To do this, start with either npm install --save bootstrap, or yarn add bootstrap, then import it into your index.js or App.js files:

// ./src/App.js
// ...other imports
import 'bootstrap/dist/css/bootstrap.min.css';
Enter fullscreen mode Exit fullscreen mode

Now that our app is set up, we can start building out our basic form.

Form Building with React-Bootstrap

Like all components, we need to use import in order to bring them in for availability in our app. Now that we have the library installed, we can easily add react-bootstrap components to our app:

// ./src/App.js
// ...other imports
import Form from 'react-bootstrap/Form';
Enter fullscreen mode Exit fullscreen mode

This convention is consistent throughout the library, but I highly suggest reviewing the documentation for specific import instructions.

Building the form follows very straightforward convention, but also keeps room open for styling choices to be mixed in. Here is the code for our form, which will be used to review food items at a restaurant:

const App = () => {
  return (
    <div className='App d-flex flex-column align-items-center'>
      <h1>How was your dinner?</h1>
      <Form style={{ width: '300px' }}>
        <Form.Group>
          <Form.Label>Name</Form.Label>
          <Form.Control type='text'/>
        </Form.Group>
        <Form.Group>
          <Form.Label>Food?</Form.Label>
          <Form.Control as='select'>
            <option value=''>Select a food:</option>
            <option value='chicken parm'>Chicken Parm</option>
            <option value='BLT'>BLT</option>
            <option value='steak'>Steak</option>
            <option value='salad'>Salad</option>
          </Form.Control>
        </Form.Group>
        <Form.Group>
          <Form.Label>Rating</Form.Label>
          <Form.Control type='number'/>
        </Form.Group>
        <Form.Group>
          <Form.Label>Comments</Form.Label>
          <Form.Control as='textarea'/>
        </Form.Group>
        <Button type='submit'>Submit Review</Button>
      </Form>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Lets break this down:

  • Following React convention, we have the div wrapping the rest of the component.
  • We wrap the entire form in a single Form component
  • Each field is grouped using the Form.Group component wrapper. This generally follows a 1:1 rule for Group:Field, but there are advanced cases such as having multiple fields on a single row where you could wrap multiple fields.
  • Use Form.Label for labelling each field. You can use added styling on the form group in order to make this display inline with your form input, but by default they will stack vertically.
  • Use Form.Control to designate the input field. Here we have a couple of options for inputs. If your field resembles an HTML input tag, you can use type='type' to determine what type of input field it will be. In our example we use type='text' and type='number'. If you will be using another HTML tag, such as a <select> tag, you can use the as='tag' designation to determine what you get. In our example we use both an as='select' and an as='textarea' to designate these.
  • To submit the form, we add a button to the bottom with a type='submit' designation. Personally, I prefer not to use the 'submit' type, as we will more than likely be overriding the default submit procedure anyway.

As you can see, we can very quickly build a form that is aesthetically pleasing, but the important next step is to make it functional!

Updating State with Form Input

Using react hooks, we're going to create 2 pieces of state: the form and the errors.

const [ form, setForm ] = useState({})
const [ errors, setErrors ] = useState({})
Enter fullscreen mode Exit fullscreen mode

The form object will hold a key-value pair for each of our form fields, and the errors object will hold a key-value pair for each error that we come across on form submission.

To update the state of form, we can write a simple function:

const setField = (field, value) => {
    setForm({
      ...form,
      [field]: value
    })
  }
Enter fullscreen mode Exit fullscreen mode

This will update our state to keep all the current form values, then add the newest form value to the right key location.

We can now add callback functions for onChange on each form field:

// do for each Form.Control:
<Form.Label>Name</Form.Label>
<Form.Control type='text' onChange={ e => setField('name', e.target.value) }/>
Enter fullscreen mode Exit fullscreen mode

As you can see, we are setting the key of 'name' to the value of the input field. If your form will be used to create a new instance in the backend, it is a good idea to set the key to the name of the field that it represents in the database.

Great! Now we have a form that updates a state object when you change the value. Now what about when we submit the form?

Checking for errors on submit

We now need to check our form for errors! Think about what we don't want our backend to receive as data, and come up with your cases. In our form, we don't want

  • Blank or null values
  • Name must be less than 30 characters
  • Ratings above 5 or less than 1
  • Comments greater than 100 characters

Using these cases, we're going to create a function that checks for them, then constructs an errors object with error messages:

const findFormErrors = () => {
    const { name, food, rating, comment } = form
    const newErrors = {}
    // name errors
    if ( !name || name === '' ) newErrors.name = 'cannot be blank!'
    else if ( name.length > 30 ) newErrors.name = 'name is too long!'
    // food errors
    if ( !food || food === '' ) newErrors.food = 'select a food!'
    // rating errors
    if ( !rating || rating > 5 || rating < 1 ) newErrors.rating = 'must assign a rating between 1 and 5!'
    // comment errors
    if ( !comment || comment === '' ) newErrors.comment = 'cannot be blank!'
    else if ( comment.length > 100 ) newErrors.comment = 'comment is too long!'

    return newErrors
}
Enter fullscreen mode Exit fullscreen mode

Perfect. Now when we call this, we will be returned an object with all the errors in our form.

Let's handle submit now, and check for errors. Here is our order of operations:

  1. Prevent default action for a form using e.preventDefault()
  2. Check our form for errors, using our new function
  3. If we receive errors, update our state accordingly, otherwise proceed with form submission!

now to handle submission:

const handleSubmit = e => {
    e.preventDefault()
    // get our new errors
    const newErrors = findFormErrors()
    // Conditional logic:
    if ( Object.keys(newErrors).length > 0 ) {
      // We got errors!
      setErrors(newErrors)
    } else {
      // No errors! Put any logic here for the form submission!
      alert('Thank you for your feedback!')
    }
  }
Enter fullscreen mode Exit fullscreen mode

By using Object.keys(newErrors).length > 0 we are simply checking to see if our object has any key-value pairs, or in other words, did we add any errors.

Now that we have errors, we need to display them in our form! This is where we will add our last bit of React-Bootstrap spice: Form.Control.Feedback.

Setting Invalidations and Feedback

React bootstrap allows us to add a feedback field, and to tell it what and when to display information.

On each of our forms, we will add an isInvalid boolean, and a React-Bootstrap Feedback component tied to it:

<Form.Group>
    <Form.Label>Name</Form.Label>
    <Form.Control 
        type='text' 
        onChange={ e => setField('name', e.target.value) }
        isInvalid={ !!errors.name }
    />
    <Form.Control.Feedback type='invalid'>
        { errors.name }
    </Form.Control.Feedback>
</Form.Group>
Enter fullscreen mode Exit fullscreen mode

With this added, Bootstrap will highlight the input box red upon a true value for isInvalid, and will display the error in Form.Control.Feedback.

There's one final step however! We need to reset our error fields once we have addressed the errors. My solution for this is to update the errors object in tandem with form input, like so:

const setField = (field, value) => {
    setForm({
      ...form,
      [field]: value
    })
    // Check and see if errors exist, and remove them from the error object:
    if ( !!errors[field] ) setErrors({
      ...errors,
      [field]: null
    })
  }
Enter fullscreen mode Exit fullscreen mode

Now, when a new input is added to the form, we will reset the errors at that place as well. Then on the next form submission, We can check for errors again!

Final product in action:

Demonstration gif

Thanks for reading! I hope this was helpful.

Discussion (1)

Collapse
mendistern profile image
Mendistern

Can you do it with typescript?
Getting some erorrs here:
if (!!errors[field]) //**
setErrors({
...errors,
[field]: null,
});
**No index signature with a parameter of type 'string' was found on type '{}'