DEV Community

Cover image for Let's build a 3-in-1 form field component
Gedalya Krycer
Gedalya Krycer

Posted on

Let's build a 3-in-1 form field component

Three for the price of one...

In this post, we are going to build a robust contact form with validation – using one input field component!

Why is this helpful? If you ever need to change the styles or functionality globally, you can do so in just this file.

I'd love to show you how it works today!

What we are going to build:

Form Filling Out Demo


How to build the component

We are going to start by building our custom component InputField. Once that is set up, we will look at styling and the parent Form component that holds everything.

Steps

  1. Set up InputField base code

  2. Add the first input field into the if statement

  3. Add the second input field into the if statement

  4. Add the final input field into the if statement

  5. Add input validator helper function

  6. Add InputField styles

  7. Setup Contact Form parent component

  8. Add our custom InputField components


1 β€” Set up InputField base code

import React from 'react';
import './inputFieldStyles.scss';

const InputField = props => {


    if (props.type === "submit") {
        return (

        )
    } else if (props.type === "textarea") {
        return (


        );
    } else {
        return (

        );
    }


};

export default React.memo(InputField);
Enter fullscreen mode Exit fullscreen mode

Breakdown

  • We start by importing React and an SCSS stylesheet.

  • Inside our InputField component we will use an if statement to determine what type of input element we want to render.

  • Our component will receive multiple props and the first one is props.type. Among other places, we will use type to choose the correct input.

  • At the bottom, we export the component and wrap around the Higher-Order React component memo. This will make sure our component won't re-render if its props haven’t change.

Back to πŸ”


2 β€” Add the first input field into the if statement

import React from 'react';
import './inputFieldStyles.scss';

const InputField = props => {


    if (props.type === "submit") {
        return (

        )
    } else if (props.type === "textarea") {
        return (


        );
    } else {
        return (
            <label className="inputField__label">
                {props.label}
                <input
                    onChange={(e) => props.onChangeHandler(e.target.value)}
                    type={props.type}
                    placeholder={props.placeholder}
                    value={props.value}
                    required={props.isRequired}
                    className="inputField__field"
                    name={props.name}
                />
            </label>
        );
    }


};

export default React.memo(InputField);
Enter fullscreen mode Exit fullscreen mode

Breakdown

  • Starting from the bottom else statement we have added our first possible input field to render.

  • It is wrapped in a <label>, with a props.label so we can dynamically pass in a name as a string. This text will appear above the form field and will also focus on the field if clicked.

  • The onChange holds props.onChangeHandler which passes back the input field's data to the parent form component.

  • The type holds the props.type. In this instance, it is used to tell if this field's functionality should be for an email, text, tel, etc

  • The placeholder holds the props.placeholder string and will show some greyed-out text before the user types.

  • The value holds the props.value which is actually the parent passing back in the onChangeHandler. This will show the text inside the field in a controlled way.

  • The required holds a boolean, which is passed in via props.isRequired. If this is added in the parent component, the field will be required. If left off it won't.

  • The name is passed in via props.name. This is especially helpful with a Netlify mail server.

Back to πŸ”


3 β€” Add the second input field into the if statement

import React from 'react';
import './inputFieldStyles.scss';

const InputField = props => {


    if (props.type === "submit") {
        return (

        )
    } else if (props.type === "textarea") {
        return (
           <label className="inputField__label">
                {props.label}
                <textarea
                    onChange={(e) => props.onChangeHandler(e.target.value)}
                    placeholder={props.placeholder}
                    value={props.value}
                    required={props.isRequired}
                    className="inputField__field"
                    rows={7}
                    name={props.name}
                />
            </label>
        );
    } else {
        return (
            <label className="inputField__label">
                {props.label}
                <input
                    onChange={(e) => props.onChangeHandler(e.target.value)}
                    type={props.type}
                    placeholder={props.placeholder}
                    value={props.value}
                    required={props.isRequired}
                    className="inputField__field"
                    name={props.name}
                />
            </label>
        );
    }


};

export default React.memo(InputField);
Enter fullscreen mode Exit fullscreen mode

Breakdown

  • Moving up to the else if statement we have now added our <textarea> field to render.

  • The props it receives are very similar to the input field below it, with one addition.

  • The rows does not receive a prop in my example, but totally can if you wish to make it dynamic. The number placed as its value will determine how tall the <textarea> is. The above example will support 7 lines of user text.

