Check if input field is changed
Asked Answered
T

1

6

I am making a stepper form using React. And the form structure is almost done..

There are two steps,

-> Basic Details

-> Employment Details

Here a form context has been used to fill out the input field default value and also if any changes made then that gets stored via state.

form-context.js

import React, { useState } from 'react';

export const FormContext = React.createContext();

export function FormProvider({ children }) {
  const [formValue, setFormValue] = useState({
    basicDetails: {
      firstName: 'John',
      lastName: '',
    },
    companyDetails: {
      companyName: 'Some Company',
      designation: 'Some Designation',
    },
  });

  return (
    <FormContext.Provider value={[formValue, setFormValue]}>
      {children}
    </FormContext.Provider>
  );
}

Here I have next and previous button to move between steps,

index.js:

  const next = () => setCurrentPage((prev) => prev + 1);
  const prev = () => setCurrentPage((prev) => prev - 1);

Requirement:

-> On click over next/previous button, I am in the need to check whether any input is changed.

-> Through this I can call API to save the changes on click of next button in my real application.

-> Here if you help me to make some console log then that would be more than enough.

Eg:

-> If user modify the first name in basic detail section from John to Doe then on click of next button can console log that there is change in basic details.

-> If none of the fields are changed in basic details section then it can directly proceed to next step (as like now).

Note: Please don't hard code any input name because I have more than 30 input fields for each step..

Edit next-dynamic-testing-issue (forked)

Tanked answered 16/11, 2020 at 11:53 Comment(0)
Y
3

I think the main issue here is that your current implementation replaces existing state, on each change, so it is impossible to know if/what changed. Not even a "usePrevious" hook to hold the previous state value would work here since it would only hold the last previous field edit, not the original value, for comparison. Many form handling solutions handle this by keeping an initial state that isn't mutated, or tracking "dirty" fields, and/or more.

Here's a suggestion of mine to tweak your form context state to track fields that have been updated. The dirtyFields object holds the original value.

FormProvider

const [formValue, setFormValue] = useState({
  basicDetails: {
    fields: {
      firstName: 'John',
      lastName: '',
    },
    dirtyFields: {},
  },
  companyDetails: {
    fields: {
      companyName: 'Some Company',
      designation: 'Some Designation',
    },
    dirtyFields: {},
  },
});

BasicDetails

const BasicDetails = () => {
  const [value, setValue] = React.useContext(FormContext);
  const {
    basicDetails: { fields }, // <-- get field values
  } = value;

  const handleInputChange = (event) => {
    const { name, value } = event.target;

    setValue((prev) => ({
      ...prev,
      basicDetails: {
        ...prev.basicDetails,
        fields: {
          ...prev.basicDetails.fields,
          [name]: value,
        },
        ...(prev.basicDetails.dirtyFields[name] // <-- only mark dirty once with original value
          ? {}
          : {
              dirtyFields: {
                ...prev.basicDetails.dirtyFields,
                [name]: prev.basicDetails.fields[name],
              },
            }),
      },
    }));
  };

  return (
    <>
      <div className="form-group col-sm-6">
        <label htmlFor="firstName">First Name</label>
        <input
          ...
          value={fields.firstName} // <-- access fields object
          ...
        />
      </div>
      <div className="form-group col-sm-4">
        <label htmlFor="lastName">Last Name</label>
        <input
          ...
          value={fields.lastName}
          ...
        />
      </div>
    </>
  );
};

EmploymentDetails

const EmploymentDetails = () => {
  const [value, setValue] = React.useContext(FormContext);
  const {
    companyDetails: { fields },
  } = value;

  const handleInputChange = (event) => {
    const { name, value } = event.target;

    setValue((prev) => ({
      ...prev,
      companyDetails: {
        ...prev.companyDetails,
        fields: {
          ...prev.companyDetails.fields,
          [name]: value,
        },
        ...(prev.companyDetails.dirtyFields[name]
          ? {}
          : {
              dirtyFields: {
                ...prev.companyDetails.dirtyFields,
                [name]: prev.companyDetails.fields[name],
              },
            }),
      },
    }));
  };

  return (
    <>
      <div className="form-group col-sm-6">
        <label htmlFor="companyName">Company Name</label>
        <input
          ...
          value={fields.companyName}
          ...
        />
      </div>
      <div className="form-group col-sm-4">
        <label htmlFor="designation">Designation</label>
        <input
          ...
          value={fields.designation}
          ...
        />
      </div>
    </>
  );
};

Check dirty fields when incrementing/decrementing the step.

Give each section an id that matches the "form key" in the form context.

const sections = [
  {
    title: 'Basic Details',
    id: 'basicDetails',
    onClick: () => setCurrentPage(1),
  },
  {
    title: 'Employment Details',
    id: 'companyDetails',
    onClick: () => setCurrentPage(2),
  },
  { title: 'Review', id: 'review', onClick: () => setCurrentPage(3) },
];

Create a checkDirty utility. Here I simply log the dirty fields

const checkDirty = (page) => {
  console.log('check dirty', 'page', page);
  console.log(
    value[sections[page - 1].id] && value[sections[page - 1].id].dirtyFields,
  );
};

const next = () => {
  setCurrentPage((prev) => prev + 1);
  checkDirty(currentPage); // <-- check for dirty fields when updating page step
};

const prev = () => {
  setCurrentPage((prev) => prev - 1);
  checkDirty(currentPage);
};

Because of the extra nested state in the form context, here's a utility to reduce it back to just form data you want rendered on the review step.

const prettyReview = sections.reduce(
  (sections, section) => ({
    ...sections,
    ...(value[section.id]
      ? { [section.id]: { ...value[section.id].fields } }
      : {}),
  }),
  {},
);

...

<pre>{JSON.stringify(prettyReview, null, 2)}</pre>

Edit check-if-input-field-is-changed

Edit

You said your data comes from a backend API call. Here's a context state initialization function that maps the API data shape to the state shape I have.

Given data from API

const apiData = {
  basicDetails: {
    firstName: 'John',
    lastName: '',
  },
  companyDetails: {
    companyName: 'Some Company',
    designation: 'Some Designation',
  },
};

Initialization function

const initializeContext = (data) =>
  Object.entries(data).reduce(
    (sections, [section, fields]) => ({
      ...sections,
      [section]: {
        fields,
        dirtyFields: {},
      },
    }),
    {},
  );

Initialize the FormProvider context state

function FormProvider({ children }) {
  const [formValue, setFormValue] = useState(initializeContext(apiData));

  return (
    <FormContext.Provider value={[formValue, setFormValue]}>
      {children}
    </FormContext.Provider>
  );
}
Yerga answered 16/11, 2020 at 17:53 Comment(9)
Thanks for your detailed answer.. I will accept your solution.. Do you have codesandbox to share regarding the same implementation?Tanked
@Tanked I thought I didn't, but I had it saved in my account so I must've intended to and just neglected to add a link to it in my answer. I've now added it.Yerga
Oh now I got point why it is not working in my real application.. In my application, these JSON values for form are coming from backend API, So I cannot modify the JSON value like you did.. Do you of any help here? Sorry its my mistake of not pointing out there.. Because thought the solution would be straight forward..Tanked
@Tanked Ah, I see... well you can create a new object from the backend data object, basically shallow copy it into the new shape and add the "dirty" properties. Assuming your backend data is the current shape from your question I can update my answer tomorrow in the morning with a mapping function (if you need it).Yerga
Yes Drew, Please add your new solution by your time morning.. I will try to implement that one as well.. Really you are much helpful and thanks for it..Tanked
@Tanked Updated answer with an initialization function to map the data shape. This should get you close to what you seek.Yerga
Thanks much Drew Reese, Appreciate your whole hearted help.. This works for me..Tanked
Bro can you help me in a question where I took the solution from your earlier answer but it doesn't work.. Question here: https://mcmap.net/q/1914834/-scroll-into-view-in-react/13270726Tanked
Did you get chance to help me in the above given question? Awaiting for your solution since yesterday.. Please kindly help me Reese..Tanked

© 2022 - 2024 — McMap. All rights reserved.