7. Writing Data Tables, Formik Forms, and Yup Validations
Devlin Basilan Duldulao1 and Ruby Jane Leyva Cabagnot1
(1)
Oslo, Norway
In the previous chapter, we learned how to write local states and send HTTP requests. We also installed ApexCharts to create our visual charts.
We will now move on to building our application, add new components to lay the groundwork for our data tables, and start writing forms using Formik and input validations using Yup. This chapter is the first part of a two-part series since it is a reasonably long topic. The first part would be creating data tables and other style components, which would be the foundation for the second part, the next chapter – focusing on writing forms and input validations.
Before we proceed with our project app, let’s review some of the libraries or components we will use in this chapter.
Form Handling
Forms allow app users to directly input and submit data through our components from a profile page or login screen to a shopping checkout page and others. It is also the main reason why it is a critical part of any web application.
Based on my own experience and that of many other React developers, creating forms in a React application can be quite tedious. More importantly, forms we create from scratch may be prone to errors because of all the reactivities we need to handle ourselves.
That’s why I choose to build forms using Formik. You can use many other excellent form libraries, including Redux Form, Formsy, and React Forms.
Formik
This gives us the Form, Field, and ErrorMessage components to create our forms, add form fields, and display error messages. Formik gives us three props:
initialValue: For the initial values of the form fields.
validate: For validation rules in the form fields.
onSubmit: This takes a function when we click Submit.
More on this when we start to build forms. I think it’s better to show it in code.
Yup
Yup is an object schema validator in JavaScript (Listing 7-1). With Yup, we can
Define the object schema and its validation.
Create a validator object with the required schema and validation.
Verify if objects are valid (satisfy schemas and validations) using a Yup utility function. If it doesn’t meet the validations, an error message is returned.
//define the object schema and its validation
const book = {
published: 1951,
author: "JD Salinger",
title: "The Catcher in the Rye",
pages: 234
};
//create a validator object with the required schema and validation
const yup = require("yup");
const yupObject = yup.object().shape({
published: yup.number.required(),
author: yup.string.required(),
title: yup.string.required(),
pages: yup.number()
});
Listing 7-1
An Example of Creating the Validations with Yup
To demonstrate how we can use all of this together, we will build a Product dashboard that will list all the products and add new products to our app.
First, we will use a data table component from Material-UI to display sets of data.
Data Tables
Product Create View
Go to views ➤ dashboard, create a new folder, and name it product. Under the product folder, create another folder and name it ProductCreateView.
And inside the ProductCreateView folder, create a new file and name it Header.tsx.
Open the Header.tsx, and after typing the snippet “rafce” for VS Code or “rsc” for WebStorm, add the header <h1>Header - CreativeView Works!</h1> for the time being.
See Listing 7-2 on creating the Header component of the ProductCreateView.
import React from 'react'.
const Header = () => {
return (
<div>
<h1>Header - CreativeView Works!</h1>
</div>
)
}
export default Header;
Listing 7-2
Creating the Header Component of ProductCreateView
Still within the ProductCreateView folder, we’ll add another file and name it ProductCreateForm.tsx.
Add an <h1> tag to the ProductCreateForm.tsx. See Listing 7-3 on creating the ProductCreateForm.tsx.
import React from 'react'
const ProductCreateForm = () => {
return (
<div>
<h1>ProductCreateForm Works! h1>
</div>
)
}
export default ProductCreateForm;
Listing 7-3
Creating the ProductCreateForm.tsx
Next, under the ProductCreateView directory, add an index.tsx file that will import the two components that we’ve just created: Header.tsx and ProductCreateForm.tsx.
Listing 7-4 creates the index.tsxof ProductCreateView.
import React from 'react';
import { Container, makeStyles } from '@material-ui/core';
import Header from './Header';
import ProductCreateForm from './ProductCreateForm';
const ProductCreateView = () => {
const classes = useStyles();
return (
<Container>
<Header />
<ProductCreateForm />
</Container>
);
};
const useStyles = makeStyles(theme => ({}));
export default ProductCreateView;
Listing 7-4
Creating the index.tsx of ProductCreateView
So we’re done with that for now. We’ll go back to those components later. The next thing we shall do is to create the product list view.
Product List View
We’ll create another folder inside the product and name it ProductListView and, under that, add two new files and call them Header.tsx and Results.tsx, respectively:
We’ll import some icons from Feather in the said file.
import {PieChart as PieChartIcon,
ShoppingCart as ShoppingCartIcon,
ChevronUp as ChevronUpIcon,
ChevronDown as ChevronDownIcon,
List as ListIcon,
FilePlus as FilePlusIcon,
LogOut as LogOutIcon,} from 'react-feather';
Listing 7-8
Updating the Named Imports for the dashboard-sidebar-navigation
Note that we’ve renamed the icons that we imported so they’re more readable or other developers in the team can easily understand at a glance what they are for.
Next, we’ll add a local state (useState) and create an event handler handleClick to update the local state. But first, don’t forget to import the useState component from React.
import React, { useEffect, useState } from 'react';
...
const [open, setOpen] = useState(false)
useEffect(() => {}, []);
const handleClick =() => {
setOpen(!open)
};
Listing 7-9
Adding useState and an Event Handler to dashboard-sidebar-navigation
After that, we’ll render a collapsible menu in the browser.
Creating a Collapsible Sidebar Menu
Let’s add a collapsible menu in between the Dashboard and settings-and-privacy.
First, let’s import the component Collapse from Material-UI Core:
import { Collapse, Divider, ListSubheader } from '@material-ui/core';
Then let’s add the following code to our collapsible menu. We will use the local state open and the event handler handleClick and Material-UI Core’s styling icon components.
Creating a Collapsible Menu (Material-UI) for dashboard-sidebar-navigation
So what's going on with our collapsible menu? We added the Management as a list subheader, and under that, we’re using the <ShoppingCartIcon /> for the collapsible Products menu to show the menus List Products and Create Product.
The <ChevronUpIcon /> and <ChevronDownIcon /> will open up and collapse the menu when the user clicks it.
In your editor, you may or may not notice a red squiggly line on {classes.nested}.
In any case, we need to do something more here. This is because we need to add it to our useStyle component. Just add it at the bottom.
nested: {
paddingLeft: theme.spacing(4),
},
Listing 7-11
Updating the useStyle Component of dashboard-sidebar-navigation
Now run the application to check if everything is still working. You should see the updated sidebar navigation like the following.
Click List Products and Create Product to check if you can successfully navigate between pages. You should be able to see the h1 header we’ve written:
(Shown when clicking the List Products tab)
Header - ListView Works!
Results - Works!
(Showing when clicking the Create Product tab)
Header - CreativeView Works!
ProductCreateForm Works!
So now that we’re done with that proof of concept that we can navigate to the new pages, I think it’s time to do some cleanup and remove the settings and privacy tab. We don’t need it anymore; we’ll add some more menus later on.
Cleaning Up a Bit…
Delete the settings-and-privacy from the routes.tsx.
Delete the file settings-and-privacy.tsx.
Next, go the dashboard-sidebar-navigation.tsx.
We will make two edits here:
1. Remove the settings and privacy.
2. Then replace the default <ExitToAppIcon /> with our own <LogoutIcon />.
<a className={classes.link} href={'/'}>
<ListItem button>
<ListItemIcon>
<LogOutIcon/>
</ListItemIcon>
<ListItemText primary={'logout'} />
</ListItem>
</a>
Listing 7-12
Logout Icon in dashboard-sidebar-navigation
I might have forgotten to use the <Divider /> from Material-UI, so we’ll put it now. Put it right after the </Toolbar>.
Adding the Divider Component in dashboard-sidebar-navigation
Now run or refresh the browser if the settings and privacy has been removed, and see if everything is still working.
Defining Type Alias of ProductType
After this, we’ll proceed with implementing the products in a data table. Since we’re using TypeScript, we’ll start building first our model types or interfaces. In this case, I prefer to use types.
In the models directory, create a new file and name it product-type.ts. The shape of our ProductType object is shown in the following.
/* this is like an enum string. The pipe | here is basically a union that allows us o choose either of the three options. */
export type InventoryType = 'in_stock' | 'limited' | 'out_of_stock';
export type ProductType = {
id: string;
attributes: string[];
category: string;
//union means can be string or number
createdAt: string | number;
currency: string;
// the ? means nullable
image?: string;
inventoryType: InventoryType;
isAvailable: boolean;
isShippable: boolean;
name: string;
price: number;
quantity: number;
updatedAt: string | number;
variants: number;
description: string;
images: string[];
includesTaxes: boolean;
isTaxable: boolean;
productCode: string;
productSku: string;
salePrice: string;
};
Listing 7-14
Creating the Shape of the ProductType Object
The shape or type is pretty much self-explanatory here. We need to make this extra effort now for code maintainability and for us to get IntelliSense in our editor. Doing this now saves us a lot of pain in the long run.
Creating the Products Endpoint
Before we go to the services, let’s update the endpoints in the axios configuration. Open the axios.ts and add the products endpoint.
export const EndPoints = {
sales: 'sales',
products: 'products'
};
Listing 7-15
Adding the Products Endpoint in axios.ts
Now that we have set up the endpoints for our sales and products, it’s time to set up their HTTP services.
Creating the Products Service
We’ll use that endpoint in a new file called productService.ts, which we will create under the services directory:
services ➤ productService.ts
Open the new file and add the functions to create the products service, as shown in Listing 7-16.
import api, {EndPoints} from '../api/axios';
import {ProductType} from '../models/product-type';
Both use Axios to send a request to the JSON server, and the return type is an array of ProductType: <ProductType[ ]> and <ProductType>, respectively.
Both functions are async-await types.
After this, let’s update our db.json with a sample of products or an array of four objects.
Updating the db.json Data
Head off to your db.json file and add the following data, as shown in Listing 7-17.
"products": [
{
"id": "5ece2c077e39da27658aa8a9",
"attributes": ["Cotton"],
"category": "dress",
"currency": "$",
"createdAt": "2021-01-01T12:00:27.87+00:20",
"image": null,
"inventoryType": "in_stock",
"isAvailable": true,
"isShippable": false,
"name": "Charlie Tulip Dress",
"price": 23.99,
"quantity": 85,
"updatedAt": "2021-01-01T12:00:27.87+00:20",
"variants": 2
},
{
"id": "5ece2c0d16f70bff2cf86cd8",
"attributes": ["Cotton"],
"category": "dress",
"currency": "$",
"createdAt": "2021-01-01T12:00:27.87+00:20",
"image": null,
"inventoryType": "out_of_stock",
"isAvailable": false,
"isShippable": true,
"name": "Kate Leopard Dress",
"price": 95,
"quantity": 0,
"updatedAt": "2021-01-01T12:00:27.87+00:20",
"variants": 1
},
{
"id": "5ece2c123fad30cbbff8d060",
"attributes": ["Variety of styles"],
"category": "jewelry",
"currency": "$",
"createdAt": 345354345,
"image": null,
"inventoryType": "in_stock",
"isAvailable": true,
"isShippable": false,
"name": "Layering Bracelets Collection",
"price": 155,
"quantity": 48,
"updatedAt": "2021-01-01T12:00:27.87+00:20",
"variants": 5
},
{
"id": "5ece2c1be7996d1549d94e34",
"attributes": ["Polyester and Spandex"],
"category": "blouse",
"currency": "$",
"createdAt": "2021-01-01T12:00:27.87+00:20",
"image": null,
"inventoryType": "limited",
"isAvailable": false,
"isShippable": true,
"name": "Flared Sleeve Floral Blouse",
"price": 17.99,
"quantity": 5,
"updatedAt": "2021-01-01T12:00:27.87+00:20",
"variants": 1
}
]
Listing 7-17
Adding the db.json Data with Product Objects
You’ll notice that we’ve created four product objects. And just for simplicity here, the names of the objects are as follows:
"name": "Charlie Tulip Dress",
"name": "Kate Leopard Dress",
"name": "Layering Bracelets Collection",
"name": "Flared Sleeve Floral Blouse",
Now that we’ve added the productService in our axios and updated our db.json, let’s test it out by sending an HTTP request.
Sending an HTTP Request
Head off to the index.tsx file of the ProductListView.
We’ll need the useEffect from React. Inside the useEffect, we’ll call the getProductAxios, which is imported from services/productService.
...
import { getProductsAxios } from 'services/productService';
const ProductListView = () => {
const classes = useStyles();
useEffect(() => {
getProductAxios();
}, []);
Listing 7-18
Using the getProductAxios in ProductListView.tsx
Go to the Chrome DevTools and click the Network tab and choose the XHR. Make sure that your JSON server is running at localhost:5000/products.
Click List Products in the browser, and in the headers, you should see Status Code: 200 OK to indicate a successful get response from the JSON server.
Next, click the Response tab to check the JSON objects. You should be able to see the array of products that we’ve added in our db.json.
Refactoring the ProductListView
Okay, now that we know that it is working, we will do some code refactoring in the ProductListView to reflect the best practices.
Go to the index.tsx of ProductListView and do the following:
Create a local state (useState) for updating the array of products’ data.
Add an async-await function to be named fetchProducts where we can call the getProductAxios().
Put the fetchProducts() inside a try-catch block as best practice.
Add a backdrop component from Material-UI that works much like a loader spinner.
/* local state uses Generics Type of type array, so we’ll know at a glance its shape. Hover your mouse over the <ProductType[]>, and you’ll see its model types. If you take out the Generics here, you’ll lose the ability to see the object’s model shape during hover. You’ll get the type ‘any’ */
Next, we need another component that will help us render the data table. Go to the folder ProductListView and create a new file and name it TableResultsHelpers.tsx.
Let’s import the named component and define the type alias of the object, as shown in Listing 7-21.
import React from 'react';
import { InventoryType, ProductType } from 'models/product-type';
import Label from 'app/components/label';
export type TableResultsHelpers = {
availability?: 'available' | 'unavailable';
category?: string;
inStock?: boolean;
isShippable?: boolean;
};
Listing 7-21
Importing the component and adding the type alias for TableResultsHelpers
Next, let’s apply the filter conditions for rendering the products to the user; see Listing 7-22.
export const applyFilters = (
products: ProductType[],
query: string,
filters: TableResultsHelpers,
): ProductType[] => {
return products.filter(product => {
let matches = true;
/* the product here comes from the parent component. */
if (query && !product.name.toLowerCase().includes(query.toLowerCase())) {
matches = false;
}
if (filters.category && product.category !== filters.category) {
matches = false;
}
if (filters.availability) {
if (filters.availability === 'available' && !product.isAvailable) {
matches = false;
}
if (filters.availability === 'unavailable' && product.isAvailable) {
The TableResultsHelpers is using the label component that we just created.
We’re also importing InventoryType and ProductType from models/product-type.
The table helpers are for the UI, so we can query or type in the filter box and see the list of results.
After that, create a new folder under src and name it helpers. Under the helpers folder, add a new file and name it inputProductOptions.ts. This file is just for labeling the table, and it’s better to put it in a separate file rather than bunching it together with the component itself.
export const categoryOptions = [
{
id: 'all',
name: 'All',
},
{
id: 'dress',
name: 'Dress',
},
{
id: 'jewelry',
name: 'Jewelry',
},
{
id: 'blouse',
name: 'Blouse',
},
{
id: 'beauty',
name: 'Beauty',
},
];
export const availabilityOptions = [
{
id: 'all',
name: 'All',
},
{
id: 'available',
name: 'Available',
},
{
id: 'unavailable',
name: 'Unavailable',
},
];
export const sortOptions = [
{
value: 'updatedAt|desc',
label: 'Last update (newest first)',
},
{
value: 'updatedAt|asc',
label: 'Last update (oldest first)',
},
{
value: 'createdAt|desc',
label: 'Creation date (newest first)',
},
{
value: 'createdAt|asc',
label: 'Creation date (oldest first)',
},
];
Listing 7-23
Creating the Helpers for inputProductOptions
That’s done for now. Now, we’ll install three NPM libraries:
$ npm i numeral
$ npm i @types/numeral
$ npm i react-perfect-scrollbar
1.
numeral.js: A JavaScript library for formatting and manipulating numbers.
2.
@types/numeral: numeral.js is built using JavaScript, so we need to add typings for this library.
3.
react-perfect-scrollbar: This allows us to make a scrollbar easily for our data table.
After successfully installing the libraries, open the file results.tsx to make some edits. I’ve mentioned earlier that we will go back to this file to build it up.
Let’s add the following named import components as shown in Listing 7-24. Aside from the several styling components from Material-UI Core that we will install, we’re importing components from inputProductOptions, TableResultsHelpers, and the product-type from the models folder.
import React, { useState, ChangeEvent } from 'react';
import clsx from 'clsx';
import numeral from 'numeral';
import PerfectScrollbar from 'react-perfect-scrollbar';
import {
Image as ImageIcon,
Edit as EditIcon,
ArrowRight as ArrowRightIcon,
Search as SearchIcon,
} from 'react-feather';
import {
Box,
Button,
Card,
Checkbox,
InputAdornment,
FormControlLabel,
IconButton,
SvgIcon,
Table,
TableBody,
TableCell,
TableHead,
TablePagination,
TableRow,
TextField,
makeStyles,
} from '@material-ui/core';
import {
availabilityOptions,
categoryOptions,
sortOptions,
} from 'helpers/inputProductOptions';
import {
applyFilters,
applyPagination,
TableResultsHelpers,
getInventoryLabel,
} from './tableResultsHelpers';
import { ProductType } from 'models/product-type';
Listing 7-24
Adding the Named Import Components to results.tsx
Next, we’ll define the type or shape of the object in Listing 7-25.
type Props = {
className?: string;
products?: ProductType[];
};
Listing 7-25
Creating the Shape or Type of the Object in results.tsx
Following the definition of the type, we’ll create some local states, as shown in Listing 7-26.
Continuing to the HTML, we’re wrapping everything in Card from Material-UI Core. We’re also adding Box, TextField, Checkbox, and various Table styles, as shown in Listing 7-28.
Keep in mind that all these stylings are something that you need not create from scratch. You can just go to the Material-UI website and, let’s say, search for “table,” and you can use anything there according to your app’s requirements. All the APIs we’re using here are available in Material-UI.
I’m just showing you again the possibilities of using a well-written and supported library to make your coding development a bit easier. Of course, as I’ve mentioned before, there are many UI component libraries that you can use, and Material-UI is just one of them.
If you are coding along, copy-paste the Card component from Material UI, as shown in Listing 7-28. We will refactor or make some changes when necessary.
We are done for now with the results.tsx. Let’s make some updates to the index.tsx of the ProductListView.
We’ll import a few components from Material-UI Core, including the Page template component, as shown in Listing 7-30.
import {
Backdrop,
Box,
CircularProgress,
Container,
makeStyles,
} from '@material-ui/core';
import Page from 'app/components/page';
Listing 7-30
Adding Named Components to the index.tsx of ProductListView
And then let’s add the useStyles from the makeStyles component, as shown in Listing 7-31.
import { createStyles } from '@material-ui/core/styles';
...
const useStyles = makeStyles(theme =>
createStyles({
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: '#fff',
},
root: {
minHeight: '100%',
paddingTop: theme.spacing(3),
paddingBottom: 100,
},
}),
);
Listing 7-31
Adding useStyles to the index.tsx of ProductListView
Okay, now that we’ve got those laid down on the ProductListView, we will use the Page template, Container , and Backdrop in the JSX, as shown in Listing 7-32.
Adding Material-UI Components to the index.tsx of ProductListView
Ensure that your JSON server runs at localhost:5000/products and then refresh your UI by clicking List Products in the sidebar dashboard.
Play around with the search box (Search products), Category, and Availability to check if you can successfully search and get the correct results based on your typed keywords. Click the refresh button also to check if the backdrop with the spinner is working.
Summary
We saw that the Products menu is working so far, at least half of it – the List Products – but we have still a long way to go to complete the Products sidebar menu. You can say that we have just laid down the skeletal foundation before we can get to the nitty-gritty of things.
In the second part, the next chapter, we will put a few finishing touches on the ProductListView and then jump straight away to using Formik and Yup validation forms.