Reorderable Lists and ordering arrays

My client would like the ability to reorder rows easily in a table, I was looking into using the “Reorderable List” component, but there is limited documentation on it. Can someone help provide some documentation on this component?

It seems like you can provide any array of strings and then be able to reorder them and submit that reordered string array to a query.

For example sake, let’s say I have rows of my favorite movies and in each row is rank_id (where rank_id is an integer to be sorted from lowest to highest, e.g. my number 1 ranked movie is Repo Man), release_year, director, and movie_title. Do you recommend displaying the whole row as JSON or something so they can see the content they’re reordering?

Thanks!

Hello @gilcrest! Sorry for the delay, the Retool team has been enjoying a few days off :slight_smile: Apologies about the lack of documentation: we indeed do need to add some more info about this component to our components reference.

To answer your question: the reorderable list component works exclusively through arrays, so you can provide an array you want to reorder and then access the re-ordered value via the reorderableList.value property. If your data structure is closer to having a rank_id value for each record in a DB or API, there's no clean way to use a "drag and drop" kind of feature in Retool; you could try this though:

  • Display your data in a table
  • Use the "default column to sort by" setting and choose the rank_id
  • Set up a "details panel" (a container with info about the row in text components)
  • Use a dropdown component to reference the currently selected row's rank_id (or textinput if it's easier)
  • Hook up a query that updates a selected row's rank_id to the value in the dropdown
  • Run that query when you select a new rank_id from the dropdown, or on button click

Here's what a basic prototype of that might look like:

Alternatively, you can use the reorderable list component and display some data about the record in your data to make it clear what you're reordering. So if you have one row per name in your dataset, you could display the names from your data as an array (e.g. {{ query.data.name }}) and then hook up a query to update your data based on how that array gets reordered in the component. Keep in mind that you can use the .value property of the reorderable list to get an array in the order that's currently displayed in the component.

Let me know if either of these help!!

2 Likes

@justin - thanks very much for the super detailed response and creative ideas! I think I am going to use something similar to the prototype you added here vs the reorderable list. I’ll let you know when I finally implement!

1 Like

awesome, looking forward to hearing back!

Hey @gilcrest. Thanks for the post. I too am interested in hearing about / seeing your end result.

2 Likes

Hey everyone - working on this... Question though - in the component, there is also a field Array of HTML / Markdown to render in place. What is this meant to do? I can kind of guess, but I'd rather not... Thanks!

were you ever able to figure out how to work the array of HTML to render for the reorderablelist?

Hey @gilcrest (sorry for the delay here) and @anthony (thank you for reopening this)!

You can map over the reorderablelist3.value array to apply HTML or Markdown to each list item :blush: Let me know if you have any questions getting this setup!


3 Likes

thank you!!

Of course!

Here's another example of HTML with a different data source + multiple data values :slight_smile:

is there a way to rig an onClick event or a button on the list to do something like deleting an item from the list?

You could likely have the values ("Array to order") come from a temp state value (docs on temp state here), and then have a button run a query that changes the temp state. Otherwise, we don't have any native onClick events in Retool (yet!)

Hi @victoria wanted to check in on anthony's question about rigging an onClick event to delete items from a reorderable list: I followed your example and created a reorderable list with list elements that have a checkbox element next to my text. The idea being that if they are selected I can click my remove button to get rid of those elements in the temp state that the reorderable list is rendering.

I gave each of the input elements a unique id and tried to access them in a query to see if they've been checked or not using document.getElementById. But I think my query is sandboxed in its own iframe away from these elements so all my document.getElementById calls are returning null.

Here is my query where console.log returns null for a valid id:

state1.value.map((row) => { 
  const thisChkBoxElement = document.getElementById(row.slug + "_chk");
  console.log(thisChkBoxElement); // returns null
  console.log(document); // the document object contains only the iframe see below
}

This is what I'm seeing in console when I look at the document object returned by the second console.log (none of the elements in my retool app are in this document):

Is there anyway to get the state of the HTML input buttons that I put into a reorderable list from a query? Can you elaborate on your recommendation to use temp state value to do this?

1 Like

I couldn't get retool's reorderable list hooked up in a way that I could remove elements using onClick. So I created my own reorderable list custom component using Atlassian's react dnd library.

Screen Shot 2021-11-06 at 9.05.38 PM

Here is the code in case its helpful to anyone else here!

<style>
  body {
    margin: 0;
  }
</style>
<script src="https://cdn.tryretool.com/js/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-beautiful-dnd@5.0.0/dist/react-beautiful-dnd.js" crossorigin></script>
<script src="https://cdn.tryretool.com/js/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/@material-ui/core@3.9.3/umd/material-ui.production.min.js"></script>

<div id="react"></div>

<script type="text/babel">
	const { Button } = window["material-ui"];
	const {DragDropContext, Draggable, Droppable } = window.ReactBeautifulDnd;

  const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => 
  {
    // a little function to help us with reordering the result
    const reorder =  (list, startIndex, endIndex) => {
      const result = Array.from(list);
      const  = result.splice(startIndex, 1);
      result.splice(endIndex, 0, removed);

      return result;
    };

    const gGrid = 2;

    const getItemStyle = (draggableStyle, isDragging) => { 
      const returnStyle = {
        // some basic styles to make the items look a bit nicer
        display: "flex",
        alignItems: "center",
        justifyContent: "space-between",
        userSelect: 'none',
        padding: gGrid * 2,
        margin: `0 0 ${gGrid}px 0`,
        color: "white",
        fontFamily: "sans-serif",
        fontSize: "small",

        // change background colour if dragging
        background: isDragging ? '#CE316C' : '#E07BA1',
      };
      return Object.assign(returnStyle, draggableStyle);
		}

    const getListStyle = (isDraggingOver) => ({
      background: isDraggingOver ? '#3c92dc' : '#555F7D',
      padding: gGrid,
      width: "100%"
    });
    
    const updateListState = (newState) => {
      	console.log("DnD Component:" + JSON.stringify(newState));
        modelUpdate({"state": newState, "new_state": newState});
    }
    
    const onDragEnd = (result) => {
        // dropped outside the list
        if(!result.destination) {
           return; 
        }
        const newState = reorder(
          model.state, 
          result.source.index, 
          result.destination.index
        );
      	updateListState(newState);
  	}
    
    const onRemoveItem = (event) => {
      if(event.target)
      {
        const removeID = event.currentTarget.getAttribute("refid");
        const newState = model.state.filter((row) => {return (row.slug != removeID); });
        updateListState(newState);
      }
    }

		return (
			<DragDropContext onDragEnd={onDragEnd}>
        <Droppable droppableId="droppable">
					{(provided, snapshot) => (
 						<div 
              ref={provided.innerRef} 
              style={getListStyle(snapshot.isDraggingOver)}
              {...provided.droppableProps}
            >
          			{model.state.map((item,index) => (
                   <Draggable
                      key={item.slug}
                      draggableId={item.slug}
                      index={index}
                    >
                      {(provided, snapshot) => (
                        <div>
                          <div
                            ref={provided.innerRef}
                            {...provided.dragHandleProps}
                            {...provided.draggableProps}
                            style={getItemStyle(
                              provided.draggableProps.style,
                              snapshot.isDragging
                            )}
                          >
                            	<div>{item.title}</div>
                            <Button size="small" color="primary" variant="contained" refid={item.slug} onClick={onRemoveItem}>Remove</Button>
                          </div>
                          {provided.placeholder}
                        </div>
                       )}
                    </Draggable>              
              ))}
              {provided.placeholder}
            </div>
          )}
  			</Droppable>
      </DragDropContext>
    );
	}


  const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
  ReactDOM.render(<ConnectedComponent />, document.getElementById("react"));
</script>

To use it create a custom component, copy and paste the code above into it and try the component out with this test data:

{"state": [{ "slug": "1", "title": "first item"}, {"slug": "2", "title": "second item"}], }

The sorted list is published to model.new_state

4 Likes

Hey Baback

This looks like it is exactly what I am after! I have created a new Custom Component with the code and test data above, but am just seeing a blank list.
image

Is there anything else that I need to do to get this working?

Thanks

Hey @turps808,

Interesting... I looked back at my code and have one thought: if you copied the code exactly, I think I forgot to make the array elements passed into the custom component's Model element into a javascript block.

Try this instead of what I posted before in the Model attribute:

{"state": {{[{ "slug": "1", "title": "first item"}, {"slug": "2", "title": 'second item'}]}}, }

And here is the exact code my component is running at the moment in case there were any changes in the interim:

<style>
  body {
    margin: 0;
  }
</style>
<script src="https://cdn.tryretool.com/js/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-beautiful-dnd@5.0.0/dist/react-beautiful-dnd.js" crossorigin></script>
<script src="https://cdn.tryretool.com/js/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/@material-ui/core@3.9.3/umd/material-ui.production.min.js"></script>

<div id="react"></div>

<script type="text/babel">
	const { Button } = window["material-ui"];
	const {DragDropContext, Draggable, Droppable } = window.ReactBeautifulDnd;

  const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => 
  {
    // a little function to help us with reordering the result
    const reorder =  (list, startIndex, endIndex) => {
      const result = Array.from(list);
      const  = result.splice(startIndex, 1);
      result.splice(endIndex, 0, removed);

      return result;
    };

    const gGrid = 2;

    const getItemStyle = (draggableStyle, isDragging) => { 
      const returnStyle = {
        // some basic styles to make the items look a bit nicer
        display: "flex",
        alignItems: "center",
        justifyContent: "space-between",
        userSelect: 'none',
        padding: gGrid * 2,
        margin: `0 0 ${gGrid}px 0`,
        color: "white",
        fontFamily: "sans-serif",
        fontSize: "small",

        // change background colour if dragging
        background: isDragging ? '#CE316C' : '#D75686',
      };
      return Object.assign(returnStyle, draggableStyle);
		}

    const getListStyle = (isDraggingOver) => ({
      background: isDraggingOver ? '#3c92dc' : '#555F7D',
      padding: gGrid,
      width: "100%"
    });
    
    const updateListState = (newState) => {
      	console.log("DnD Component:" + JSON.stringify(newState));
        modelUpdate({"state": newState, "new_state": newState});
    }
    
    const onDragEnd = (result) => {
        // dropped outside the list
        if(!result.destination) {
           return; 
        }
        const newState = reorder(
          model.state, 
          result.source.index, 
          result.destination.index
        );
      	updateListState(newState);
  	}
    
    const onRemoveItem = (event) => {
      if(event.target)
      {
        const removeID = event.currentTarget.getAttribute("refid");
        const newState = model.state.filter((row) => {return (row.slug != removeID); });
        updateListState(newState);
      }
    }

		return (
			<DragDropContext onDragEnd={onDragEnd}>
        <Droppable droppableId="droppable">
					{(provided, snapshot) => (
 						<div 
              ref={provided.innerRef} 
              style={getListStyle(snapshot.isDraggingOver)}
              {...provided.droppableProps}
            >
          			{model.state.map((item,index) => (
                   <Draggable
                      key={item.slug}
                      draggableId={item.slug}
                      index={index}
                    >
                      {(provided, snapshot) => (
                        <div>
                          <div
                            ref={provided.innerRef}
                            {...provided.dragHandleProps}
                            {...provided.draggableProps}
                            style={getItemStyle(
                              provided.draggableProps.style,
                              snapshot.isDragging
                            )}
                          >
                            	<div>{item.title}</div>
                            <Button size="small" color="primary" variant="contained" refid={item.slug} onClick={onRemoveItem}>Remove</Button>
                          </div>
                          {provided.placeholder}
                        </div>
                       )}
                    </Draggable>              
              ))}
              {provided.placeholder}
            </div>
          )}
  			</Droppable>
      </DragDropContext>
    );
	}


  const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
  ReactDOM.render(<ConnectedComponent />, document.getElementById("react"));
</script>

And here is the editor setup in retool:

Hope that helps!

Hi Baback - many thanks for taking the time to reply. Sadly this is still not working for me - I'll need to do a bit of in depth investigation. Out of interest, you don't have anything set up in the Scripts & Styles section that this solution relies on (Libraries, CSS etc)...?

I had a very similar need for a more custom reorderable list and your solution was perfect, @Baback thank you!!

@Baback and @turps808 I saw a similar blank page using the code provided, was able to figure out what was going on in the example provided.

There is a line in there that has invalid syntax causing the custom component to crash and show the blank page, const = result.splice(startIndex, 1); specifically causes it to crash.

I recopied that code with the fix below hope this helps!

<style>
  body {
    margin: 0;
  }
</style>
<script src="https://cdn.tryretool.com/js/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-beautiful-dnd@5.0.0/dist/react-beautiful-dnd.js" crossorigin></script>
<script src="https://cdn.tryretool.com/js/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/@material-ui/core@3.9.3/umd/material-ui.production.min.js"></script>

<div id="react"></div>

<script type="text/babel">
	const { Button } = window["material-ui"];
	const {DragDropContext, Draggable, Droppable } = window.ReactBeautifulDnd;

  const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => 
  {
    // a little function to help us with reordering the result
    const reorder =  (list, startIndex, endIndex) => {
      const result = Array.from(list);
      const removed = result.splice(startIndex, 1);
      result.splice(endIndex, 0, removed);

      return result;
    };

    const gGrid = 2;

    const getItemStyle = (draggableStyle, isDragging) => { 
      const returnStyle = {
        // some basic styles to make the items look a bit nicer
        display: "flex",
        alignItems: "center",
        justifyContent: "space-between",
        userSelect: 'none',
        padding: gGrid * 2,
        margin: `0 0 ${gGrid}px 0`,
        color: "white",
        fontFamily: "sans-serif",
        fontSize: "small",

        // change background colour if dragging
        background: isDragging ? '#CE316C' : '#D75686',
      };
      return Object.assign(returnStyle, draggableStyle);
		}

    const getListStyle = (isDraggingOver) => ({
      background: isDraggingOver ? '#3c92dc' : '#555F7D',
      padding: gGrid,
      width: "100%"
    });
    
    const updateListState = (newState) => {
      	console.log("DnD Component:" + JSON.stringify(newState));
        modelUpdate({"state": newState, "new_state": newState});
    }
    
    const onDragEnd = (result) => {
        // dropped outside the list
        if(!result.destination) {
           return; 
        }
        const newState = reorder(
          model.state, 
          result.source.index, 
          result.destination.index
        );
      	updateListState(newState);
  	}
    
    const onRemoveItem = (event) => {
      if(event.target)
      {
        const removeID = event.currentTarget.getAttribute("refid");
        const newState = model.state.filter((row) => {return (row.slug != removeID); });
        updateListState(newState);
      }
    }

		return (
			<DragDropContext onDragEnd={onDragEnd}>
        <Droppable droppableId="droppable">
					{(provided, snapshot) => (
 						<div 
              ref={provided.innerRef} 
              style={getListStyle(snapshot.isDraggingOver)}
              {...provided.droppableProps}
            >
          			{model.state.map((item,index) => (
                   <Draggable
                      key={item.slug}
                      draggableId={item.slug}
                      index={index}
                    >
                      {(provided, snapshot) => (
                        <div>
                          <div
                            ref={provided.innerRef}
                            {...provided.dragHandleProps}
                            {...provided.draggableProps}
                            style={getItemStyle(
                              provided.draggableProps.style,
                              snapshot.isDragging
                            )}
                          >
                            	<div>{item.title}</div>
                            <Button size="small" color="primary" variant="contained" refid={item.slug} onClick={onRemoveItem}>Remove</Button>
                          </div>
                          {provided.placeholder}
                        </div>
                       )}
                    </Draggable>              
              ))}
              {provided.placeholder}
            </div>
          )}
  			</Droppable>
      </DragDropContext>
    );
	}


  const ConnectedComponent = Retool.connectReactComponent(MyCustomComponent);
  ReactDOM.render(<ConnectedComponent />, document.getElementById("react"));
</script>

Is anyone having trouble in which reorderable list wont scroll to show the complete array??? this is driving me crazy. So I wanted to reorder a table, but since is not posible I added a modal where users can reorder the rows showing minimun information, but if the rows are a quite few, the UI will break....

1 Like

Hey @charliemolain! Happy to help here. I can't seem to reproduce this issue, but I definitely see the problem in your screenshot. Would it be alright if I hopped into your app to take a look? If not, totally okay! It just make take a little more back and forth to reproduce