Back to πŸ”


4 β€” Add the final input field into the if statement

import React from 'react';
import './inputFieldStyles.scss';

const InputField = props => {


    if (props.type === "submit") {
        return (
            <input
                className='primaryBtn primaryBtn--big g__justify-self-center'
                type='submit'
                value={props.label}
                disabled={validateInput(props.formValues)}
            />
        )
    } else if (props.type === "textarea") {
        return (
           <label className="inputField__label">
                {props.label}
                <textarea
                    onChange={(e) => props.onChangeHandler(e.target.value)}
                    placeholder={props.placeholder}
                    value={props.value}
                    required={props.isRequired}
                    className="inputField__field"
                    rows={7}
                    name={props.name}
                />
            </label>
        );
    } else {
        return (
            <label className="inputField__label">
                {props.label}
                <input
                    onChange={(e) => props.onChangeHandler(e.target.value)}
                    type={props.type}
                    placeholder={props.placeholder}
                    value={props.value}
                    required={props.isRequired}
                    className="inputField__field"
                    name={props.name}
                />
            </label>
        );
    }


};

export default React.memo(InputField);
Enter fullscreen mode Exit fullscreen mode

Breakdown

  • Moving up to the top if statement we have now added our <input type="submit"> field to render.

  • This input will be the submit button for our forms.

  • The value takes in a props.label because this is technically the label or button text. (Such as "Submit", "Send", "Confirm", etc)

  • The disabled method takes in a custom function that also passes in an array from props called props.formValues. This will be explained in the next step.

Back to πŸ”


5 β€” Add input validator helper function

import React from 'react';
import './inputFieldStyles.scss';

const InputField = props => {

  const validateInput = values => {
        if (values.some(f => f === "") || values[0].indexOf("@") === -1) {
            return true
        } else {
            return false
        }
    }

    if (props.type === "submit") {
        return (
            <input
                className='primaryBtn primaryBtn--big g__justify-self-center'
                type='submit'
                value={props.label}
                disabled={validateInput(props.formValues)}
            />
        )
    } else if (props.type === "textarea") {
        return (
           <label className="inputField__label">
                {props.label}
                <textarea
                    onChange={(e) => props.onChangeHandler(e.target.value)}
                    placeholder={props.placeholder}
                    value={props.value}
                    required={props.isRequired}
                    className="inputField__field"
                    rows={7}
                    name={props.name}
                />
            </label>
        );
    } else {
        return (
            <label className="inputField__label">
                {props.label}
                <input
                    onChange={(e) => props.onChangeHandler(e.target.value)}
                    type={props.type}
                    placeholder={props.placeholder}
                    value={props.value}
                    required={props.isRequired}
                    className="inputField__field"
                    name={props.name}
                />
            </label>
        );
    }


};

export default React.memo(InputField);
Enter fullscreen mode Exit fullscreen mode

Breakdown

  • This function is used in the input type="submit"disabled field.

  • It takes in an array of all the form values. This was passed down as props from the parent component. It's important to note that the email value will always be the first item in this array.

  • The function checks if any of the values in the array empty using the .some() method. If true, then the function will return true and the button will be disabled.

  • It then checks if the email value contains an "@". If not, then the function will return true and the submit input will also be disabled.

  • In all other cases the function will return false and the submit input will *not be disabled. (Remember that disabled={false} will keep the input active.)

Back to πŸ”


6 β€” Add InputField styles

@use "../../../sassStyles/_variables" as v;
@use "../../../sassStyles/_mixins" as m;

.inputField__label {
  display: grid;
  grid-row-gap: 10px;
  color: v.$secondary2;
  font-size: 16px;
  margin: 0 auto;
  width: 100%;
  max-width: 400px;
  @include m.poppinsFontStack;

  @include m.smMinBreakPoint {
    font-size: 18px;
  }
}

.inputField__field {
  @include m.poppinsFontStack;
  background-color: v.$primaryDark3;
  border: none;
  font-size: 16px;
  padding: 16px 20px;
  margin: 0 auto;
  width: 100%;
  max-width: 400px;
  font-weight: bold;
  color: v.$secondary2;

  @include m.smMinBreakPoint {
    font-size: 18px;
    padding: 20px 25px;
  }
}

