Material-table: Support draggable rows

Created on 18 Oct 2019  路  45Comments  路  Source: mbrn/material-table

Is your feature request related to a problem? Please describe.
It would be nice if material-table would support reordering rows by dragging them

Describe the solution you'd like
A new option to mark a table as row draggable and a callback to be notified when a row is dragged.

Describe alternatives you've considered
Is this possible to achieve by overriding components and using react-beautiful-dnd ? If so, this would be fine but I think a built-in option would be cleaner.

feature help wanted

Most helpful comment

Sure. The use case is where the order of rows is important, such as a list of rules where the first rule is applied first. The user would drag around the rules in the order that they like and this order would be persisted on the server - you would most likely disable the column sorting feature for this use case. Usually when this feature is available a Drag Handle icon is added to the row.

Here's an example from another project: https://www.ag-grid.com/javascript-grid-row-dragging/

All 45 comments

Rows are typically sorted according to the corresponding column and order direction. I don't see how something like you suggest might be useful. Mind to describe the case?

Sure. The use case is where the order of rows is important, such as a list of rules where the first rule is applied first. The user would drag around the rules in the order that they like and this order would be persisted on the server - you would most likely disable the column sorting feature for this use case. Usually when this feature is available a Drag Handle icon is added to the row.

Here's an example from another project: https://www.ag-grid.com/javascript-grid-row-dragging/

i would also like to see this feature. i'd like to test material tables for one of the projects i'm working on, but can't unless this feature is implemented.

I definitely need this feature. I'm using material-table and I love it so far. I'm surprised they don't have draggable rows feature. I have a requirement for something like drag and drop rows. I've tried various options like overriding the MTableBody and MTableRow with react-beautiful-dnd but none worked.

PRs are welcome - never forget :P

I would also need this feature. Is there a workaround I can implement meanwhile?

+1

+1 here.

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1 thans!!

+1

+1

+1

+1

+1

+1

+1

+1

It will be tricky in a tree structure

Any news on this topic ?
thanks

+1

+1

+1

