In my next project, I created a Material-UI Autocomplete component named HsnComp (located at components/hsnComp.tsx). The component is functioning properly, as demonstrated in the provided CodeSandbox. Please note that I am implementing autocompletion using MongoDB. The component fetches description (a string) and hsCode (a string) from MongoDB and displays autocompletion suggestions in the format: description (hsCode), updating as users type in the textfield.
Now, onto the problem:
The structure of the hsn field is as follows:
hsn: {
description: string,
hsCode: string,
},
While my code is working correctly, I've used a callback function in the page.tsx (form page) to set the values of description and hsCode. However, I believe this might not be the optimal approach. The issue arises when it comes to error validation, such as redirecting to the field if it's empty and displaying a red error message below the particular field.
I'm unsure how to integrate HsnComp.tsx into page.tsx (formPage) and simultaneously apply error validation (e.g., required fields). The challenge is registering both description and hsCode in the form.
CodeSandBox:Link
Code:
page.tsx:
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { Controller, useForm } from "react-hook-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { FormSchema } from "./zodSchema";
import { toast } from "@/components/ui/use-toast";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@radix-ui/react-label";
import { Input } from "@/components/ui/input";
import HsnComp from "@/components/hsnComp";
type FormValues = z.infer<typeof FormSchema>;
export default function Home() {
const {
control,
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(FormSchema) });
function onSubmit(data: FormValues) {
console.log(data);
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
const watchMode = watch("modes");
return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
{/* Origin-destination card */}
<Card className="m-5">
<CardHeader>
<CardTitle>HS CODE</CardTitle>
</CardHeader>
<CardContent>
<HsnComp/> //<------------------- hsnComp here
</CardContent>
</Card>
{/* Load card */}
<Card className="m-5">
<CardHeader>
<CardTitle>Load</CardTitle>
</CardHeader>
<CardContent>
<div>
<div className="mb-5">
<Label className="mb-2">Modes of Transporation</Label>
<Controller
name={"modes"}
control={control}
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
{field.value ? (
<SelectValue placeholder="Select " />
) : (
"Select "
)}
</SelectTrigger>
<SelectContent>
<SelectItem value="FCL">
Full container load(FCL)
</SelectItem>
<SelectItem value="LCL">
Less container load(LCL)
</SelectItem>
<SelectItem value="BULK">Bulk</SelectItem>
</SelectContent>
</Select>
)}
/>
{errors.modes && (
<span className="text-red-500">{errors.modes.message}</span>
)}
</div>
{/* SELECT==FCL */}
{watchMode === "FCL" && (
<>
<div className="flex flex-col space-y-2 mt-5">
<Label className="mb-2">Quantity</Label>
<Input
type="number"
placeholder="Enter the Quantity"
{...register("f_quantity")}
/>
{(errors as any).f_quantity && (
<span className="text-red-500">
{(errors as any).f_quantity.message}
</span>
)}
<Label className="mb-2">Weight</Label>
<Input
type="number"
className="py-3 px-4 pr-16 block w-full border-none shadow-sm rounded-md text-sm"
placeholder="Enter the Weight"
{...register("f_weight")}
/>
{(errors as any).f_weight && (
<span className="text-red-500">
{(errors as any).f_weight.message}
</span>
)}
</div>
</>
)}
{/* SELECT=LCL */}
{watchMode === "LCL" && (
<>
<div className="flex flex-col space-y-5 ">
<Controller
name={"l_container_type"}
control={control}
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
{field.value ? (
<SelectValue placeholder="Select " />
) : (
"Select "
)}
</SelectTrigger>
<SelectContent>
<SelectItem value="pallete">
Full container load(pallete)
</SelectItem>
<SelectItem value="boxes">
Less container load(boxes)
</SelectItem>
<SelectItem value="package">package</SelectItem>
<SelectItem value="bag">bag</SelectItem>
</SelectContent>
</Select>
)}
/>
{(errors as any).l_container_type && (
<span className="text-red-500">
{(errors as any).l_container_type.message}
</span>
)}
<Input
placeholder="Enter the Quantity"
{...register("l_loading")}
/>
{(errors as any).l_loading && (
<span className="text-red-500">
{(errors as any).l_loading.message}
</span>
)}
</div>
</>
)}
{/* SELECT=package */}
{watchMode === "BULK" && (
<>
<div className="flex flex-col space-y-5">
<Label className="mb-2">Quantity</Label>
<Input
type="number"
placeholder="Enter the Quantity"
{...register("b_loading_rate")}
/>
{(errors as any).b_loading_rate && (
<span className="text-red-500">
{(errors as any).b_loading_rate.message}
</span>
)}
<Label className="mb-2">Weight</Label>
<Input
type="number"
className="py-3 px-4 pr-16 block w-full border-none shadow-sm rounded-md text-sm"
placeholder="Enter the Weight"
{...register("b_discharge_rate")}
/>
{(errors as any).b_discharge_rate && (
<span className="text-red-500">
{(errors as any).b_discharge_rate.message}
</span>
)}
</div>
</>
)}
<div className="mt-5">
<Label className="mb-2">Additional information</Label>
<Textarea
placeholder="Write message..."
className="resize-none"
{...register("comment")}
/>
{errors.comment && (
<span className="text-red-500">{errors.comment.message}</span>
)}
</div>
</div>
</CardContent>
</Card>
<Button type="submit" className="ml-5">
Sumbit
</Button>
</form>
</>
);
}
hsnComp.tsx:
import React, { useState, ChangeEvent } from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
interface AutoCompleteOption {
_id: string;
htsno: string;
description: string;
}
interface IHSNCompProps {
onSelectHSN: (description: string, hsnCode: string) => void;
}
const HSNComponent: React.FC<IHSNCompProps> = ({ onSelectHSN }) => {
const [options, setOptions] = useState<AutoCompleteOption[]>([]);
const [inputValue, setInputValue] = useState('');
const fetchOptions = async (input: string) => {
try {
const response = await fetch(`/api/hsnAuto?query=${input}`);
const result = await response.json();
if (Array.isArray(result)) {
setOptions(result);
} else {
console.error('API response is not an array:', result);
}
} catch (error) {
console.error('Error fetching options:', error);
}
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const newInputValue = event.target.value;
setInputValue(newInputValue);
fetchOptions(newInputValue);
};
return (
<Autocomplete
options={options}
getOptionLabel={(option) => `${option.description} (${option.htsno})`}
renderInput={(params) => (
<TextField
{...params}
placeholder="Search HSN"
variant="outlined"
fullWidth
onChange={handleInputChange}
value={inputValue}
InputLabelProps={{
shrink: false,
style: { transform: 'translate(14px, 12px) scale(1)' },
}}
sx={{
'& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': {
boxShadow: '0 0 0 2px black',
borderColor: 'transparent',
},
'& .MuiOutlinedInput-root': {
borderRadius: '8px',
borderColor: 'lightgray',
},
'& .MuiInputBase-input': {
padding: '8px',
fontSize: '14px',
height: '10px',
},
}}
/>
)}
onChange={(_, selectedOption) => {
if (selectedOption) {
onSelectHSN(selectedOption.description, selectedOption.htsno);
}
}}
renderOption={(props, option) => (
<li {...props}>
<p>
{option.description}({option.htsno})
</p>
</li>
)}
/>
);
};
export default HSNComponent;
zodSchema.ts:
import * as z from "zod";
const fclSchema = z.object({
hsn:z.object({description:z.string(),hsCode:z.string()}),
modes: z.literal("FCL"),
f_quantity: z.coerce.number().min(1).max(32767),
f_weight: z.coerce.number().min(1).max(32767),
comment: z.string().min(3).max(160).optional(),
});
const lclSchema = z.object({
hsn:z.object({description:z.string(),hsCode:z.string()}),
modes: z.literal("LCL"),
l_container_type: z.enum(["pallete", "boxes", "package", "bag"]),
l_loading: z.string().min(1),
comment: z.string().min(3).max(160),
});
const bulkSchema = z.object({
hsn:z.object({description:z.string(),hsCode:z.string()}),
modes: z.literal("BULK"),
b_loading_rate: z.coerce.number().min(1),
b_discharge_rate: z.coerce.number().min(1),
comment: z.string().min(3).max(160),
});
export const FormSchema = z.discriminatedUnion("modes", [
fclSchema,
lclSchema,
bulkSchema,
]);
Your assistance would be greatly appreciated
Creating the Autocomplete Component:
Create a new Autocomplete component in your project. You can use MUI's
Autocompletecomponent to build it. Here's an example of how to create a basic Autocomplete component:Error Validation:
To include error validation, you can pass an
errorprop to theTextFieldcomponent inside your Autocomplete based on your form's validation logicIntegrate with React Form (Formik):
To integrate the Autocomplete component with a React form (Formik in this case), you can use Formik's
Fieldcomponent to render it within your form. Here's how you can use it:Define Validation Schema and Submit Handler:
You'll need to define a validation schema (e.g., Yup) for your form and create a submit handler function that can handle the form submission.
Styling and Customization:
Customize the Autocomplete component and form as per your requirements in terms of styling and functionality. This is a basic outline of how you can create an Autocomplete component with error validation and integrate it into a React form in a Next.js 13 project. You may need to adapt it to your specific use case and form validation requirements.