# How to Create Draggable Rows in React Admin

> A step by step example on how to create draggable rows in React Admin. This includes examples available to create your own React project.

HTML: https://endertech.com/blog/how-to-create-draggable-data-rows-in-react-admin
Published: 2021-08-11T00:00:00+00:00
Categories: Web Development

Introduction

This article branches off from a previous introduction to React Admin and how to extend it. If you need a refresher to React Admin, you can read the first article or explore React Admin’s documentation page. In this tutorial, I’ll explain how to order nested data entities with draggable rows in React.

More specifically, we are going to:

Download a starter repository on Github

Customize the React Admin data display properties

Implement drag-n-drop using Material-UI and react-beautiful-dnd

Query the React Admin data provider to save the data after it’s “dropped”

Adding draggable rows in Reach are a simple way to create a rich user interface for your React Admin projects, so I’m excited to get started.

Prerequisites

There are a couple of prerequisites that this tutorial requires:

Understanding of how ReactJS works

Basic networking jargon (i.e., request, API call, fetch)

How to open and use the command line on your OS

Git installed and configured

Code editor (i.e., VSCode, Sublime Text, Atom, Vim)

I am using VSCode on a macOS system. If you need help along the way, check out the complete repository on Github for guidance.

Download Starting Repository

To download the starter template, open up a new terminal (command-line) and navigate to where you want to place the repository.

Then, execute the following command to clone the repo.

git clone https://github.com/jdretz/starter-template-react-admin-draggable-rows.git react-admin-drag-n-drop

Change directories into the new repository and install the dependencies.

cd react-admin-drag-n-drop
yarn install

Finally, after downloading all the packages, run yarn start to run the application. A new browser should open up to http://localhost:3000, and you’ll see the app’s dashboard.

Project Overview

The application has one central resource: Forms. Users can add, remove, delete, or edit a form. Nested inside each form are a few simple data fields.

If you click the inline Edit button on a form, the app takes you to the edit view for that form. There, you can edit or view the form’s:

ID

Title

Questions

Furthermore, the Questions field is an array where you can add, edit, or delete the questions on each form.

The app retrieves the data from your browser’s local storage with the help of a mock service worker. The service worker has a series of handlers (at src/mocks/handlers.js) that intercepts requests from the custom data provider (in src/dataProvider.js).
Therefore, if you ever need to reset the data during this tutorial, you can go into your browser’s developer tools, navigate to local storage, and delete the “forms” item.

Using a mock service worker as a data provider is not something that you should do in production. However, it’s convenient for the tutorial.

Customize Array Input Component

The view components for the Forms resource are inside src/forms.js.

The component holds the array input where we want to implement the drag-n-drop.

...
export const FormsEdit = props => (
 ...

 ...
)
...

Create a new file in the src director named CustomIterator.js.

Inside CustomIterator.js add the code below:

import React from 'react';
import { FieldArray } from 'react-final-form-arrays';
import { TextInput, NumberInput } from 'react-admin';
import Add from '@material-ui/icons/Add';
import Button from '@material-ui/core/Button';

const CustomIterator = ({ record }) => {
 return (
 
 {(fieldProps) => {
 return (
 
 {fieldProps.fields.map((question, index) => {
 return (

 fieldProps.fields.remove(index)}>
 Remove

 )
 })}
 fieldProps.fields.push({ id: '', question: '' })}
 color="secondary"
 variant="contained"
 style={{ marginTop: '16px' }}
 >

 )
 }
 }
 
 )
}

export default CustomIterator;

In a moment, I’ll explain what’s changed, but first—back in forms.js—import the custom iterator and use it to replace the .

...
export const FormsEdit = props => (

 // new

);
...

Save the file, and take a look at the new iterator.

It’s lost some of its visual appeals. But, we gained access to the inner iterator component. Moving forward, we can customize the iterator inside of the component while working within React Admin’s workflow. Per the documentation;

“ expects a single child, which must be a form iterator component. A form iterator is a component accepting a fields object as passed by react-final-form-array, and defining a layout for an array of fields.”

Under the hood, React Admin uses the react-final-form-arrays library to create the form iterator. Consequently, we can re-engineer parts of the component using components straight from that package.

In the custom iterator, it’s worth noting that sourcing the suitable properties for inputs became more complex, but we can still use dot notation to access nested elements.

Defining a Material-UI Table Layout

Next, Material-UI components provide us a table layout and drag icon. The next step uses Material-UI’s:

Wrap the current custom iterator elements with the appropriate components.

… other imports
import Table from '@material-ui/core/Table'; // new
import TableBody from '@material-ui/core/TableBody'; // new
import TableCell from '@material-ui/core/TableCell'; // new
import TableContainer from '@material-ui/core/TableContainer'; // new
import TableRow from '@material-ui/core/TableRow'; // new
import DragHandleIcon from '@material-ui/icons/DragHandle'; // new

const CustomIterator = ({ record }) => {
 return (
 
 {(fieldProps) => {
 return (
 
 // new
 // new
 // new
 {fieldProps.fields.map((question, index) => {
 return (
 // new
 // new
 // new
 // new
 // new
 
 // new
 // new
 
 // new
 // new
 fieldProps.fields.remove(index)}>
 Remove
 
 // new
 // new
 )
 })}
 // new
 // new
 fieldProps.fields.push({ id: '', question: '' })}
 color="secondary"
 variant="contained"
 style={{ marginTop: '16px' }}
 >

 // new
 
 )
 }
 }
 
 )
}