::placeholder { /* Firefox */
  font-weight: normal;
  color: v.$primary
}

:-ms-input-placeholder { /* Internet Explorer 10-11 */
  color: v.$primary;
  font-weight: normal;
}

::-ms-input-placeholder { /* Microsoft Edge */
  color: v.$primary;
  font-weight: normal;
}

input[disabled] {
  background-color: v.$primaryDark2;
  cursor: default;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);

  &:hover {
    background-color: v.$primaryDark2;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
    transform: scale(1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Breakdown

  • These styles are applied to the labels, inputs, placeholders, and even the disabled states.

  • I am importing SCSS mixins for pre-determined breakpoints and variables for colors. But you can easily replace them with media queries and hex color codes.

Back to πŸ”


7 β€” Setup Contact Form parent component

import React, {useState} from 'react';
import './contactFormStyles.scss';
import InputField from "../../ui/InputField/InputField";

const ContactForm = props => {

    const [email, setEmail] = useState('');
    const [name, setName] = useState('');
    const [message, setMessage] = useState('');

    const coolFunctionHandler = e => {
      // your code here
    }


    return (
      <form className="mc__form" onSubmit={(e) => coolFunctionHandler(e)}>



      </form>
    )
}

export default ContactForm;
Enter fullscreen mode Exit fullscreen mode

Breakdown

  • This component is the base for the Contact Form.

  • We are importing React, styles, and our custom InputForm components

  • We are setting up states for each input field in our form. (Not including the submit input). These will hold the values that our users enter.

  • The onSubmit on the <form> will can contain any next steps you want to happen once the form is submitted.

Back to πŸ”


7 β€” Add our custom InputField components

import React, {useState} from 'react';
import './contactFormStyles.scss';
import InputField from "../../ui/InputField/InputField";

const ContactForm = props => {

    const [email, setEmail] = useState('');
    const [name, setName] = useState('');
    const [message, setMessage] = useState('');

    const coolFunctionHandler = e => {
      // your code here
    }


    return (
      <form className="mc__form" onSubmit={(e) => coolFunctionHandler(e)}>

        <InputField
          label="Name"
          onChangeHandler={setName}
          type="text"
          value={name}
          placeholder="Jane Smith"
          isRequired
          name="name"
        />

        <InputField
          label="Email"
          onChangeHandler={setEmail}
          type="email"
          value={email}
          placeholder="your@email.com"
          isRequired
          name="email"
        />

        <InputField
          label="Message"
          onChangeHandler={setMessage}
          type="textarea"
          value={message}
          placeholder="How can we help..."
          isRequired
          name="message"
        />

        <InputField
          label="send"
          type="submit"
          formValues={[email, name, message]}
        />

      </form>
    )
}

export default ContactForm;
Enter fullscreen mode Exit fullscreen mode

Breakdown

  • Now we add in our custom InputField components and pass in the prop values that we previously set up.

  • Note how the last <InputField /> takes in an array on the formValues prop, with email being the first item. This is for the validation and making sure it isn't active if there is a single missing field or invalid email entry.

Back to πŸ”


Summary

It definitely took a few steps, but now you have a super robust component to use across all your website's forms! Over the long run, this setup will save a lot of time.

Happy Coding! πŸ€“


Thumbnail designed with Figma

Top comments (4)

Collapse
 
lyqht profile image
Estee Tey

what are your thoughts when we start to have more InputField types that we want to provide as a component? e.g. radio, checkbox, combo etc. how would you address the code smell of switch statements in this case?

Collapse
 
gedalyakrycer profile image
Gedalya Krycer

To be honest I am not very familiar with "code smell of switch statements". Could you further elaborate on what the potential issues would be by utilizing a switch statement?

Collapse
 
lyqht profile image
Estee Tey

Sure! This is a good resource on the Switch Statement code smell.

In the example you have given, you were checking the if condition props.type === someType on this component and return the appropriate component. For 3 types of components, the code seems fine, but if we expand the InputField component to have more types, it will become difficult to navigate this code. And for other types of InputField components, you might also want different validation instead of a catch-all validateInput() method.

So i'm just curious how would you address these issues πŸ˜„

Thread Thread
 
gedalyakrycer profile image
Gedalya Krycer

These are good points. To be honest I’m not sure. :) I suspect it might come down to breaking out the more complex form fields into their own components.