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 theForm
(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.