Overriding row component I was able to give drag support. Please check may be helping you as well

 components={{
                                        Row: props => (
                                            <MTableBodyRow {...props}
                                                draggable="true"
                                                onDragStart={(e) => {
                                                    console.log('onDragStart');
                                                    DrageState.row = props.data.tableData.id

                                                }}

                                                onDragEnter={e => {
                                                    e.preventDefault();
                                                    if( props.data.tableData.id != DrageState.row){
                                                        DrageState.dropIndex = props.data.tableData.id ;

                                                    }                                                  
                                                }}

                                                onDragEnd={(e) => {
                                                    console.log(`onDragEnd`);
                                                    if(DrageState.dropIndex != -1){
                                                        reodering({index: DrageState.row, dropIndex: DrageState.dropIndex})
                                                    }
                                                    DrageState.row = -1;
                                                    DrageState.dropIndex = -1;

                                                }}
                                            />

                                        )
                                    }

@rucsacman dude, it looks like a cool quick solution but I can't understand it. There are misspelling errors and stuff that get me confused. What's DrageStatus ?

+1

@rucsacman dude, it looks like a cool quick solution but I can't understand it. There are misspelling errors and stuff that get me confused. What's DrageStatus ?

Hi Dude ,
Yes, it should be DragState.
It will keep the clicked row and when mouse button release position of the near dropIndex keep the data about the new row position.

"reodering" function reorder the table rows.

const DragState = { row: -1, //current row dropIndex: -1 // drag target };

Sorry for the confused

I see that there are still comments about this feature request.

For everyone that is still looking for a solution - there was a PR opened with a complete implementation of that.
You can check #1995 (Draggable rows / rows reordering / drag & drop rows).

The PR was closed because the maintainer decided to close all "big" PRs and not merge them before a refactoring phase to the library (which is currently undergoing, but there is no ETA for when it will be done).

Maybe if more people will try and push it inside the maintainer will do so.

@rucsacman can u please provide full example as am not able to achieve it

Overriding row component I was able to give drag support. Please check may be helping you as well

 components={{
                                        Row: props => (
                                            <MTableBodyRow {...props}
                                                draggable="true"
                                                onDragStart={(e) => {
                                                    console.log('onDragStart');
                                                    DrageState.row = props.data.tableData.id

                                                }}

                                                onDragEnter={e => {
                                                    e.preventDefault();
                                                    if( props.data.tableData.id != DrageState.row){
                                                        DrageState.dropIndex = props.data.tableData.id ;

                                                    }                                                  
                                                }}

                                                onDragEnd={(e) => {
                                                    console.log(`onDragEnd`);
                                                    if(DrageState.dropIndex != -1){
                                                        reodering({index: DrageState.row, dropIndex: DrageState.dropIndex})
                                                    }
                                                    DrageState.row = -1;
                                                    DrageState.dropIndex = -1;

                                                }}
                                            />

                                        )
                                    }

@rucsacman can u please provide full example as am not able to achieve it

Overriding row component I was able to give drag support. Please check may be helping you as well

 components={{
                                        Row: props => (
                                            <MTableBodyRow {...props}
                                                draggable="true"
                                                onDragStart={(e) => {
                                                    console.log('onDragStart');
                                                    DrageState.row = props.data.tableData.id

                                                }}

                                                onDragEnter={e => {
                                                    e.preventDefault();
                                                    if( props.data.tableData.id != DrageState.row){
                                                        DrageState.dropIndex = props.data.tableData.id ;

                                                    }                                                  
                                                }}

                                                onDragEnd={(e) => {
                                                    console.log(`onDragEnd`);
                                                    if(DrageState.dropIndex != -1){
                                                        reodering({index: DrageState.row, dropIndex: DrageState.dropIndex})
                                                    }
                                                    DrageState.row = -1;
                                                    DrageState.dropIndex = -1;

                                                }}
                                            />

                                        )
                                    }
const DrageState = {
    row: -1,
    dropIndex: -1 // drag target
};

<MaterialTable
            components={{
                Row: props => (
                    <MTableBodyRow {...props}
                        draggable="true"
                        onDragStart={(e) => {
                            console.log('onDragStart');
                            DrageState.row = props.data.tableData.id
                        }}
                        onDragEnter={e => {
                            e.preventDefault();
                            if( props.data.tableData.id !== DrageState.row){
                                DrageState.dropIndex = props.data.tableData.id ;
                            }                                                  
                        }}
                        onDragEnd={(e) => {
                            console.log(`onDragEnd`);
                            if(DrageState.dropIndex !== -1){
                                reOrderRow(DrageState.row, DrageState.dropIndex)
                            }
                            DrageState.row = -1;
                            DrageState.dropIndex = -1;
                        }}
                    />
                )
            }} // components
        />

//Reorder the table row
const offsetIndex = (from, to, arr = []) => {
    if (from < to) {
      let start = arr.slice(0, from),
        between = arr.slice(from + 1, to + 1),
        end = arr.slice(to + 1);
      return [...start, ...between, arr[from], ...end];
    }
    if (from > to) {
      let start = arr.slice(0, to),
        between = arr.slice(to, from),
        end = arr.slice(from + 1);
      return [...start, arr[from], ...between, ...end];
    }
    return arr;
  }
const reOrderRow = (from, to) =>{
    let newtableData = offsetIndex(from, to, tableData);
    //Update react state
    setTableData(newtableData);
}

Rucsacman unfortunately I could not get it done by the way you suggested, I think MTableBodyRow does not have onDragStart/onDragEnter/onDragEnd properties, so it did not work for me.

Nevertheless, using react-beautiful-dnd library and watching the demo here:
https://egghead.io/lessons/react-persist-list-reordering-with-react-beautiful-dnd-using-the-ondragend-callback

I was able to combine material-table with react-beatiful-dnd as such:


import React from "react";
import ReactDOM from "react-dom";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import MaterialTable, { MTableBody, MTableBodyRow } from "material-table";

export default function App() {
  const values = [
    { name: "one" },
    { name: "two" },
    { name: "three" },
    { name: "four" },
  ];

  const [myState, setMyState] = React.useState(values);

  const onDragEnd = (result) => {
    const { destination, source } = result;
    if (!destination) return;
    if (source.index !== destination.index) {
      let copyArray = [...myState];
      let temp = myState[source.index];
      copyArray.splice(source.index, 1);
      copyArray.splice(destination.index, 0, temp);
      setMyState(copyArray);
    }
  };

  return (
    <div style={{ maxWidth: "100%" }}>
      <MaterialTable
        columns={[{ title: "Numbers", field: "name" }]}
        data={myState}
        title=""
        components={{
          Body: (props) => (
            <div>
              <DragDropContext onDragEnd={onDragEnd}>
                <Droppable droppableId={"singleColumnDroppableAreaThusStaticInput"}>
                  {(provided) => (
                    <div ref={provided.innerRef}>
                      <MTableBody {...props} />
                      {provided.placeholder}
                    </div>
                  )}
                </Droppable>
              </DragDropContext>
            </div>
          ),
          Row: (props) => (
            <Draggable
              draggableId={props.data.tableData.id.toString()}
              index={props.data.tableData.id}
            >
              {(provided) => (
                <div
                  {...provided.draggableProps}
                  {...provided.dragHandleProps}
                  ref={provided.innerRef}
                >
                  <MTableBodyRow {...props} />
                </div>
              )}
            </Draggable>
          ),
        }}
      />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

this works but I am getting a few warnings on the console. When grouped, these are:

  1. I am inserting div's into table/tbody/tr's thus getting validateDOMNesting warnings
  2. MTable is adding overflow auto into a div above the MTableBody, and react beautiul dnd is annoyed that there's a nested scroll that it cannot control, I haven't looked more into it, but looks like with the help of this community, this structure can be improved.

Thanks y'all

@rucsacman can u please provide full example as am not able to achieve it

Overriding row component I was able to give drag support. Please check may be helping you as well

 components={{
                                        Row: props => (
                                            <MTableBodyRow {...props}
                                                draggable="true"
                                                onDragStart={(e) => {
                                                    console.log('onDragStart');
                                                    DrageState.row = props.data.tableData.id

                                                }}

                                                onDragEnter={e => {
                                                    e.preventDefault();
                                                    if( props.data.tableData.id != DrageState.row){
                                                        DrageState.dropIndex = props.data.tableData.id ;

                                                    }                                                  
                                                }}

                                                onDragEnd={(e) => {
                                                    console.log(`onDragEnd`);
                                                    if(DrageState.dropIndex != -1){
                                                        reodering({index: DrageState.row, dropIndex: DrageState.dropIndex})
                                                    }
                                                    DrageState.row = -1;
                                                    DrageState.dropIndex = -1;

                                                }}
                                            />

                                        )
                                    }
const DrageState = {
    row: -1,
    dropIndex: -1 // drag target
};
<MaterialTable
            components={{
                Row: props => (
                    <MTableBodyRow {...props}
                        draggable="true"
                        onDragStart={(e) => {
                            console.log('onDragStart');
                            DrageState.row = props.data.tableData.id
                        }}
                        onDragEnter={e => {
                            e.preventDefault();
                            if( props.data.tableData.id !== DrageState.row){
                                DrageState.dropIndex = props.data.tableData.id ;
                            }                                                  
                        }}
                        onDragEnd={(e) => {
                            console.log(`onDragEnd`);
                            if(DrageState.dropIndex !== -1){
                                reOrderRow(DrageState.row, DrageState.dropIndex)
                            }
                            DrageState.row = -1;
                            DrageState.dropIndex = -1;
                        }}
                    />
                )
            }} // components
        />
//Reorder the table row
const offsetIndex = (from, to, arr = []) => {
    if (from < to) {
      let start = arr.slice(0, from),
        between = arr.slice(from + 1, to + 1),
        end = arr.slice(to + 1);
      return [...start, ...between, arr[from], ...end];
    }
    if (from > to) {
      let start = arr.slice(0, to),
        between = arr.slice(to, from),
        end = arr.slice(from + 1);
      return [...start, arr[from], ...between, ...end];
    }
    return arr;
  }
const reOrderRow = (from, to) =>{
  let newtableData = offsetIndex(from, to, tableData);
  //Update react state
  setTableData(newtableData);
}

Thank you it worked for me馃檪
Can we include an icon to each row next to/ before checkbox to indicate to drag,
As it was failing to include

@rucsacman Kindly help with it

Rucsacman unfortunately I could not get it done by the way you suggested, I think MTableBodyRow does not have onDragStart/onDragEnter/onDragEnd properties, so it did not work for me.

Nevertheless, using react-beautiful-dnd library and watching the demo here:
https://egghead.io/lessons/react-persist-list-reordering-with-react-beautiful-dnd-using-the-ondragend-callback

I was able to combine material-table with react-beatiful-dnd as such:


import React from "react";
import ReactDOM from "react-dom";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import MaterialTable, { MTableBody, MTableBodyRow } from "material-table";

export default function App() {
  const values = [
    { name: "one" },
    { name: "two" },
    { name: "three" },
    { name: "four" },
  ];

  const [myState, setMyState] = React.useState(values);

  const onDragEnd = (result) => {
    const { destination, source } = result;
    if (!destination) return;
    if (source.index !== destination.index) {
      let copyArray = [...myState];
      let temp = myState[source.index];
      copyArray.splice(source.index, 1);
      copyArray.splice(destination.index, 0, temp);
      setMyState(copyArray);
    }
  };

  return (
    <div style={{ maxWidth: "100%" }}>
      <MaterialTable
        columns={[{ title: "Numbers", field: "name" }]}
        data={myState}
        title=""
        components={{
          Body: (props) => (
            <div>
              <DragDropContext onDragEnd={onDragEnd}>
                <Droppable droppableId={"singleColumnDroppableAreaThusStaticInput"}>
                  {(provided) => (
                    <div ref={provided.innerRef}>
                      <MTableBody {...props} />
                      {provided.placeholder}
                    </div>
                  )}
                </Droppable>
              </DragDropContext>
            </div>
          ),
          Row: (props) => (
            <Draggable
              draggableId={props.data.tableData.id.toString()}
              index={props.data.tableData.id}
            >
              {(provided) => (
                <div
                  {...provided.draggableProps}
                  {...provided.dragHandleProps}
                  ref={provided.innerRef}
                >
                  <MTableBodyRow {...props} />
                </div>
              )}
            </Draggable>
          ),
        }}
      />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

this works but I am getting a few warnings on the console. When grouped, these are:

  1. I am inserting div's into table/tbody/tr's thus getting validateDOMNesting warnings
  2. MTable is adding overflow auto into a div above the MTableBody, and react beautiul dnd is annoyed that there's a nested scroll that it cannot control, I haven't looked more into it, but looks like with the help of this community, this structure can be improved.

Thanks y'all

Hope material-table internally react dnd, we need to explicitly import again
Which version of mat table you are using?
Try to re-implement with his solution

material-table internally imports react-dnd, it is one of its dependencies, and I am using the latest version of material-table, but somehow rucsacman's solution did not work for me. Can you post yours on github or somewhere, so I can look?

Hope material-table internally react dnd, we need to explicitly import again
Which version of mat table you are using?
Try to re-implement with his solution

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jlgreene2 picture jlgreene2  路  3Comments

victorwvieira picture victorwvieira  路  3Comments

kfirshahar picture kfirshahar  路  3Comments

lazeebee picture lazeebee  路  3Comments

Likurg2010 picture Likurg2010  路  3Comments