In this article I'll be going through how we can build our own simple contact form component with validation in React, Typescript and Material UI. Scroll down to the end of the page to see the CodeSandbox url for this.
Form Skeleton 💀
First, we'll create the react component, let's call it ContactForm
const ContactForm = () => {}
Then we'll add an empty form element
export const ContactForm = () =>
{
return (
<form>
</form>
)
}
This form will do nothing at the moment and won't display anything on the page. So we'll start adding the form elements using Material UI components. This will build up the basic skeleton of the contact form. The elements we're adding are:
- Three text fields for the user to input their name, email and message.
- One button used to submit the form.
export const ContactForm = () =>
{
return (
<form>
<TextField label="Full Name" />
<TextField label="Email"/>
<TextField label="Message"/>
<Button type="submit">Submit</Button>
</form>
)
}
The form now should look like this:
We'll make some adjustments to make the form look nicer so we'll add fullWidth
to the TextField
components and add multiline
and rows={5}
to the message text field:
If fullWidth is set, the input will take up the full width of its container.
export const ContactForm = () =>
{
return (
<form>
<TextField label="Full Name" fullWidth autocomplete="none"/>
<TextField label="Email" fullWidth autocomplete="none"/>
<TextField label="Message" fullWidth multiline rows={5} autocomplete="none"/>
<Button type="submit">Submit</Button>
</form>
)
}
Form Validation ✅
Now that our form is looking a bit better we'll start looking at the validation side of things.
Let's create a new function in a separate file to handle our validation and we'll add and expose the functions that we need to validate the form's input values.
const initialFormValues = {
fullName: "",
email: "",
message:"",
formSubmitted: false,
success: false
}
export const useFormControls = () => {
// We'll update "values" as the form updates
const [values, setValues] = useState(initialFormValues);
// "errors" is used to check the form for errors
const [errors, setErrors] = useState({} as any);
const validate: any = (fieldValues = values) => {
// this function will check if the form values are valid
}
const handleInputValue: any = (fieldValues = values) => {
// this function will be triggered by the text field's onBlur and onChange events
}
const handleFormSubmit = async (e: any) => {
// this function will be triggered by the submit event
}
const formIsValid: any = () => {
// this function will check if the form values and return a boolean value
}
return {
handleInputValue,
handleFormSubmit,
formIsValid,
errors
};
}
Now we have the functions in place we'll set up the event handling. We'll also need to get access to the functions in the useFormControls
component so we'll create an object that will contain the initial form values
export const ContactForm = () => {
const {
handleInputValue,
handleFormSubmit,
formIsValid,
errors
} = useFormControls();
return (
<form onSubmit={handleFormSubmit}>
<TextField name="fullName" onBlur={handleInputValue} onChange={handleInputValue} label="Full Name" fullWidth autoComplete="none" {...(errors["fullName"] && { error: true, helperText: errors["fullName"] })}/>
<TextField name="email" onBlur={handleInputValue} onChange={handleInputValue} label="Email" fullWidth autoComplete="none" {...(errors["email"] && { error: true, helperText: errors["email"] })}/>
<TextField name="message" onBlur={handleInputValue} onChange={handleInputValue} label="Message" fullWidth multiline rows={5} autoComplete="none" {...(errors["message"] && { error: true, helperText: errors["message"] })}/>
<Button type="submit" disabled={!formIsValid()}>Submit</Button>
</form>
)
}
Our input fields have shared properties and values so to make the code DRY, we'll create an array with our text fields' properties' values and add it to the top of the file and loop through it:
const inputFieldValues = [
{
name: "fullName",
label: "Full Name",
id: "my-name"
},
{
name: "email",
label: "Email",
id: "my-email"
},
{
name: "message",
label: "Message",
id: "my-message",
multiline: true,
rows: 10
}
];
export const ContactForm = () => {
const {
handleInputValue,
handleFormSubmit,
formIsValid,
errors
} = useFormControls();
return (
<form onSubmit={handleFormSubmit}>
{inputFieldValues.map((inputFieldValue, index) => {
return (
<TextField
key={index}
onBlur={handleInputValue}
onChange={handleInputValue}
name={inputFieldValue.name}
label={inputFieldValue.label}
multiline={inputFieldValue.multiline ?? false}
rows={inputFieldValue.rows ?? 1}
autoComplete="none"
{...(errors[inputFieldValue.name] && { error: true, helperText: errors[inputFieldValue.name] })}
/>
);
})}
<Button
type="submit"
disabled={!formIsValid()}
>
Send Message
</Button>
</form>
)
}
That's all set up then .. Now we just need to start filling in the values in the useFormControls
component.
We'll start with the onBlur and onChange events. We need this to show an error message if the user clicks in the input box and clicks out without typing anything. The onChange event will be triggered when the value in the text field is changed and this will trigger the same function handleInputValue
const handleInputValue = (e: any) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
validate({ [name]: value });
};
This 👆🏼 will update the state variable values
for a specific element (e.g. When the "email" text field is updated where the name is "email", the value of "email is updated).
This function will call the validate
function which validates the value of the text field that was changed and sets the appropriate error message. A regex will be used to validate against the email value to ensure the correct format hhas been entered. The state variable errors
gets updated with the relevent message
const validate: any = (fieldValues = values) => {
let temp: any = { ...errors }
if ("fullName" in fieldValues)
temp.fullName = fieldValues.fullName ? "" : "This field is required."
if ("email" in fieldValues) {
temp.email = fieldValues.email ? "" : "This field is required."
if (fieldValues.email)
temp.email = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(fieldValues.email)
? ""
: "Email is not valid."
}
if ("message" in fieldValues)
temp.message =
fieldValues.message ? "" : "This field is required."
setErrors({
...temp
});
}
Next we update the formIsValid
function
The errors state variable would have been updated in the
validate
function.
We check if the properties are set, to cover the first time the contact form loads when the errors state variable is empty.
const formIsValid = (fieldValues = values) => {
const isValid =
fieldValues.fullName &&
fieldValues.email &&
fieldValues.message &&
Object.values(errors).every((x) => x === "");
return isValid;
};
And last but not least we have the function that actually submits the form to be sent. The functionality to send the contact form by email postContactForm
isn't covered as part of this tutorial but I will be covering it in a later tutorial.
const handleFormSubmit = async (e: any) => {
e.preventDefault();
if (formIsValid()) {
await postContactForm(values);
alert("You've posted your form!")
}
};
At the end of this, you will have a functioning contact form (minus the sending email part 😊).
I hope this helps. You can find the full working code here:
In a later post, I will be going through sending an email to a .NET Core backend and displaying a message on the screen.
Top comments (3)
Good idea for creating a Form Controls hook. However, your design doesn't keep the state of the form at the high-level component. I am not sure this is the "React" way of doing things.
Fair point 😃 This is quite an old post from when I first started learning react. Looking at the code I would probably do it differently now 😅
Thanks very much Hiba your post, help me much in my project, because i need start fast