Salable tech blog #1 Fixing the ‘Create New Product’ form

A screenshot of some code
Earlier this week I picked up a new ticket to work on for our MVP, it was a bug regarding our ‘Create New Product’ form. It was reported that during the form journey, a user could leave both the name and displayName fields empty (required fields) and no errors would be displayed to the user and they wouldn't be prevented from carrying on with the form.

This meant that the user could go through the entire form without being made aware of missing required fields and then when they submit the form, they would be prompted to return to fill in the missing fields.

Now it’s not the worse possible situation because the form can’t be submitted without the required fields being inputted. But it does provide a bit of a janky UX for the user. Imagine if this form wasn’t 2 sections long but rather 5, 6, or more and they missed several fields along the way. Being prompted to return and fill in each of those and essentially re-do the form is a bit of an annoyance so if we can do something about it, we should.

With this in mind, we’re going to update the form so that the user cannot progress from the first section of the form to the second without filling in the required fields. And if they try to, they will be prompted with errors to fill in the required fields on that section of the form.

Investigation

So after investigating the problem and looking through the relevant code, I noticed that we were using React State to handle the change or thestep of the form we were showing to the user. When the step state is initialized we default it to a value of 0 to show the first section for name, displayName, and description.

Then when the user clicks on the Next button we fire on an onClick handler which updates the state to 1, in turn updating the section of the form displayed to the user. At this point, we have zero validation happening on the press of the Next button to see if the required fields are populated and instead we wait until the user presses the Create Product button at the end of the form to validate the whole thing.

Now, because we use Formik for handling our forms and their values/inputs when the button of type submit is pressed, Formik runs its validation against the form values and inputs/throws any errors if needed. This is why the missing required fields are flagged when the form is submitted but not when we change sections of the form.

The Solution

Now we know where the issue lies in our code, we can begin fixing it. Looking through the documentation on Formik, I found a section talking about manually triggering the validation of forms which is exactly what we needed.

There is a function called validateForm which manually triggers the validation of a form. This function returns a Promise which we can await in the onClick function used with our ‘next’ button.

So this part takes care of validating the form fields and showing errors on required fields if they have not been populated. But, we still need to block the user from progressing in the form if the required fields have not been populated.

While validateForm() doesn't do this itself, we can use the onfulfilled value provided to us once the validateForm() promise fulfills. This onfulfilled value contains any errors that were detected during the validation of the form, where they were located and if there were no errors, the value is empty.

For example, for our ‘Create New Product’ form if we didn’t fill in the required fields name and displayName, the returned onfulfilled object would look like this;

{
  "productInfo": {
    "name": "Product name is required",
    "displayName": "Display name for product is required"
  }
}

Knowing this, we can use an if statement to conditionally run code based on if the productInfo value is within the object or not by using conditional chaining in the if condition.

if (onfulfilled?.productInfo) {
	// Run this code if productInfo is truthy / exists on the onfulfilled object.
}

In our situation, we are only interested in running code within the if statement if the returned onfulfilled object is empty, which would indicate there are no errors and the form validated successfully - meaning the required fields are populated.

So if we add in everything we just covered to our code, our button’s onClick handler would look like this;

onClick={async () => {
	await validateForm().then((onfulfilled) => {
		if (!onfulfilled?.productInfo) {
			changeStep(1);
		}
	});
}}

With the changes we’ve made, we now have a form that prevents the user from continuing if the required fields haven’t been populated on that section of the form. But there is one issue still to resolve, see if you can spot it…

https://youtu.be/m3W1eAhqYsY

That’s right no errors are being shown to the user to prompt them to fill in the required fields that are causing the errors and in turn preventing the form from continuing.

But, thankfully this is a quick and easy fix. If we look at the code that controls the rendering of the error messages we can see that the error messages are being shown conditionally, based on if the input has been ‘touched’ or not and if there is a value that isn’t an empty string.

‘Touched’ means the user has clicked on and then clicked off the input.

error={touched.productInfo?.name && !!errors.productInfo?.name}

If the user clicks on the input and then the ‘Next’ button without populating the field, they will be shown the error message for that input because it has been ‘touched’ and is empty. The issue occurs for us when the user doesn’t touch the input and tries to push the next button without doing anything else.

Luckily Formik has us covered here as well, they export a function called setTouched() which as you've probably guessed allows us to set if an input or field has been touched.

All we need to do is update our onClick handler for the ‘next’ button to set both of the required fields on the first section of the form to be touched before we call the validateForm() function.

Then when the validateForm() function runs it will see the values are empty and they have been touched, in turn displaying the error states and messages to the user. So our final code for the "Next" button's onClick handler looks like this;

onClick={async () => {
	setFieldTouched('productInfo.name', true, true);
	setFieldTouched('productInfo.displayName', true, true);
	
	await validateForm().then((onfulfilled) => {
		if (!onfulfilled?.productInfo) {
			changeStep(1);
		}
	});
}}

For the app itself with these changes made, the form now looks like this.

Access Salable, tell us what you think

Related blogs

Tweet from Neal Riley saying "Commercial freedom and flexibility is a must for any digitally enabled business. @SalableApp gives you the tools to build your SaaS business."

Tweet from Neal Riley saying "Commercial freedom and flexibility is a must for any digitally enabled business. @SalableApp gives you the tools to build your SaaS business."

Tech Blog #2 Salable Package Architecture

Over the last few months, we’ve been working on a new Global Design System (GDS) for Salable. But as part of this process, we’ve also had to make several decisions...

Tech Blog #3: Using a private GitHub repository as an NPM package in another repository

Creating NPM packages is an easy and convenient way to share code across multiple projects. But, if you want to keep the package private and are unwilling to pay for the

Coner Murphy
07 Nov 2022

Tech Blog #4: How to version all packages synchronously, in a monorepo using Lerna

Today we are setting up automatic versioning of packages inside of a monorepo using Lerna. There are many ways in which you can do this with different technologies...

Coner Murphy
22 Nov 2022