I'm experiencing a weird situation. I need to have two sortable lists that should interchange elements by drag-n-drop or Add/Remove events.
I created a directive that works well. Also the controller event do the right job. The problem begins when methods are combined (button Add + drag-n-drop + button Add again).
KA-BOOM!
I put together this plnkr: http://plnkr.co/edit/DumufP1kDdkz1INAXwmF?p=preview
Click on the elements before click the button action (Add/Remove).
Let me share some of the code of the directive just for fun but please visit the link to see the entire implementation. There is more information of how to reproduce the issue in the plnkr
.directive('sortableList', function ($log) {
return {
restrict: 'A',
scope: {
fromList: '=',
toList: '='
},
link: function (scope, elm, attrs) {
var callback = {
receive: function (event, ui) {
//-- Get the scope of the list-item
var scopeItem = ui.item.scope();
//-- Get new list index
var newIdx = ui.item.index();
//-- Find position in the list
var prevIdx = scope.fromList.indexOf(scopeItem.obj);
//-- Remove from source list
scope.fromList.splice(prevIdx, 1);
//-- Add to target list
if (newIdx >= scope.toList.length) {
scope.toList.push(scopeItem.obj);
}
else {
scope.toList.splice(newIdx, 0, scopeItem.obj);
}
//ui.item.removeClass('selectedSortListItem').addClass('sortListItem');
scope.$apply();
},
stop: function (event, ui) {
//$log.log(ui);
}
};
//-- Apply jquery ui sortable plug-in to element
elm.sortable({
handle: ".handle",
connectWith: '.sortColumnsConnect',
dropOnEmpty: true,
cancel: ".ui-state-disabled",
receive: callback.receive,
stop: callback.stop
}).disableSelection();
//-- Sniff for list changes
/*scope.$watch(attrs.sortableList, function (newVal) {
//-- Apply callback
//if (angular.isUndefined(newVal)) return;
elm.sortable('option', 'receive', callback.receive);
if (!angular.isUndefined(attrs.trackSorting) && Boolean(attrs.trackSorting)) {
elm.sortable('option', 'stop', callback.stop);
}
});*/
}
}
})
Help is appreciated.
I finally made it work. I have it all working in this plunker.
I thought it was related to the scope of both directives (custom + ng-repeat) but it turned out to be that I needed to leave ng-repeat do the whole job and never remove the ng-repeat comments, otherwise the angular directive will brake.
One thing though that my directive needs to take care is the $destroy even, as the directive itself is holding a reference of an object that better is removed later when the page leaves or something in order to avoid a memory leak situation.
Now, let's share some code here for fun.. again..
.directive('sortableList', function ($log, $parse,$timeout) {
return {
restrict: 'A',
scope: {
list: '='
},
link: function (scope, elm, attrs) {
/*
* We need to preserve a copy of ng-repeat comments
* in order to not brake the ng directive. Lets made a copy and
* and insert it back to the html list in the remove even.
*/
var comments = elm.contents().filter(function(){
return this.nodeType == 8;
});
var comment = undefined;
if (comments.length > 0){
comment = comments[0];
}
var callback = {
start: function(event, ui){
ui.item.sortable = {
received: false,
startIndex: ui.item.index()
};
},
receive: function (event, ui) {
ui.item.sortable.received = true;
},
update: function (event, ui) {
//$log.log(elm);
$log.log('update');
var scopeItem = ui.item.scope();
//-- Get new list index. Index in array not in html list
var newIdx = ui.item.index();
if (ui.item.sortable.received){
$log.log('received');
ui.sender.sortable('cancel');
ui.item.sortable.received = false;
//ui.item.sortable.doremove = true;
scope.$apply(function(){
//-- Add to target list
if (newIdx >= scope.list.length) {
scope.list.push(scopeItem.obj);
}
else {
$log.log(newIdx);
scope.list.splice(newIdx, 0, scopeItem.obj);
}
ui.item.removeClass('selectedSortListItem').addClass('sortListItem');
});
}
else {
//-- Sort list
if (ui.item.sortable.startIndex != newIdx){
$log.log('sort list');
scope.$apply(function(){
var idx = scope.list.indexOf(scopeItem.obj);
//-- end destroy
if (idx > -1){
scope.list.splice(idx, 1);
}
//-- Add to the new position
scope.list.splice(newIdx, 0, scopeItem.obj);
});
}
}
},
remove: function( event, ui ) {
var scopeItem = ui.item.scope();
/* Do the normal node removal */
//-- Seek
var idx = scope.list.indexOf(scopeItem.obj);
//-- end destroy
if (idx > -1){
scope.list.splice(idx, 1);
}
/*
* Insert back ng-repeat comments to the html list to avoid braking
* the angular directive.
*/
if (elm.children("li:not('.ui-state-disabled')").length === 0 && angular.isDefined(comment)){
$log.log('insert comment');
$log.log(comment);
elm.append(comment);
//$log.log(elm);
}
//$log.log('I have childrens: ' + elm.children("li:not('.ui-state-disabled')").length);
//$log.log('remove me please at:' + idx);
},
stop: function (event, ui) {
$log.log('stop');
}
};
scope.$watch('list.length', function() {
// Timeout to let ng-repeat modify the DOM
$timeout(function() {
$log.log('epa!');
//-- need to unselect those selected, otherwise Vishal will go like: Leo there is an error.. what? what? what?
elm.children("li.selectedSortListItem").toggleClass('selectedSortListItem').toggleClass('sortListItem');
elm.sortable('refresh');
});
});
//-- Apply jquery ui sortable plug-in to element
elm.sortable({
handle: ".handle",
connectWith: '.sortColumnsConnect',
dropOnEmpty: true,
cancel: ".ui-state-disabled",
helper: "clone",
start: callback.start,
receive: callback.receive,
update: callback.update,
stop: callback.stop,
remove: callback.remove
}).disableSelection();
}
}
})
Take a look at the plunker to understand how the directive is invoked and the purpose of it. I might have some stuff that forgot to remove after so many re-factories.. but seems like now is doing the right thing.. at least it's not braking like it was before.
Thanks.