javascriptreactjsdragula

How to retain object attributes when re-ordering with dragula?


The below code is for a simple component that fetches a list of colors from an API endpoint. A user can then drag colors between a left container and a right container. In the componentDidMount lifecycle method, the component pushes all of the colors from the API into the component's state as objects with the following attributes:

{ id, name, index }

These colors are correctly placed into the left container when the state updates. The right container remains empty.

In the render method, if I add a logger to spit out the availableColors array, each object has a name, ID, and index. As it should be. E.g:

{ id: 1, name: 'red', index: 0 }

But when I drag and drop colors from the left container into the right container and the dragula on drop callback is executed, I can only access the innerHTML of each color added to the right container. This means I lose out on the object's properties like the ID it got from the API.

In other words, where I'm pushing into newColorList, color.id is blank. I think my issue is that I shouldn't be fetching the dropped elements using:

const targetContainer = document.querySelector('#right');
const selectedColorItems = targetContainer.getElementsByTagName("li");

How should I fix this code?

class DragApp extends Component {
  constructor(props) {
    super(props);

    this.state = {
      availableColors: [],
      selectedColors: []
    }
  }

  componentDidMount() {
    fetch('/api/color-list.json')
      .then(function(response) {
        return response.json()
      })
      .then(function(json) {
        var availableColors = [];

        json.forEach(function(color, index) {
          availableColors.push({ index, name: color.name, id: color.id })
        });

        this.setState({ availableColors });
      }.bind(this))
      .catch(function(ex) {
        // handle failure
      });

    dragula([document.querySelector('#left'), document.querySelector('#right')])
      .on('drop', function(el, _) {
          const newColorList = [];
          const targetContainer = document.querySelector('#right');
          const selectedColorItems = targetContainer.getElementsByTagName("li");

          Array.from(selectedColorItems).forEach(function(color) {
            // getIndexInParent returns index of element
            const index = getIndexInParent(color);
            newColorList.push({ index, name: color.innerHTML, id: color.id })
          })

          this.setState({ selectedColors: newColorList });
      }.bind(this));
  }

  render() {
    const colorsList = this.state.availableColors;
    const colors = colorsList.map((color) =>
      <li key={ color.id }>
        { color.name }
      </li>
    );

    return (
      <div className='wrapper'>
        <ul id="left" className="container">
          { colors }
        </ul>

        <ul id="right" className="container"></ul>
      </div>
    )
  }
}

Solution

  • I suggest to use the data attributes to store your colors info and retrieve them when you drop the li:

    class DragApp extends Component {
        constructor(props) {
            super(props);
    
            this.state = {
                availableColors: [],
                selectedColors: []
            }
        }
    
        componentDidMount() {
            fetch('/api/color-list.json')
                .then(function(response) {
                    return response.json()
                })
                .then(function(json) {
                    var availableColors = [];
    
                    json.forEach(function(color, index) {
                        availableColors.push({
                            index,
                            name: color.name,
                            id: color.id
                        })
                    });
    
                    this.setState({
                        availableColors
                    });
                }.bind(this))
                .catch(function(ex) {
                    // handle failure
                });
    
            dragula([document.getElementById('#left'), document.getElementById('#right')])
                .on('drop', function(el, _) {
                    const targetContainer = document.querySelector('#right');
                    const selectedColorItems = targetContainer.getElementsByTagName("li");
                    newColorList = Array.from(selectedColorItems).map(function(color) {
                        return {
                            index: color.dataset.index,
                            id: color.dataset.id,
                            name: color.text
                        };
                    });
                    this.setState({
                        selectedColors: newColorList
                    });
                }.bind(this));
        }
    
        render() {
            const colorsList = this.state.availableColors;
            const colors = colorsList.map((color) =>
                <li key = { color.id } data-id="{ color.id }" data-index="{ color.index }">
                    { color.name }
                </li>
            );
    
            return (
                <div className='wrapper'>
                    <ul id="left" className="container">
                        { colors }
                    </ul>
    
                    <ul id="right" className="container"> </ul>
                </div>
            )
        }
    }
    

    If you want to keep the getIndexInParent idea, you should use array.find()

    var getIndexInParent = function getIndexInParent(id) {
        return this.state.availableColors.find(function(color) {
            return color.id == id;
        });
    };