0

I have a problem, I have a form of multiple steps. Between each step there is a Continue and Back button to transition back and forth between steps. However, for the Continue button, it requires double click in order to move forward, given the condition that all required fields are completed.

Step1.txs

interface Step1Props {
  onValidationChange: (isValid: boolean) => void;
  setErrorMessage: (message: string) => void;
  hasAttempted1Continue: boolean;
}

interface FormData {
  location?: string;
  title?: string;
  category?: string;
}

const Step1: React.FC<Step1Props> = ({
  onValidationChange,
  setErrorMessage,
  hasAttempted1Continue,
}) => {
   const [formData, setFormData] = useState<FormData>({});
   const [errors, setErrors] = useState<{ [key: string]: string }>({});

  useEffect(() => {
    if (hasAttempted1Continue) {
      const newErrors: { [key: string]: string } = {};
      if (!formData.title) newErrors.title = "Campaign title is required.";
      if (!formData.location) newErrors.location = "Location is required.";
      if (!formData.category) newErrors.category = "Category is required.";
      

      setErrors(newErrors);
      const isValid = Object.keys(newErrors).length === 0;
      onValidationChange(isValid);

      if (!isValid) {
        setErrorMessage(
          // "Please fix the following errors: " +
            Object.values(newErrors).join(", "),
        );
      } else {
        setErrorMessage("");
      }
    }
  }, [
    formData,
    onValidationChange,
    setErrorMessage,
    hasAttempted1Continue,
  ]);

return (
    <div className="flex flex-col gap-y-[30px] pb-[10px] pt-[10px]">
        <CardButtonInput
            placeholder={t("title")}
            type="text"
            name="title"
            value={formData.title}
            onChange={handleInputChange}
            error={errors["title"]}
        />
        <Radio
            options={cities}
            name="location"
            placeholder={t("location")}
            value={formData.location}
            onChange={handleLocationChange}
            error={errors["location"]}
            float={true}
        />
        <Radio
            options={campaignCategory}
            name="category"
            placeholder={"Category"}
            value={formData.category}
            onChange={handleCategoryChange}
            error={errors["cateory"]}
            float={true}
        />
    </div>
)

Main.tsx

Const Main = () => {
    const [currentStep, setCurrentStep] = useState(1);
    const [isStep1Valid, setIsStep1Valid] = useState(false); 
    const [errorMessage, setErrorMessage] = useState<string | null>(null);
    const [hasAttempted1Continue, sethasAttempted1Continue] = useState(false);

    const handleContinue = async () => {
    if (currentStep === 1) {
      sethasAttempted1Continue(true);
      if (!isStep1Valid) {
        setErrorMessage("Please complete the information before continuing..");
        return;
      }

      if (currentStep === 2) {
      sethasAttempted2Continue(true);
      if (!isStep2Valid) {
        setErrorMessage("Please complete the information before continuing..");
        return;
      }

      try {
        //CallAPI() + payload
      } catch (error) {
        setErrorMessage("Failed to submit the form. Please try again.");
        return;
      }
    } else {
      setCurrentStep((prevStep) =>
        Math.min(prevStep + 1, StepItems.length + 1),
      );
      window.scrollTo({
        top: 0,
        behavior: "smooth",
      });
    }
    setErrorMessage(null);
    }

const getStepItems = () => {
    const commonSteps = [
      {
        statusDefault: "default",
        statusVerified: "verified",
        statusUnverified: "unverified",
        personalInformation: t("step1"),
        numberOfSteps: 1,
      },
      {
        statusDefault: "default",
        statusVerified: "verified",
        statusUnverified: "unverified",
        personalInformation: t("step2"),
        numberOfSteps: 2,
      },
        ...
    ];

    return commonSteps.map((step, index) => {
      if (index + 1 < currentStep) {
        return { ...step, status: "verified" };
      } else if (index + 1 === currentStep) {
        return { ...step, status: "unverified" };
      }
      return { ...step, status: "" };
    });
  };
    ...
    return (
       <div className="flex flex-row items-center justify-center">
        {StepItems.map((card, index) => (
          <React.Fragment key={index}>
            <CardStep
              status={card.status}
              statusDefault={card.statusDefault}
              statusVerified={card.statusVerified}
              statusUnverified={card.statusUnverified}
              personalInformation={card.personalInformation}
              numberOfSteps={card.numberOfSteps}
            />
            {index < StepItems.length - 1 && (
              <div className="mb-20 flex flex-row items-center justify-center">
                <div className="h-[5px] w-[5px] rounded-full"></div>
                <div className="w-[150px] border-t border-[#9F76FF]"></div>
                <div className="h-[5px] w-[5px] rounded-full"></div>
              </div>
            )}
          </React.Fragment>
        ))}
      </div>
      {currentStep === 1 && (
        <Step1
          onValidationChange={setIsStep1Valid}
          setErrorMessage={setErrorMessage}
          hasAttempted1Continue={hasAttempted1Continue}
        />
      )}
      {currentStep === 2 && (
        <Step2
          onValidationChange={setIsStep2Valid}
          setErrorMessage={setErrorMessage}
          hasAttempted2Continue={hasAttempted2Continue}
        />
      )}
      {currentStep === 3 && <Step3 />}
      {currentStep === 4 && <Step4 />}
      <div className="flex flex-row items-center justify-center gap-x-[20px]">
        <CardButton
          text={t("back")}
          border="border-[#9F76FF]"
          color="text-[#9F76FF]"
          bg="bg-transparent"
          onClick={handleBack}
        />

        <CardButton
          text={t("continue")}
          onClick={handleContinue}
        />
      </div> 
    )
}

After some time of debugging, I have found that in handleContinue() of Main.tsx, the return statement causes the logic. Meaning, if i commented out the line, clicking Continue shall allow me to move to step 2 even when errors are detected in the form.

I do know that return are meant to disrupt the process of moving to step 2 when there are errors detected. But it also blocks me from moving to next step even when all required data are filled.

I want, when all required fields are completed, clicking once on Continue button shall take the client to the next step. How can I resolve this?

1 Answer 1

0

the way you have done it is not resualbe. here is how i have done it.

import React, { ReactElement, useState, useMemo, useCallback } from 'react';

export function useMultiStepForm<C>(
  steps: { component: ReactElement; label: string }[],
  defaultData?: Partial<C>
) {
  const [currentStepIndex, setCurrentStepIndex] = useState(0);
  const [stepsData, setStepsData] = useState<Partial<C> | undefined>(
    defaultData
  );

  const next = useCallback(() => {
    setCurrentStepIndex((i) => {
      if (i >= steps.length - 1) return i;
      return i + 1;
    });
  }, [steps.length]);

  function back() {
    setCurrentStepIndex((i) => {
      if (i <= 0) return i;
      return i - 1;
    });
  }

  function goTo(index: number) {
    setCurrentStepIndex(index);
  }
  const stepComponents = useMemo(
    () =>
      steps.map((stepComponent, index) => {
        return React.cloneElement(stepComponent.component, {
          key: index,
          onNext: next,
          stepsData,
          isActive: currentStepIndex === index,
          setStepsData,
        });
      }),
    [steps, next, currentStepIndex, stepsData]
  );

  return {
    currentStepIndex,
    step: stepComponents[currentStepIndex],
    steps: stepComponents,
    originalSteps: steps,
    isFirstStep: currentStepIndex === 0,
    isLastStep: currentStepIndex === steps.length - 1,
    goTo,
    next,
    back,
  };
}

Component:

import React from 'react';

import AdditionalInfo from '@/components/ui/PropertyForm/Form/AdditionalInfo';
import BasicInfo from '@/components/ui/PropertyForm/Form/BasicInfo';
import Images from '@/components/ui/PropertyForm/Form/Images';
import LocationInfo from '@/components/ui/PropertyForm/Form/LocationInfo';
import { useMultiStepForm } from '@/hooks/useMultiStepForm';
import { PropertyType } from '@/types';

import StepNavigation from './StepNavigation';
import Videos from './Videos';
import VisitingTimeInfo from './VisitingTimeInfo';

const placeHolderFn = () => {};
export default function Form({
  defaultValues,
}: {
  defaultValues?: PropertyType;
}) {
  const { currentStepIndex, step, goTo, originalSteps } = useMultiStepForm<any>(
    [
      {
        component: (
          <BasicInfo
            stepsData={{}}
            onNext={placeHolderFn}
            setStepsData={placeHolderFn}
            key={'basic-info'}
            defaultValues={defaultValues}
          />
        ),
        label: 'Basic Info',
      },
      //NOTE landlord is not allowed to edit location
      ...(defaultValues
        ? []
        : [
            {
              component: (
                <LocationInfo
                  stepsData={{}}
                  key={'location'}
                  onNext={placeHolderFn}
                  setStepsData={placeHolderFn}
                  defaultValues={defaultValues}
                />
              ),
              label: 'Location',
            },
          ]),
      {
        component: (
          <AdditionalInfo
            stepsData={{}}
            key={'availability-and-amenities'}
            onNext={placeHolderFn}
            setStepsData={placeHolderFn}
            defaultValues={defaultValues}
          />
        ),
        label: 'Additional Info',
      },
      {
        component: (
          <VisitingTimeInfo
            stepsData={{}}
            key={'visiting-times'}
            onNext={placeHolderFn}
            setStepsData={placeHolderFn}
            defaultValues={defaultValues}
          />
        ),
        label: 'VISIT TIMES',
      },

      {
        component: (
          <Images
            stepsData={{}}
            onNext={placeHolderFn}
            setStepsData={placeHolderFn}
            key={'images'}
            defaultValues={defaultValues}
          />
        ),
        label: 'Images',
      },
      {
        component: (
          <Videos
            stepsData={{}}
            onNext={placeHolderFn}
            setStepsData={placeHolderFn}
            key={'videos'}
            defaultValues={defaultValues}
          />
        ),
        label: 'Videos',
      },
    ]
  );

  return (
    <div>
      <StepNavigation
        allowNavigationOnClick={!!defaultValues}
        currentStepIndex={currentStepIndex}
        steps={originalSteps}
        goTo={goTo}
        isEditing={!!defaultValues}
      />
      <div>{step}</div>
    </div>
  );
}

Not the answer you're looking for? Browse other questions tagged or ask your own question.