Making a custom reorderable list component

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?

2 Likes

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

I ran into this issue too. Reorderable list seems to have a fixed height and no ability to handle overflow by using something like a scrollbar or auto-expanding the component's height.

Ah! Understood. We have an ongoing bug report for this, just added this conversation to the ticket so I can report back with any updates. Thanks for taking the time to clarify here, @benknight :slight_smile:

Any update on adding a scrollbar to the reorderable list component or fixing the bug? I'm unable to use the reorderable list component without that functionality. I upgraded to the latest version (2.111.9) of the self hosted instance and I'm still seeing the issue. Thanks.

Hey @Matthew.Clayton! No updates yet as the team working on components is currently focusing on the Table component. Nevertheless, I posted your comment internally to let the team know!

1 Like

Any updates on that?

My answer is going the be the same as above, I'm afraid. No updates yet, but I did add your +1 internally! And to anyone else seeing this, please do continue adding your +1s so we can prioritize accordingly :slight_smile:

+1

I wish I could use the reorderable list component a lot more, it's very limited in comparison to the others

1 Like

Thank you for your input! Noted.

+1

Noted, thank you!

+1

Noted (and bumped internally). Thank you!