Creating a generic text-input component with React

Gracefully managing forms and input values in React State (Part II)

image

Creating a generic text-input component with React

Gracefully managing forms and input values in React State (Part II)

This post is the second part of my previous post: How to Handle Forms and Inputs with React. This post is a guide on how to make a text input compatible with the Form we made in the last post. In case you missed the post, here are a few highlights:

  • Make a class component: Form that holds the data for all input fields.

  • Share field data through the context API.

  • Add methods in the Form component to update (setField) and add new fields (addField).

  • Add a method to validate fields and provide few pre-defined validation rules.

Now, we will make a text-input field: TextInput. This component will:

  • Accept initial value and configs from the developer and register itself to the Form.

  • Be rendered as text-input or textarea based on the props value.

  • Call custom onChange function after saving its value to the Form (optional).

  • Validate itself on the field value change (optional).

Let’s start with the basics. Let’s create the base input component that renders a text input field:

import React from "react";

const TextInput = (props) => {
  return (
    <div>
      <input
        type="text"
        value={props.value}
        onChange={(event) => console.log("value changed!")}
      />
      <p>// place for errors</p>
    </div>
  );
};

export default TextInput;

The component above is the simplest example of the TextInput field. It renders an input field and sets its value as passed in props. Now let’s take all the steps one-by-one and make the field more functional:

  • Accepts initial value and configs from the developer and register itself to the Form:

In this step, we will pass the default configs of the TextInput field. Default configs can be something like the field ID, default value, placeholder, field styles, error styles, validation rules. And then, maybe a few function callbacks like onChange, onError. We will use the function: addField provided by the Form in context. Let’s add code for this to the component:

import React, { useContext, useEffect } from "react";
import { FormCtx } from "./Form";

const TextInput = (props) => {
  const { setFields, addField } = useContext(FormCtx);
  const {
    id,
    value,
    classes,
    onChange,
    validate,
    placeholder,
    label = "",
  } = props;
  const { contClass, fieldClass, errorClass } = classes;

  useEffect(() => {
    addField({
      field: props,
      value,
    });
  }, []);

  return (
    <div class={contClass}>
      {label}
      <input
        id={id}
        type="text"
        value={value}
        class={fieldClass}
        onChange={onChange}
        validatge={validate}
        placeholder={placeholder}
      />
      <p class={errorClass}>// place for errors</p>
    </div>
  );
};

export default TextInput;

Although we are rendering props to make the component dynamic, if we see closely, you will find that we are not using the Form provided field data in the TextInput. And that means we are omitting the “Single Source Of Truth” rule — All the data should be stored in one place, and all components should refer to that data to render dynamic parts of the application. Let’s fix this issue and get all the data from the ‘Form’ component:

import React, { useContext, useEffect } from "react";
import { FormCtx } from "./Form";

const TextInput = (props) => {
  const { id } = props;
  const { setFields, addField, fields } = useContext(FormCtx);
  const field = fields[id] || {};
  const {
    value,
    classes,
    validate,
    placeholder,
    label = "",
    events = {},
  } = field;
  const { onChange, ...restEvents } = events;
  const { contClass, fieldClass, errorClass } = classes;

  const handleChange = (event) => {
    setFields(event, field);

    if (typeof onChange === "function") {
      onChange({
        ...field,
        value: event.target.value,
      });
    }
  };

  useEffect(() => {
    addField({
      field: props,
      value,
    });
  }, []);

  return (
    <div className={contClass}>
      {label}
      <input
        {...restEvents}
        id={id}
        type="text"
        value={value}
        className={fieldClass}
        validate={validate}
        onChange={handleChange}
        placeholder={placeholder}
      />
      <p className={errorClass}>// place for errors</p>
    </div>
  );
};

export default TextInput;

I did two significant changes in the component: a) Now the component utilizes data saved at Form to render the input and its attributes, b) Instead of passing each event individually, the component accepts a prop: events. This way you can pass all native input events, and all events will be passed as-is to the input field by spreading the events object. The onChange event is also changed a little. I made a different function: handleChange that calls the setFields to update field value in Form state and then we call the user-defined events.onChange function.

  • Renders the component as text-input or textarea based on the props value:

As the native textarea and text-input, both serve the same purpose. I think it’s better to club them both into a single component and render either of them based on the user input. Let’s introduce another prop in the component — type to choose what to render:

import React, { useContext, useEffect } from "react";
import { FormCtx } from "./Form";

const TextInput = (props) => {
  const { id } = props;
  const { setFields, addField, fields } = useContext(FormCtx);
  const field = fields[id] || {};
  const {
    name,
    rows,
    value,
    validate,
    placeholder,
    label = "",
    type = "text",
    events = {},
    classes = {},
  } = field;
  const { onChange, ...restEvents } = events;
  const { contClass, fieldClass, errorClass } = classes;

  const handleChange = (event) => {
    setFields(event, field);

    if (typeof onChange === "function") {
      onChange({
        ...field,
        value: event.target.value,
      });
    }
  };

  useEffect(() => {
    addField({
      field: props,
      value,
    });
  }, []);

  const fieldProps = {
    ...restEvents,
    id,
    name,
    type,
    value,
    validate,
    placeholder,
    className: fieldClass,
    onChange: handleChange,
  };

  if (type === "textarea") {
    delete fieldProps.type;
    delete fieldProps.value;

    fieldProps.defaultValue = value;
    fieldProps.rows = rows || 2;
  }

  return field ? (
    <div className={contClass}>
      {label}
      {type === "textarea" ? (
        <textarea {...fieldProps} />
      ) : (
        <input {...fieldProps} />
      )}
      <p className={errorClass}>// place for errors</p>
    </div>
  ) : (
    ""
  );
};

export default TextInput;

