Problems with DynamicTable in Form

Hi, do you have any tips for using Form components inside DynamicTable rows? I’ve found that:

  1. The default values aren’t visible on these Textfield components
  2. Their values aren’t retrievable with getValues and handleSubmit - and changes aren’t recognised in the formState object.

Here’s some code that demonstrates the issue.

import React from 'react';
import ForgeReconciler, { useForm, DynamicTable, Form, Label, Textfield, Button } from '@forge/react';

import { head, presidents } from './data';

const App = () => {

  const presidentEntries = Object.fromEntries(
    presidents.flatMap((president: { id: number; name: string; party: string; term: string; }) => [
      [`president.${president.id}.name`, president.name],
      [`president.${president.id}.party`, president.party],
      [`president.${president.id}.term`, president.term],
    ])
  );
  presidentEntries['president.writein.name'] = 'Steamboat Mickey'

  const { register, handleSubmit, getFieldId, getValues, formState } = useForm({defaultValues: presidentEntries});

  const formRows = presidents.map( (president, index) => ({
    key: `row-${index}`,
    cells: [
      {
        key: `row-${index}-name`,
        content: <Textfield {...register(`president.${president.id}.name`), {appearance: "subtle"}} />,
      },
      {
        key: `row-${index}-party`,
        content: <Textfield {...register(`president.${president.id}.party`), {appearance: "subtle"}} />,
      },
      {
        key: `row-${index}-term`,
        content: <Textfield {...register(`president.${president.id}.term`), {appearance: "subtle"}} />,
      },
    ],
  }));
  
  return (
    <Form onSubmit={handleSubmit((data) => {console.log(data); console.log(getValues('president')); console.log(formState)})}>
      <DynamicTable
        caption="List of US Presidents"
        head={head}
        rows={formRows}
      />
      <Label labelFor={getFieldId('president.writein.name')}>Your write-in president</Label>
      <Textfield {...register('president.writein.name')} />
      <Button appearance="primary" type="submit">Submit</Button>
    </Form>
  );
};

ForgeReconciler.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

On submission, the formState is reporting changes to the ‘president.writein.name’ field but not to any changes to fields in the DynamicTable.

{
    "errors": {},
    "isSubmitted": true,
    "isSubmitSuccessful": true,
    "isSubmitting": false,
    "submitCount": 2,
    "isValid": true,
    "dirtyFields": {
        "president": {
            "writein": {
                "name": true
            }
        }
    },
    "touchedFields": {
        "president": {
            "writein": {
                "name": true
            }
        }
    }
}

Is there something that I can do to make DynamicTable play more nicely with Form?

Okay, here’s a working example with the useReducer hook. useReducer is like the useAction hook. This is in TypeScript latest.

Although this works, the useForm hook doesn’t appear functional at all, with register and handleSubmit having no effect.

import React, { useReducer } from 'react';
import ForgeReconciler, { useForm, DynamicTable, Button, Form, Textfield } from '@forge/react';
import Flexirank from 'flexirank';

import { head, presidents } from './data';

const App = () => {

  interface RankEnd { sourceIndex: number; sourceKey: string; destination?: { index: number; afterKey?: string; beforeKey?: string; }; }
  interface President { [key: string]: string | number | undefined; id: number; name: string; party: string; term: string; rank?: string }

  const updatePresidentsList = (currentPresidents: President[], presidentChange: any) => {
    let newPresidents: President[] = Array.from(currentPresidents ?? createPresidentsList(presidents));
    switch (presidentChange.type) {
      case 'rank_change':
        const rankChange: RankEnd = presidentChange['rankChange'];
        if (rankChange.destination?.beforeKey && rankChange.destination?.afterKey) {
          newPresidents[rankChange.sourceIndex]!.rank = Flexirank.newRank({
            previousItemRank: rankChange.destination.beforeKey,
            nextItemRank: rankChange.destination.afterKey
          });
        } else if (rankChange.destination?.beforeKey) {
          newPresidents[rankChange.sourceIndex]!.rank = Flexirank.newRank({
            previousItemRank: rankChange.destination.beforeKey
          });
        } else if (rankChange.destination?.afterKey) {
          newPresidents[rankChange.sourceIndex]!.rank = Flexirank.newRank({
            nextItemRank: rankChange.destination.afterKey
          });
        }
        newPresidents = newPresidents.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''));
        break;
      case 'cell_change':
        const input = presidentChange.event.target;
        const presidentId = input.name.split('.')[1] as number;
        const presidentColumn = input.name.split('.')[2] as string;
        const presidentIndex = newPresidents.findIndex((president) => president.id == presidentId)
        if (newPresidents[presidentIndex]![presidentColumn] != input.value) {
          newPresidents[presidentIndex]![presidentColumn] = input.value
        }
        break;
    }
    console.log(newPresidents);
    return newPresidents
  };

  const createPresidentsList = (defaultPresidents: President[]) => {
    let thisRank = Flexirank.newRank();
    return defaultPresidents.map((president: President) => {
      president.rank = thisRank;
      thisRank = Flexirank.newRank({ previousItemRank: thisRank });
      return president;
    });
  };

  const [myPresidents, dispatchChange] = useReducer(
    updatePresidentsList,
    presidents as President[],
    createPresidentsList
  );

  const saveChanges = (event: any) => console.log(event);

  const columns = ['name', 'party', 'term'];

  const { register, handleSubmit } = useForm({
    defaultValues: Object.fromEntries(
      myPresidents.flatMap((president: President) => columns.map((column) =>
        [`president.${president.id}.${column}`, president[column]])
      ))
  });

  return <Form onSubmit={handleSubmit(() => console.log('onSubmit never executes'))}>
    <DynamicTable
      caption="List of US Presidents"
      head={head}
      rows={myPresidents.map((president: President) => {
        const rowKey = president.rank ?? `row-${president.id}`;
        return {
          key: rowKey, cells: columns.map((column) => {
            return {
              key: `${rowKey}-${column}`, content: <Textfield {
                ...register(`president.${president.id}.${column}`), {
                  name: `president.${president.id}.${column}`,
                  appearance: 'subtle',
                  isCompact: true,
                  onBlur: (event) => dispatchChange({ type: 'cell_change', event: event }),
                  defaultValue: president[column],
                }
              } />
            }
          })
        }
      })}
      isRankable={true}
      onRankEnd={(rankEnd: RankEnd) => dispatchChange({ type: 'rank_change', rankChange: rankEnd })}
    />
    <Button appearance="primary" onClick={(event) => saveChanges(event)}>Submit</Button>
  </Form>

};

ForgeReconciler.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);