Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jquery-UI sortable list not playing nice with reactive updates in Meteor template

I'm trying to implement a sortable list of objects with JQuery-UI in the manner described at http://differential.com/blog/sortable-lists-in-meteor-using-jquery-ui.

However, rather than sort a list of separate documents, I'm sorting a list of objects embedded within a single document. That is, I have a document like so:

{  name: "Name of this Rolodex",
   cards: [{name: "...", rank: 0, id: "some-unique-id"},
           {name: "...", rank: 1, id: "some-other-unique-id"}, ... ]
}

And I just want to make the cards sortable. My template is as follows -- it's passed a single Rolodex as the context:

<template name="rolodex">
  Rolodex Name: {{name}}
  <div class="cards-list">
    {{#each sortedCards}}
      {{> cardTemplate}}
    {{/each}}
  </div>
</template>

And the helper JS file:

Template.rolodex.helpers({
  sortedCards: function() {
    return this.cards.sort(function(cardA, cardB) {
      return cardA.rank - cardB.rank;
    });
  }
});

Template.rolodex.rendered = function() {
  this.$(".cards-list").sortable({
    stop: function(e, ui) {
      // Get dragged HTML element and one immediately before / after
      var el = ui.item.get(0)
      var before = ui.item.prev().get(0)
      var after = ui.item.next().get(0)

      // Calculate new rank based on ranks of items before / after
      var newRank;
      if(!before) {
        // First position => set rank to be less than card immediately after
        newRank = Blaze.getData(after).rank - 1;
      } else if(!after) {
        // Last position => set rank to be more than card immediately after
        newRank = Blaze.getData(before).rank + 1;
      } else {
        // Average before and after
        newRank = (Blaze.getData(after).rank + 
                   Blaze.getData(before).rank) / 2;
      }

      // Meteor method that updates an attribute for a single card in a
      // Rolodex based on IDs for the Rolodex and Card
      Meteor.call('cards/update',
                  Blaze.getData(ui.item.parent().get(0))._id, // Rolodex ID
                  Blaze.getData(el).id, // Card ID
                  {rank: newRank});
    }
  });
};

The problem I'm running into is that after sorting a card into its expected position, the DOM is then updated with the card in a new, wrong position. The server has the correct rankings stored though, and refreshing the page causes the card to be listed in its correct position (at least until another sort is attempted).

My best guess as to what is happening is that Meteor's templating system doesn't seem to understand that the JQuery-UI has moved the DOM elements around and is reactively updating my template in the wrong order.

For example, suppose my cards are: A, B, C. I move C such that we now have C, A, B. JQuery-UI updates the DOM accordingly and fires an event which results in C's rank being changed to be less than A's.

However, Meteor doesn't know that the DOM has already been altered by JQuery-UI. It does, however, see the change in rank to C and reactively updates the order of the list based on prior assumptions about what the list order was. In this case, we end up with B, C, A.

Any suggestions as to what I'm doing wrong here?

like image 841
Andrew F Avatar asked Dec 04 '22 05:12

Andrew F


2 Answers

Meteor/Blaze uses an _id attribute to identify data objects and link them to DOM elements. This applies not only to arrays of documents returned by a Collection cursor, but to any array of objects. So in the above issue, the problem was that I used an id value to identify each card rather than _id. Switching id to _id fixes the issue and allows Blaze to properly update the DOM, even if the DOM has previously been modified by JQuery-UI's sortable plugin.

like image 134
Andrew F Avatar answered Dec 09 '22 14:12

Andrew F


The Meteor reactivity force you to choose who is in charge of DOM updates.

While it is ok to let Blaze render the DOM and then manipulate it with a third party library (usually a jQuery plugin invoked in a .rendered() method), you are now in a condition in which Blaze doesn't know what happened to your DOM, so every subsequent reactive update could not be consistent.

This is the reason why, for interactive and reactive interface elements, we need a new class of plugins/components/packages Meteor-aware (or better reactive-aware). See for example the difficulties of porting datatables.net to Meteor versus the Meteoric reactive-tables.

All that said, my hack to overcome this problem is to write some reactive code that takes care of detroying and rebuilding the plugin whenever the DB gets updated. This way, you restore the original DOM which Blaze is aware of, let him update it and then reinvoke your jQuery plugin. This is a far (very far) from optimal solution, but saved my day a couple of times.

For .sortable() maybe the best solution is to disable reactivity with option {reactive: false} in the Collection.find() so that when you update the attribute of a card with Meteor.call no redraw takes place but your interface is already consistent.

like image 32
physiocoder Avatar answered Dec 09 '22 15:12

physiocoder