-
Notifications
You must be signed in to change notification settings - Fork 438
Sortable messes up ng-repeat comments (ng1.2) #48
Conversation
Ok. I think I have it working. The basic idea is we don't allow Here is my current /*
jQuery UI Sortable plugin wrapper
@param [ui-sortable] {object} Options to pass to $.fn.sortable() merged onto ui.config
*/
angular.module('ui.sortable', [])
.value('uiSortableConfig',{})
.directive('uiSortable', [
'uiSortableConfig', '$timeout', '$log',
function(uiSortableConfig, $timeout, log) {
return {
require: '?ngModel',
link: function(scope, element, attrs, ngModel) {
var savedNodes;
function combineCallbacks(first,second){
if( second && (typeof second === "function") ){
return function(e,ui){
first(e,ui);
second(e,ui);
};
}
return first;
}
var opts = {};
var callbacks = {
receive: null,
remove:null,
start:null,
stop:null,
update:null
};
angular.extend(opts, uiSortableConfig);
if (ngModel) {
// When we add or remove elements, we need the sortable to 'refresh'
scope.$watch(attrs.ngModel+'.length', function() {
// Timeout to let ng-repeat modify the DOM
$timeout(function() {
element.sortable( "refresh" );
// We need to make a copy of the current element's contents so
// we can restore it after sortable has messed it up
savedNodes = element.contents();
});
});
callbacks.start = function(e, ui) {
// Save position of dragged item
ui.item.sortable = { index: ui.item.index() };
};
callbacks.update = function(e, ui) {
// Fetch saved and current position of dropped element
var end, start;
start = ui.item.sortable.index;
end = ui.item.index();
// Cancel the sort (let ng-repeat do the sort for us)
element.sortable('cancel');
// Put the nodes back exactly the way they started (this is very
// important because ng-repeat uses comment elements to delineate
// the start and stop of repeat sections and sortable doesn't
// respect their order (even if we cancel, the order of the
// comments are still messed up).
savedNodes.detach().appendTo(element);
// Reorder array and apply change to scope
ngModel.$modelValue.splice(end, 0, ngModel.$modelValue.splice(start, 1)[0]);
scope.$digest();
};
angular.forEach(callbacks, function(value, key){
opts[key] = combineCallbacks(value, opts[key]);
});
} else {
log.info('ui.sortable: ngModel not provided!', element);
}
// Create sortable
element.sortable(opts);
}
};
}
]); Note: this isn't a pull request because I removed some existing What does everyone think? |
Just in case anyone else is looking at this code, I found a small bug that needed fixing. I didn't know this, but // Reorder array and apply change to scope
ngModel.$modelValue.splice(end, 0, ngModel.$modelValue.splice(start, 1)[0]);
scope.$digest(); to // Reorder array and apply change to scope
scope.$apply(function() {
ngModel.$modelValue.splice(end, 0, ngModel.$modelValue.splice(start, 1)[0]);
}); |
Out of curiosity - I'm an angular newbie, and still wrapping my head around this stuff - would this issue possibly result in the order of the items in the model getting out of sync with the order of the items in the UI? I'm rewriting the
In theory, then, the steps in my UI should always have ascending numbers, but I keep getting results that look like this: |
Quite possibly. I can see two possible reasons. One might be that the locations in the DOM is out of order with the model. The other is that you might need to wrap your model changes in a |
I don't think it's the second, as (due to some troubleshooting) I'm wrapping the whole call in I don't know enough about angular at this point to comment intelligently on your proposed solution, but I'm happy to test out your code when you get it into shape. |
It should be in shape now. Im using it in my app.
On Fri, Nov 8, 2013 at 10:42 AM, Ken Smith [email protected] wrote:
|
Just trying it out now . I added back in the bit that watched for callbacks, and once I did that, it seems to have fixed my problem, and otherwise (so far) seems to be working just fine for me. For other folks looking here, this is the version that I'm using (with the various fixes referenced above included):
Many thanks, Jeremy. |
Any time. If this continues to work for people, we might want to adopt it
On Fri, Nov 8, 2013 at 12:18 PM, Ken Smith [email protected] wrote:
|
So it turns out that angular messes with the DOM way more often than I thought, and ng-repeat keeps internal pointers to the actual DOM objects, as opposed to reinspecting the DOM when it needs to do it things. The result is I needed to snapshot the DOM on start as opposed to model change. The following patch fixed a couple intermittent bugs: // Timeout to let ng-repeat modify the DOM
$timeout(function() {
element.sortable( "refresh" );
- // We need to make a copy of the current element's contents so
- // we can restore it after sortable has messed it up
- savedNodes = element.contents();
});
});
callbacks.start = function(e, ui) {
// Save position of dragged item
ui.item.sortable = { index: ui.item.index() };
+
+ // We need to make a copy of the current element's contents so
+ // we can restore it after sortable has messed it up
+ savedNodes = element.contents().not(
+ //Don't inlcude the placeholder
+ element.find('.ui-sortable-placeholder'));
}; |
I just ran in the exact same problem today, it seems like that your patch fixed. When I added new items (when the list was messed up), it would add the in the middle of the list instead at the end. |
I faced the same problem, but had connected lists, thus your solution was not enough for me, so I expended it to work for connected list as well. Here is the result:
I also made it work with custom placeholder (as with custom placeholders the reloading of all items caused the placeholder to be visible after sort). |
Please, since the issue tracker is not a code repo, create a fork and refer to commits in your posts. Moreover if this is an issue related to angular1.2 only, please update the title to include "(ng1.2)". |
I'm doing that right now. I apologize, the trend of posting code was started by me. I didn't want to make it a pull request because it wasn't a patch as much as it was a rewrite, but I didn't think to create a fork and then refer to commits in my posts. Now that I think about it that is of course the right thing to do. I wasn't sure it was an angular 1.2 thing only, but based on the comments it appears that is true, so I'm going to update the title as well. Again, my apologies for not following github best practices. |
Ok. I've moved all of my code into a fork (https://github.com/CyborgMaster/ui-sortable). And merged in the changes from everyone in this thread. What do you think about it, is this something we should think about merging into the main repo? The only disclaimer I would make is that I didn't have a good test bench to verify drag and drop between lists. I'm pretty sure that part works, but if it doesn't feel free to submit patches (@filipkis). Note to others in this thread: if you want to use my code, use the fork for now, until we figure out how to get this code merged into the main repo. |
…uld be working now).
As I've had 4 or 5 people contact me saying this fixes their problems, I'm changing this into a pull request. All missing features have been re-implemented (configuration using the directive, updatable callbacks, drag and drop between lists), and have been thoroughly tested. If no one objects, I move this be merged into master. |
+1 from me. Thanks. |
You're welcome. |
Good work! Thanks |
… provided in the options
Perfect! Thank you! |
+1 from me, fixed my issues with multiple connected lists |
Awesome! @thgreasi says he's working on some tests, after he's done with that we can hopefully merge this into master and let everyone start using the main repo instead of the fork. |
I have found one more bug in the code I posted, that got integrated in this version and also implemented a way to cancel the sorting for myself, so I can contribute these changes. However, I won't be able to do this until earliest tomorrow. |
Sweet. If you could do it on an issue/pull-request in my fork that would be the most helpful. |
This approach appears to fail when combined with ng-repeat's orderBy filter. Reordering the $modelValue array doesn't work since the array order isn't what's important. I'm looking into possible solutions or workarounds, but nothing so far. |
That is a very good point. It wouldn't work. I don't think that is in any way unique to my implementation, but rather a limit to the technique used by this library since the beginning. If you come up with any clever ideas let me know and we can work them in. I can't think of anything simple off of the top of my head. |
You're right, it doesn't look like it's unique to your implementation. The more I dig into this, the more I realize that my previous approach was working in spite of the technique used in this library, rather than because of it. However, the old implementation results in the DOM being ordered properly (until the comments get messed up and the ng-repeat breaks), whereas with your new implementation, the DOM ends up unchanged. So, with the old implementation I was able to grab the array of DOM elements after being sorted (in an update callback) and use the index of each to reassign my positioning value. Something like this:
Not the cleanest solution, but it was working until I ran into the same bugs that started this issue (i.e. the comments getting moved around and things getting out of sync). However, since your new implementation reverts the DOM in the update function, my approach no longer works. |
I'll keep thinking about it too. I've had it in the back of my head since your first post and haven't gotten anywhere. You would have to either find a way to build a map back to the original array for ui-sortable to use, or somehow make it possible to go from the dom element back to the original index. Even then how would ui-sortable know what to modify about your elements to make them sort in the right order. Tough problem. Maybe there is a better approach, like allowing the use to edit whatever field you are orderBy'ing by. |
I may have an idea here. It occurred to me that my previous solution (with some slight tweaks) still works if I invert the combineCallback function so my user defined update() runs before the one defined in the library. Now I realize that's hacky as hell, and seems just as likely to cause issues as the way it is now. However, I wonder if expanding the callbacks system to provide before hooks would be worthwhile. Callbacks of the same name (update, stop, etc...) could continue to behave as they do now, as after hooks, providing backwards compatibility. Thoughts? |
I also wanted to let you guys know that with the latest version from the @CyborgMaster repository, and with Angular 1.2.1, I'm still having trouble on occasion with the ng-repeat comments getting messed up, and items getting sorted incorrectly as a result. I haven't done a dive into it yet to see if I can figure out why, but just wanted to let y'all know - before scurrying home for the weekend :-). If I have a chance, I'll see if I can poke around and find out what's going on. |
@CyborgMaster yesterday I merged some of my tests to the master repo. Please pull them to test your code.I also recommend testing against the commented ones (those work on ng1.0 but on ng1.2). Also (a late discussion catch up on filtering/ordering): |
@thgreasi, sorting the list in a controller and forgoing the orderBy filter is the solution I ended up going with. However, I'm only working with a very small list, so the performance gains are negligible. In the end, I think it just feels wrong to provide a sorting library that doesn't function within the structure of the built in angular ordering filters. |
I'm closing this issue, as I just opened #61 as a request for collaborator reviews. |
…ular 1.2 ng-repeat comments. #48
…t's class, not the default ui-sortable-placeholder class. #48
Just a note for people who have found this thread trying to work out interaction issues between |
The current implementation restores the DOM elements to the state before the drag and just changes the model. So ngRepeat never knows that the DOM changed (since this is before any digest). Can you provide a proof of concept with track by? |
The note was regarding other sortables (including the unmodified jQuery sortable). ... I realize this is not the most ideal venue for this sort of thing, but it is probably the most useful given this is the most detailed writeup on the issue (It also keeps coming up near the top of most Google queries related to it). e: So just to clarify: I am not suggesting you alter anything about |
thanks a lot, it works in new version |
The
ng-repeat
directive uses comments to mark the beginning and end of repeat sections (at least in the latest version:v1.2.0-rc3
). jQuery Sortable (and really all of jQuery for that matter) doesn't pay attention to comments and therefore clobbers them. Some clever work arounds are needed to get them to play together well.For example, an
ng-repeat
generated section of the DOM might look as follows:After dragging things around a couple of times, the DOM now looks like this:
Notice how all the
ngRepeat
comments have ended up at the bottom of the containing div. This causesng-repeat
to get very confused, throw javascript errors and crash when the model is modified. For example: If we now delete the "Group Start" filter, the following error is thrown:and all of the filters are removed from the DOM (even though the model is still intact with only the "Group Start" filter removed).
Note: the above experiment was performed even with calling
element.sortable('cancel')
inside of every sort (which is why the filters ended in the same order), but jQuery.sortable still messed up the comments.This can be seen either as a weakness of jQuery for not properly handling comment nodes as real nodes, or as a weakness of Angular's ng-repeat for depending on comment nodes for it's functionality (I personally don't think ng-repeat has any business using comment nodes), but either way, we need a work around.
I'm working on one now, will post it in a comment.