jQuery UI Sortable with React.js buggy
Asked Answered
O

3

6

I have a sortable list in React which is powered by jQuery UI. When I drag and drop an item in the list, I want to update the array so that the new order of the list is stored there. Then re-render the page with the updated array. i.e. this.setState({data: _todoList});

Currently, when you drag and drop an item, jQuery UI DnD works, but the position of the item in the UI does not change, even though the page re-renders with the updated array. i.e. in the UI, the item reverts to where it used to be in the list, even though the array that defines its placement has updated successfully.

If you drag and drop the item twice, then it moves to the correct position.

    // Enable jQuery UI Sortable functionality
    $(function() {
      $('.bank-entries').sortable({
        axis: "y",
        containment: "parent",
        tolerance: "pointer",
        revert: 150,
        start: function (event, ui) {
            ui.item.indexAtStart = ui.item.index();
        },
        stop: function (event, ui) {
            var data = {
                indexStart: ui.item.indexAtStart,
                indexStop: ui.item.index(),
                accountType: "bank"
            };
            AppActions.sortIndexes(data);
        },
      });
    });

    // This is the array that holds the positions of the list items
    var _todoItems = {bank: []};

    var AppStore = assign({}, EventEmitter.prototype, {
      getTodoItems: function() {
        return _todoItems;
      },
      emitChange: function(change) {
        this.emit(change);
      },
      addChangeListener: function(callback) {
        this.on(AppConstants.CHANGE_EVENT, callback);
      },
      sortTodo: function(todo) {
        // Dynamically choose which Account to target
        targetClass = '.' + todo.accountType + '-entries';

        // Define the account type
        var accountType = todo.accountType;

        // Loop through the list in the UI and update the arrayIndexes
        // of items that have been dragged and dropped to a new location
        // newIndex is 0-based, but arrayIndex isn't, hence the crazy math
        $(targetClass).children('form').each(function(newIndex) {
          var arrayIndex = Number($(this).attr('data-array-index'));
          if (newIndex + 1 !== arrayIndex) {
            // Update the arrayIndex of the element
            _todoItems[accountType][arrayIndex-1].accountData.arrayIndex = newIndex + 1;
          }
        });

        // Sort the array so that updated array items move to their correct positions
        _todoItems[accountType].sort(function(a, b){
          if (a.accountData.arrayIndex > b.accountData.arrayIndex) {
            return 1;
          }
          if (a.accountData.arrayIndex < b.accountData.arrayIndex) {
            return -1;
          }
          // a must be equal to b
          return 0;
        });

        // Fire an event that re-renders the UI with the new array
        AppStore.emitChange(AppConstants.CHANGE_EVENT);
      },
    }


  function getAccounts() {
    return { data: AppStore.getTodoItems() }
  }

  var Account = React.createClass({
      getInitialState: function(){
          return getAccounts();
      },
      componentWillMount: function(){
          AppStore.addChangeListener(this._onChange);

          // Fires action that triggers the initial load
          AppActions.loadComponentData();
      },
      _onChange: function() {
          console.log('change event fired');
          this.setState(getAccounts());
      },
      render: function(){
          return (
              <div className="component-wrapper">
                  <Bank data={this.state.data} />
              </div>
          )
      }
  });
Optimism answered 19/4, 2015 at 2:30 Comment(3)
Here's an example that might be helpful: gist.github.com/petehunt/7882164Browse
Thanks for the suggestion @MikeDriver, but this line puts me off "The key thing to note is that we have the render() method do absolutely nothing". I'm trying to utilize the render method to keep with React/Flux architecture.Optimism
I feel that if you're deviating from the react architecture enough to use a jquery plugin instead of equivalent functionality native to react - then compromises have to be made. I'm not saying you shouldn't use a jquery plugin inside react - clearly there are cases where this is the only practical solution, however then trying to keep things "reactish" is a bit shutting the barn door after the horse has bolted IMO.Browse
O
6

The reason jQuery UI Sortable doesn't work with React is because it directly mutates the DOM, which is a big no no in React.

To make it work, you would have to modify jQuery UI Sortable so that you keep the DnD functionality, but when you drop the element, it does not modify the DOM. Instead, it could fire an event which triggers a React render with the new position of the elements.

Optimism answered 23/4, 2015 at 6:7 Comment(0)
W
12

The trick is to call sortable('cancel') in the stop event of the Sortable, then let React update the DOM.

componentDidMount() {
    this.domItems = jQuery(React.findDOMNode(this.refs["items"]))
    this.domItems.sortable({
        stop: (event, ui) => {
            // get the array of new index (http://api.jqueryui.com/sortable/#method-toArray)
            const reorderedIndexes = this.domItems.sortable('toArray', {attribute: 'data-sortable'}) 
            // cancel the sort so the DOM is untouched
            this.domItems.sortable('cancel')
            // Update the store and let React update (here, using Flux)
            Actions.updateItems(Immutable.List(reorderedIndexes.map( idx => this.state.items.get(Number(idx)))))
        }
    })
}
Waggery answered 4/10, 2015 at 23:29 Comment(2)
Thanks! This is the simplest work-around I've found so farPearly
YES! cancel is the only one that works -- 100% beats implementing a 'Sortable' ReactJS componentEssary
O
6

The reason jQuery UI Sortable doesn't work with React is because it directly mutates the DOM, which is a big no no in React.

To make it work, you would have to modify jQuery UI Sortable so that you keep the DnD functionality, but when you drop the element, it does not modify the DOM. Instead, it could fire an event which triggers a React render with the new position of the elements.

Optimism answered 23/4, 2015 at 6:7 Comment(0)
U
2

Since React uses a Virtual DOM, you have to use the function React.findDOMNode() to access an actual DOM element.

I would call the jQuery UI function inside the componentDidMount method of your component because your element has to be already rendered to be accessible.

// You have to add a ref attribute to the element with the '.bank-entries' class
$( React.findDOMNode( this.refs.bank_entries_ref ) ).sortable( /.../ );

Documentation - Working with the browser (everything you need to know is here)

Hope that makes sense and resolves your issue

Umberto answered 20/4, 2015 at 22:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.