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

1

There are 1 best solutions below

1
Hossein Azizdokht On

Creating the Autocomplete Component:

Create a new Autocomplete component in your project. You can use MUI's Autocomplete component to build it. Here's an example of how to create a basic Autocomplete component:

Autocomplete.js

import React from 'react';
   import Autocomplete from '@mui/material/Autocomplete';
   import TextField from '@mui/material/TextField';

   const MyAutocomplete = ({ options, value, onChange }) => {
     return (
       <Autocomplete
         options={options}
         value={value}
         onChange={(_, newValue) => onChange(newValue)}
         renderInput={(params) => <TextField {...params} label="Select an option" />}
       />
     );
   };

   export default MyAutocomplete;

Error Validation:

To include error validation, you can pass an error prop to the TextField component inside your Autocomplete based on your form's validation logic

const MyAutocomplete = ({ options, value, onChange, error, helperText }) => {
     return (
       <Autocomplete
         options={options}
         value={value}
         onChange={(_, newValue) => onChange(newValue)}
         renderInput={(params) => (
           <TextField
             {...params}
             label="Select an option"
             error={error}
             helperText={helperText}
           />
         )}
       />
     );
   };

Integrate with React Form (Formik):

To integrate the Autocomplete component with a React form (Formik in this case), you can use Formik's Field component to render it within your form. Here's how you can use it:

import { Formik, Field, Form, ErrorMessage } from 'formik';

   // ...

   <Formik
     initialValues={{
       autocompleteValue: null,
     }}
     validationSchema={YourValidationSchema}
     onSubmit={YourSubmitHandler}
   >
     {({ values, handleSubmit, isSubmitting }) => (
       <Form onSubmit={handleSubmit}>
         <Field
           name="autocompleteValue"
           render={({ field, form }) => (
             <MyAutocomplete
               options={YourOptions}
               value={field.value}
               onChange={(newValue) => form.setFieldValue(field.name, newValue)}
               error={form.touched[field.name] && !!form.errors[field.name]}
               helperText={form.touched[field.name] && form.errors[field.name]}
             />
           )}
         />
         <ErrorMessage name="autocompleteValue" component="div" />
         <button type="submit" disabled={isSubmitting}>
           Submit
         </button>
       </Form>
     )}
   </Formik>

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.