Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meteor sorting oddity... #287

Closed
Siyfion opened this issue Feb 20, 2015 · 19 comments
Closed

Meteor sorting oddity... #287

Siyfion opened this issue Feb 20, 2015 · 19 comments

Comments

@Siyfion
Copy link

Siyfion commented Feb 20, 2015

So I have the following code in my template and corresponding javascript. My issue is that it seems that after moving a productField to a new position in the list, the update is sent to the server, which causes the DOM to reload, then Sortable "remembers" the change that was made and almost re-applies it. Causing the ordering on screen to be wrong. ie. the productFields aren't in index order.

Is there a way I can "reset" the Sortable internal state back to "unsorted"?

Or is there a better way to achieve this!?

...
          <ul class="list-group" id="selected-fields">
            {{#each productFieldDefinitions}}
              <li class="list-group-item">
                {{displayName}}
                <span class="badge">
                  <i class="fa fa-bars drag-handle"></i>
                </span>
              </li>
            {{/each}}
          </ul>
...
  var el = document.getElementById('selected-fields');
  Sortable.create(el, {
    group: {
      name: "fields",
      pull: false,
      put: true
    },
    sort: true,
    handle: '.drag-handle',
    draggable: '.list-group-item',
    onUpdate: function (event) {
      var productField = Blaze.getData(event.item);
      var orderedFields = _.sortBy(instance.data.category.productFields, 'index');

      // Remove the field from it's old position
      orderedFields.splice(event.oldIndex, 1);

      // Do we now need to shift the new index?
      var newIndex = event.newIndex;
      if (event.oldIndex < event.newIndex) {
        newIndex--;
      }

      // Insert the field into it's new position
      orderedFields.splice(newIndex, 0, productField);

      // Re-index the array
      _.each(orderedFields, function (field, index) {
        field.index = index;
      });

      // Save the update to the server
      ProductCategories.update({
        _id: instance.data.category._id
      }, {
        $set: { productFields: instance.data.category.productFields } 
      }, function (error, result) {
        if (!!error) {
          console.log(error);
          return;
        }
      });
    }
  });

Template.categoryEditor.helpers({
  productFieldDefinitions: function () {
    return _.sortBy(Template.instance().data.category.productFields, 'index') || [];
  }
});
@hamoid
Copy link

hamoid commented Mar 27, 2015

I'm having the same issue. One thing to add is, the data is correctly saved to the database. If I reload the page, then items are shown sorted as expected.

The issue is that the sorting seems to be applied twice: once on the client side by the sorting library, and once on the server side (the data is saved sorted, which triggers a reactive change on the client side).

@hamoid
Copy link

hamoid commented Mar 27, 2015

One more hack trying to make meteor work...

var originalOrder;
sortableMaterial = Sortable.create(el, {
  onStart: function() {
    originalOrder = sortableMaterial.toArray();
  },
  onSort: function(e) {
    Meteor.call('objectArraySort', id, 'material', sortableMaterial.toArray());
    sortableMaterial.sort(originalOrder);
  }
});

When you start dragging, save the order. When you end dragging, reset the order (and let the reactivity 'sort' things out). It shortly shows the incorrect state, which is ugly, but it works.
I hope someone shows a better way to do this.

@hamoid
Copy link

hamoid commented Mar 27, 2015

I managed to avoid the blinking by creating a helper that gets executed when the data changes, and only in that moment is .sort(originalOrder) called. Still feels very much like a hack, but it works smoothly.

@filipstachura
Copy link

I've experienced same problem.

@dhampik
Copy link

dhampik commented Apr 10, 2015

Same issue for me. I have a document in mongo with nested array of items and I want to sort them with Sortable, but after item is dropped the order becomes wrong :-( Unfortunately @hamoid's apporach does not work for me, at least I wasn't able to make it work.
@RubaXa any ideas how to fix that at least temporary?

@dhampik
Copy link

dhampik commented Apr 10, 2015

It looks like to make it work we should have an option which will prevent the final insertion of a node to another place in the DOM and will just trigger reactive stuff in meteor, which will change the order of items by himself...

@dhampik
Copy link

dhampik commented Apr 10, 2015

I just noticed that @hamoid's approach works if I have unique data-ids for the items, but unfortunately, in my case there could be no ids, cause that is just an array

@hamoid
Copy link

hamoid commented Apr 10, 2015 via email

@filipstachura
Copy link

Guys, I've tried today few different ways to make it work with helper returning an array (without using stuff in rendered function). Nothing worked.

Will try again within few days. Please let me know if you find any working solution.

@dhampik
Copy link

dhampik commented Apr 13, 2015

@hamoid yes, index as data-id works, too bad that spacebars do not support index out of the box.
Could you please give a snippet of code or provide more details of how did you resolve flickering issue, as you wrote here:

I managed to avoid the blinking by creating a helper that gets executed when the data changes, and only in that moment is .sort(originalOrder) called. Still feels very much like a hack, but it works smoothly.

@dhampik
Copy link

dhampik commented Apr 13, 2015

@hamoid do you mean do it like that?

            Meteor.call('setSortedItems', id, items, function () {
                sortable.sort(originalOrder);
            });

So that sortable.sort is called only when server-side update is done.
Looks like it resolves flickering of items, but also feels like a nasty hack...

@dhampik
Copy link

dhampik commented Apr 13, 2015

Forget my words, it does not order items properly anyway. Crap.
Just noticed that in normal mode in chrome dragging works, but in mobile device mode in chrome items re-ordered incorrectly after dragging. Totally weird.

@hamoid
Copy link

hamoid commented Apr 14, 2015

@dhampik This is what I'm doing now.

var originalOrder;
var sortableMaterial = false;

Template.objectMaterialDisplay.rendered = function() {
  var el = document.getElementsByClassName('materials');
  var id = this.data.obj._id;

  sortableMaterial = Sortable.create(el[0], {
    onStart: function() {
      originalOrder = sortableMaterial.toArray();
    },
    onSort: function(e) {
      Meteor.call('objectArraySort', id, 'material', sortableMaterial.toArray());
    }
  });

  Tracker.autorun(function() {
    // re-runs when the order changes
    var trigger = Objects.findOne(id, { fields: { material:1 } });   
    if(sortableMaterial && originalOrder) {
      // undo the local DOM sorting
      sortableMaterial.sort(originalOrder);
    }
  });

}

ps. gee, I must test sorting on mobile then!

@renanlecaro
Copy link

@hamoid You should reset originalOrder to null after using it.

if(sortableMaterial && originalOrder) {
      // undo the local DOM sorting
      sortableMaterial.sort(originalOrder);
originalOrder=null;
    }

Without this, at each update of the array content (say, one value of one field) the sort will be re-updated in some weird way.
Big thanks for this solution btw

@hamoid
Copy link

hamoid commented Sep 18, 2015

@renanlecaro You're welcome :) About your proposed change: what happens the second time you sort the list? originalOrder would be the null in that case, or? During my tests I did not notice any issues. But I'm no longer working on this...

@renanlecaro
Copy link

The onstart event recreates originalorder so it is not an issue ;) it seems
to work well in my project this way

Le ven. 18 sept. 2015 15:47, Abe Pazos notifications@github.com a écrit :

@renanlecaro https://github.com/renanlecaro You're welcome :) About
your proposed change: what happens the second time you sort the list?
originalOrder would be the null in that case, or? During my tests I did not
notice any issues. But I'm no longer working on this...


Reply to this email directly or view it on GitHub
#287 (comment).

@joaobarcia
Copy link

@dhampik

index as data-id works, too bad that spacebars do not support index out of the box

I haven't tried all of the proposed solutions, as I just landed on this thread. Just to tell you that the recently released meteor 1.2 supports @Index

@RubaXa
Copy link
Collaborator

RubaXa commented Feb 5, 2016

Dear all! Meteor is moved to the separate repository. If this issue is still actual, please create it there once again.

For your info: this project needs a maintainer.

@RubaXa RubaXa closed this as completed Feb 5, 2016
@ajschmaltz
Copy link

ajschmaltz commented Mar 15, 2017

The meteor repo wasn't able to help me. If anyone else has a method of using drag & drop to sort the array of a document, please tell me.

After several hours of trying to preserve the old order of sortable, I gave up. Then, I realized I could preserve the sort order of my helper ... if this helps anyone ... here you go ...

This is what my helper looked like. I have an array of media items with a weight property. Since I don't know how to sort this in the query, I'm doing it on the client side.

this.media.sort((a,b) => a.weight - b.weight);

Then I realized that if I saved the result I could return the result in the same order as the first time the helper was run...

let result = this.media.sort((a,b) => a.weight - b.weight);
if(oldResult && oldResult.length === result.length) {
    // the oldResult exists and they are the same length
    return oldResult;
}
oldResult = result;
return oldResult;

So, if I was reordering & saving anything this just returned the old result.
If I delete or add anything, this returns the new result -- messing stuff up again.

The last trick was to have the Sortable.create in an autorun statement that also checked the lengths of these. So this is in an autorun statement.

var newQuery = Businesses.findOne({});
if(self.sortable && (newQuery.media.length != oldResult.length)) {
    // an item has been added or removed.  Destroy and proceed
    self.sortable.destroy();
    delete self.sortable;
}
self.sortable = Sortable( // and here it is

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants