Search code examples
javascriptreactjstypescriptform-data

How to manage nested objects in formdata with dynamic inputs?


I am using react to create a form with multiple dynamic inputs. This is my form.tsx:

interface Property {
  name: string;
  values: string;
}

function CreateForm({ categories }: { categories: CategoryField[] }) {
  const [properties, setProperties] = useState<Property[]>([]);
  const initialState = {
    message: null,
    errors: {},
  };

  function addProperty() {
    setProperties((prev) => {
      return [...prev, { name: '', values: '' }];
    });
  }
  function handlePropertyNameChange(
    index: number,
    property: Property,
    newName: string
  ) {
    setProperties((prev) => {
      const properties = [...prev];
      properties[index].name = newName;
      return properties;
    });
  }
  function handlePropertyValuesChange(
    index: number,
    property: Property,
    newValues: string
  ) {
    setProperties((prev) => {
      const properties = [...prev];
      properties[index].values = newValues;
      return properties;
    });
  }
  function removeProperty(indexToRemove: number) {
    setProperties((prev) => {
      return [...prev].filter((p, pIndex) => {
        return pIndex !== indexToRemove;
      });
    });
  }
  const [state, dispatch] = useFormState(saveCategory, initialState);

  console.log('properties: ', properties);
  return (
    <div>
      <form
        action={(form) => {
          const p = form.get('name');
          // const parent = form.get('')
          const k = new FormData();
          const k2 = new Array().concat(properties);

          console.log('k2 before split: ', k2, ' properties: ', properties);
          
          k2.forEach((p: any) => {
            p.values = p.values.split(',');
          });
          console.log('k2 after forEach: ', k2);
          k.append('properties', JSON.stringify(k2));
          if (p) {
            k.append('name', p);
          }

          dispatch(k);
        }}
      >
    /* more static inputs here */

     {/* properties */}
          <div className='mb-2'>
            <label className='block' htmlFor='properties'>
              Properties
            </label>
            <button
              onClick={addProperty}
              type='button'
              className='btn-default text-sm mb-2'
            >
              Add new property
            </button>
            {properties.length > 0 &&
              properties.map((property, index) => (
                <div key={index} className='flex gap-1 mb-2'>
                  <input
                    type='text'
                    className='mb-0'
                    onChange={(ev) =>
                      handlePropertyNameChange(index, property, ev.target.value)
                    }
                    value={property.name}
                    placeholder='property name (example: color)'
                  />
                  <input
                    type='text'
                    className='mb-0'
                    onChange={(ev) =>
                      handlePropertyValuesChange(
                        index,
                        property,
                        ev.target.value
                      )
                    }
                    value={property.values}
                    placeholder='values, comma separated'
                  />
                  <button
                    onClick={() => removeProperty(index)}
                    type='button'
                    className='btn-red'
                  >
                    Remove
                  </button>
                </div>
              ))}
          </div>
        </div>
        <Button type='submit'> Add category </Button>
      </form>
    </div>

And this is my action.ts:

const FormSchema = z.object({
  id: z.string(),
  name: z.string(),
  parentCategory: z
    .string({
      invalid_type_error: 'Please select a category',
    })
    .nullable()
    .optional(),
  properties: z.string(),
});


export type CategoryState = {
  errors?: {
    name?: string[];
    parentCategory?: string[];
    properties?: string[];
  };
  message?: string | null;
};

const CreateCategory = FormSchema.omit({ id: true });

export async function saveCategory(
  prevState: CategoryState,
  formData: FormData
) {
  console.log('FormData:::: ', formData, prevState);
  const validatedFields = CreateCategory.safeParse({
    parentCategory: formData.get('parentCategory'),
    properties: formData.get('properties'),
    name: formData.get('name'),
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'Missing Fields. Failed to Create Invoice.',
    };
  }

  // Prepare data for insertion into the database
  const { parentCategory, properties, name } = validatedFields.data;

  console.log(
    'This is the parsed data:::::  ',
    parentCategory,
    properties,
    name
  );

  const parsed = JSON.parse(properties);
  console.log('parsed with json: ', JSON.parse(properties));
..........

So the whole idea is to add as many properties inputs as the user wants, where each property is an array of strings, separated by ,. I want to parse my formdata to achieve this format:

{
name: 'whatevername'
properties: {
 property1name: ["array" , "of", "string values"],
 property2name: ["another property"]
}

}

Right now I'm managing this using a useState hook to count and remember how many properties inputs are, and then pass them to the dispatch action. However, this doesnt feel right, even though it works for now. It doesn't really feel "elegant"


Solution

  • For Forms in React you can use the react-hook-form library. It's pretty simple to learn and very useful.

    Here is the documentation. It has some advantages over classic controlled forms. I Strongly advice you to use it, It makes it much easier to manage nested form value objects.