In the component above, I added a condition for the component to render either text-input or a textarea based on the prop type. I have added a few component-specific props too. Like, if the value of type is textarea, we add a new prop: row. It is to specify the default row count in the textarea field. I also changed the value prop name to defaultValue when we render the textarea. You can make a separate component for textarea as well, but I prefer it this way. And in this way, I can show you how you can bend the rules according to your needs in your custom library.

  • Calls custom onChange function after saving its value to the Form:

Though we have covered the custom onChange part in earlier parts of the post. Here, We will first save the field data to the Form state and if there is no error in the process, then we will call the custom onChange function. We will also see how to add and use custom events in this part:

import React, { useContext, useEffect } from "react";
import { FormCtx } from "./Form";

const TextInput = (props) => {
  const { id } = props;
  const { setFields, addField, fields } = useContext(FormCtx);
  const field = fields[id] || {};
  const {
    name,
    rows,
    value,
    validate,
    placeholder,
    label = "",
    type = "text",
    events = {},
    classes = {},
  } = field;
  const { onChange, ...restEvents } = events;
  const { contClass, fieldClass, errorClass } = classes;

  const handleChange = (event) => {
    try {
      setFields(event, field);
    } catch (error) {
      throw error;
    }

    if (typeof onChange === "function") {
      onChange({
        ...field,
        value: event.target.value,
      });
    }
  };

  useEffect(() => {
    addField({
      field: props,
      value,
    });
  }, []);

  const fieldProps = {
    ...restEvents,
    id,
    name,
    type,
    value,
    validate,
    placeholder,
    className: fieldClass,
    onChange: handleChange,
  };

  if (type === "textarea") {
    delete fieldProps.type;
    delete fieldProps.value;

    fieldProps.defaultValue = value;
    fieldProps.rows = rows || 2;
  }

  return field && field.value !== undefined ? (
    <div className={contClass}>
      {label}
      {type === "textarea" ? (
        <textarea {...fieldProps} />
      ) : (
        <input {...fieldProps} />
      )}
      <p className={errorClass}>// place for errors</p>
    </div>
  ) : (
    ""
  );
};

export default TextInput;

Now, let’s see how to use our TextInput component in the Form component:

import React from "react";
import ReactDOM from "react-dom";

import Form from "./Form";
import TextInput from "./TextInput";

function App() {
  return (
    <div className="App">
      <Form>
        <TextInput
          id="test"
          placeholder="testing"
          validatge="numeric"
          events={{
            onChange: (data) => console.log(data),
            onFocus: (val) => console.log("focused!"),
            onBlur: (value) => console.log("blurred!"),
          }}
        />
      </Form>
    </div>
  );
}

Observe in the above component that all the events are passed in the prop: events. Mind that the event names should be the same as the native input event names. We didn’t pass the type prop in the above example. The component renders as text-input by default, but, if we add the type prop with value “textarea”. It will be rendered as a textarea.

  • Validates itself on the field value change:

Though we have covered the validation part in the previous post, now we will see how to use that in an input. We can validate the input whenever we want. But, for the sake of the post, let’s validate it when the field value changes. All we need to do is call the validateField function from the form context to validate the field:

import React, { useContext, useEffect } from "react";
import { FormCtx } from "./Form";

const TextInput = (props) => {
  const { id } = props;
  const { setFields, addField, fields, validateField, errors } =
    useContext(FormCtx);
  const field = fields[id] || {};
  const {
    name,
    rows,
    value,
    validate,
    placeholder,
    label = "",
    type = "text",
    events = {},
    classes = {},
  } = field;
  const fieldError = errors[id];

  const { onChange, ...restEvents } = events;
  const { contClass, fieldClass, errorClass } = classes;

  const handleChange = (event) => {
    try {
      setFields(event, field);
    } catch (error) {
      throw error;
    }

    if (typeof onChange === "function") {
      onChange({
        ...field,
        value: event.target.value,
      });
    }
  };

  useEffect(() => {
    if (value !== undefined) {
      validateField(id);
    }
  }, [value, id]);

  useEffect(() => {
    addField({
      field: props,
      value,
    });
  }, []);

  const fieldProps = {
    ...restEvents,
    id,
    name,
    type,
    value,
    validate,
    placeholder,
    className: fieldClass,
    onChange: handleChange,
  };

  if (type === "textarea") {
    delete fieldProps.type;
    delete fieldProps.value;

    fieldProps.defaultValue = value;
    fieldProps.rows = rows || 2;
  }

  return field && field.value !== undefined ? (
    <div className={contClass}>
      {label}
      {type === "textarea" ? (
        <textarea {...fieldProps} />
      ) : (
        <input {...fieldProps} />
      )}
      <p className={errorClass}>{fieldError}</p>
    </div>
  ) : (
    ""
  );
};

export default TextInput;

We are using useEffect hook to detect the field value change. If the value changes, we will trigger the validateField function. We are also showing the field errors now. Observe that the placeholder for the error renders a variable that holds the field errors.

So, this completes how to make a text input (or a textarea) field that is very similar to the native text input field. Of course, you can still add a lot of things to this component. If you want something production-ready, please feel free to check out my “react-form” repo. This handles all corner cases and has a few extra functionalities that you may find useful in real-life scenarios. You can find it on NPM with the name react-state-form. You can see the full code of this example on this Sandbox.

In the next post, we will cover how to make an “auto-suggest” input box by using the same TextInput component.

About the Author:

Bharat has been a Front-End developer since 2011. He has a thing for “Front-End development experience”. He likes to learn and teach about technology. He enjoys his life with the loveliest woman and two precious twins.

Overall a nice guy. Find him on Twitter, Github, Linkedin.

Enjoyed this article?

Share it with your network to help others discover it

Continue Learning

Discover more articles on similar topics