export default CustomIterator;

Save the file after adding the new input structure. The table is now ready for the drag-n-drop feature!

Add Drag-N-Drop Feature

The react-beautiful-dnd library uses a series of React contexts and refs to create smooth user interactions.

The breakdown for adding drag-n-drop to our project is as follows;

Everything that’s part of the drag-n-drop experience needs to be inside the .

Use the component to provide a landing zone for the components in motion.

Wrap draggable items inside the component

Provide the required props and refs along the way.

To map the above directives onto our table we need to:

Wrap the in the 

Wrap the with the component, adding required props to 

Wrap each with a component, adding props to the 

Spread drag handle props onto the parent of our 

The new custom iterator file should look like the code below.

...other imports
import { Draggable, DragDropContext, Droppable } from 'react-beautiful-dnd'; // new

const CustomIterator = ({ record }) => {
 return (
 
 {(fieldProps) => {
 return (

 {/* new */}
 {(provided, snapshot) => ( // new
 
 {fieldProps.fields.map((question, index) => {
 return (
 {/* new */}
 {(provided, snapshot) => ( // new

 fieldProps.fields.remove(index)}>
 Remove

 )}
 // new
 )
 })}
 {provided.placeholder} {/* new */}
 // new
 )}
 {/* new */}
 
 fieldProps.fields.push({ id: '', question: '' })}
 color="secondary"
 variant="contained"
 style={{ marginTop: '16px' }}
 >

 // new
 )
 }}
 
 )
...

Again, save the file and test out the drag-n-drop animations!

The animations and drag handle work exactly as we want. Unfortunately, the changes do not persist. In the next step, we add an onDragEnd function and React Admin mutation query.

Persisting Changes

OnDragEnd

The exposes several callbacks that execute at different points in the interaction. The one that we focus on in this tutorial is onDragEnd. After a user drops the row into the location, we need to save the new order by sending a mutate API call to our data provider.

First, above the main return statement in define the onDragEnd function.

const CustomIterator = ({ record }) => {

 const onDragEnd = (result, provided) => {
 const {
 source,
 destination
 } = result;

 console.log("Item at index " + source.index + " is now at index " + destination.index)

 // Get the item
 const item = record.questions[source.index];

 // Remove item from array
 const newArray = record.questions.filter((el, index) => index !== source.index);

 // Insert item at destination
 newArray.splice(destination.index, 0, item)

 // Call mutation function
 // reorder(newArray)
 }

return (
...
 
...

The callback function receives two arguments. The first, result, is an object providing source and destination information from the drop event. Save the file and move a few of the rows around. Then, check the developer console to see how the indices give us information on ordering the new array.

Reorder

Currently, the reorder function is commented out. The function comes from React Admin’s useMutation hook. Add useMutation, and the useNotify hook, to the function.

import { TextInput, NumberInput, useNotify, useMutation } from 'react-admin'; // modified
...
const CustomIterator = ({ record }) => {
 const notify = useNotify();
 const [mutate, { loading }] = useMutation({}, {
 onSuccess: () => {
 notify('Questions reordered', 'info', {}, true);
 },
 onFailure: (error) => notify(`Error: ${error.message}`, 'warning'),
 });
...

Next, we use the mutate function, returned from useMutation, to pass in a payload for the data provider dynamically.

...
const CustomIterator = ({ record }) => {
 const notify = useNotify();
 const [mutate, { loading }] = useMutation({}, {
 ...
 });

 const reorder = (newQuestions) => mutate({
 type: 'update',
 resource: 'forms',
 payload: {
 id: record.id,
 data: { questions: newQuestions }
 }
 });

 const onDragEnd = (result, provided) => {
...

The function is now named reorder. After defining the reorder function, you can uncomment the call to reorder() in onDragEnd.

Save the file. The data now persists through each drag-n-drop!

Unfortunately, it’s not perfect yet.

The drag-n-drop animation ends, but React Admin renders the old list before updating the table with the new data.

We can’t undo the reorder in the notification pop-up.

Final Touch

Thankfully, this is an easy fix because React Admin exposes a property on the configuration object that we pass into the useMutation function.

The property (mutationMode) modifies React Admin’s default optimistic rendering behavior.

To resolve both bugs, add the property to the configuration object and set the value to ‘undoable’.

...
const CustomIterator = ({ record }) => {
 const notify = useNotify();
 const [mutate, { loading }] = useMutation({}, {
 // https://marmelab.com/react-admin/Actions.html#optimistic-rendering-and-undo
 mutationMode: 'undoable', // new
 onSuccess: () => {
 notify('Questions reordered', 'info', {}, true);
 },
 onFailure: (error) => notify(`Error: ${error.message}`, 'warning'),
 });
...

For the final time in this tutorial, save the file and reorder the questions list.

Beautiful!

Conclusion

Using a couple of free libraries, we quickly enhanced the look and features of our React Admin project!

However, the drag-n-drop may be too good. To clarify, the feature automatically saves the modified entity, but the add and remove buttons do not behave in the same way. To improve on this form and not confuse the user, the add and remove buttons should automatically save the modified entity, providing an undo button to reverse the action.

Despite this discrepancy, we can be proud of what we were able to accomplish! Thanks for reading and following along!
