I have a dynamic table with many rows. Each rows position can be changed within the table by first clicking the row you want to move and then clicking the row where you want to move it to. Only the updated order of entities is needed for the backend/database update. This doesn't allow adding new entities or updating their info (no CRUD).
Each row also has two action "buttons" (or radio buttons) that just set a boolean value for that specific column. The selected one should of course follow the row when the rows are being re-ordered.
Name | Is ready | Is in production |
---|---|---|
Foo | [ ] | [x] |
Bar | [x] | [ ] |
The form should not be valid before both of the radio button columns have a set value.
What I now have is a useState for the order of entities in the table. That is updated solely in the frontend and then looping the updated order of entities and adding a hidden input field with the entity's primary key so that the action can read the updated order after a submit.
I can get the radio buttons work by following the same way of storing the state in UI but that feels like I'm just skipping the Remix altogether. I can't come up with a nice way to communicate each of the clicks (either reordering the rows or clicking radio buttons) to the action handler so that the radio buttons would stay within the row they were activated in.
I guess the question is, can this be done in the way of remix, and if, how?
Here's an example on how to use fetcher.Form
to submit actions. I created a component <FetcherCell>
that will automatically submit whenever the child input changes.
Each submit includes an intent
value that the action
function uses to determine what to do. isReady
and isInProduction
updates the value for that id
.
Whereas the moveup
and movedown
intents are used to reorder the data items. And reset
uses the Remix Form
to reset the data.
As you can see, there is not a single useState
in sight.
https://stackblitz.com/edit/remix-run-remix-acm3aq?file=app%2Froutes%2F_index.tsx
export async function action({ request }: DataFunctionArgs) {
let formData = await request.formData();
let { id, intent, ...data } = Object.fromEntries(formData.entries());
switch (intent) {
case 'isReady':
case 'isInProduction': {
let value = data[intent] === 'true';
await updateData(Number(id), { [intent]: value });
break;
}
case 'moveup':
case 'movedown': {
await updateOrder(JSON.parse(String(data.orders)));
break;
}
case 'reset': {
await resetData();
break;
}
default:
throw new Response('Bad Request', { status: 400 });
}
return json({ success: true });
}
{data.map((item) => (
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.name}</td>
<td>
<FetcherCell id={item.id} intent="isReady">
<input
name="isReady"
type="checkbox"
value="true"
defaultChecked={item.isReady}
/>
</FetcherCell>
</td>
<td>
<FetcherCell id={item.id} intent="isInProduction">
<input
name="isInProduction"
type="checkbox"
value="true"
defaultChecked={item.isInProduction}
/>
</FetcherCell>
</td>
<td style={{ display: 'flex', gap: '1rem' }}>
<FetcherCell id={item.id} intent="moveup">
<button
name="orders"
value={getOrderUp(data, item.id)}
disabled={item.order < 2}
>
Up
</button>
</FetcherCell>
<FetcherCell id={item.id} intent="movedown">
<button
name="orders"
value={getOrderDown(data, item.id)}
disabled={item.order >= data.length}
>
Down
</button>
</FetcherCell>
</td>
<td>{item.order}</td>
</tr>
))}
function FetcherCell({ id, intent, children }: FetcherCellProps) {
const fetcher = useFetcher();
const handler = useCallback(
(e: FormEvent) => fetcher.submit(e.currentTarget as HTMLFormElement),
[fetcher]
);
return (
<fetcher.Form method="post" onChange={handler}>
<input type="hidden" name="intent" value={intent} />
<input type="hidden" name="id" value={id} />
{children}
</fetcher.Form>
);
}