Angular view gets out of sync with model when dragging items from one list to another - jquery-ui

I have created a custom directive that allows me to connect multiple sortable lists via drag and drop using angular js and jquery ui. The way it should work is the following:
When drag starts, keep track of the initial position of the item in the array and the value of ng-model for that sortable
When the drag ends, if the item is received to a different list, keep track of the ng-model of that list and the target position of the element
Broadcast an event with that data so that the controller can change the positions of the items from one array to another
The problem is that once I move one item from one list to another, even though the items in the arrays go where they should, in the view some HTML elements disappear.
Here is the sortable directive:
app.directive('mySortable',function(){
return {
link:function(scope,el,attrs){
var options = {};
if(attrs.connectWith)
{
options.connectWith = attrs.connectWith;
}
el.sortable(options);
el.disableSelection();
el.on("sortstart", function(event, ui){
var from_index = angular.element(ui.item).scope()?angular.element(ui.item).scope().$index : 0;
var from_model = angular.element(ui.item.parent()).attr('ng-model');
ui.item.scope().sortableData = {from_index: from_index, from_model: from_model};
});
el.on("sortreceive", function(event, ui){
ui.item.scope().sortableData.to_index = el.children().index(ui.item);
ui.item.scope().sortableData.to_model = angular.element(el).attr('ng-model');
});
el.on( "sortdeactivate", function( event, ui ) {
var to_model = angular.element(el).attr('ng-model');
var from = angular.element(ui.item).scope()?angular.element(ui.item).scope().$index : 0;
var to = el.children().index(ui.item);
if(to>=0){
scope.$apply(function(){
if(from>=0){
scope.$emit('list-sorted', {from:from,to:to}, ui.item.scope());
}else{
scope.$emit('list-appended', {to:to, name:ui.item.text()});
ui.item.remove();
}
})
}
} );
}
}
})
And here is the controller logic that handles it's event:
$scope.$on('list-sorted', function(ev, val, task_scope){
var sd = task_scope.sortableData;
if(sd.to_model)
{
$timeout(function(){
$scope[sd.to_model].splice(sd.to_index, 0, $scope[sd.from_model].splice(sd.from_index, 1)[0]);
});
}
else
{
$timeout(function(){
$scope[sd.from_model].splice(val.to, 0, $scope[sd.from_model].splice(val.from, 1)[0]);
});
}
console.log($scope);
});
What's wrong?
Example JS Fiddle

It seems that the controller logic comports an error.
Is it fine like this:
var sd = item_scope.sortableData;
// If the item is supposed to be dropped to a different list, move it from one list to another
if(sd.to_model)
{
console.log("to a different list", val)
$timeout(function(){
$scope[sd.to_model].splice(val.to, 0, $scope[sd.from_model].splice(sd.from_index, 0));
});
}
else
{
console.log("to the same list")
$timeout(function(){
$scope[sd.from_model].splice(val.to, 0, $scope[sd.from_model].splice(val.from, 1)[0]);
});
}

Related

jQuery UI Sortable with React.js buggy

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>
)
}
});
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)))))
}
})
}
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.
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

Angularjs: jquery selectable

i have created a directive to handle selectable provided by Jquery
mydirectives.directive('uiSelectable', function ($parse) {
return {
link: function (scope, element, attrs, ctrl) {
element.selectable({
stop: function (evt, ui) {
var collection = scope.$eval(attrs.docArray)
var selected = element.find('div.parent.ui-selected').map(function () {
var idx = $(this).index();
return { document: collection[idx] }
}).get();
scope.selectedItems = selected;
scope.$apply()
}
});
}
}
});
to use in html
<div class="margin-top-20px" ui-selectable doc-array="documents">
where documents is an array that get returned by server in ajax response.
its working fine i can select multiple items or single item
Issue: i want to clear selection on close button
http://plnkr.co/edit/3cSef9h7MeYSM0cgYUIX?p=preview
i can write jquery in controller to remove .ui-selected class but its not recommended approach
can some one guide me whats the best practice to achieve these type of issue
Update:
i fixed the issue by broadcasting event on cancel and listening it on directive
$scope.clearSelection=function() {
$scope.selectedItems = [];
$timeout(function () {
$rootScope.$broadcast('clearselection', '');
}, 100);
}
and in directive
scope.$on('clearselection', function (event, document) {
element.find('.ui-selected').removeClass('ui-selected')
});
is this the right way of doing it or what is the best practice to solve the issue.
http://plnkr.co/edit/3cSef9h7MeYSM0cgYUIX?p=preview

Getting Property Values of Dropped Object in JQuery UI

I am (working with one of my students) using jqueryui getting the properties of a dropped object. The object that is being dropped is an image. All images are set to draggable through a single jquery call to draggable.
The challenge here is really getting any of the properties of the object being dropped on the drop target. the drop event handler works fine (I can easily alert on it) - but have not been able to get any of the properties of the objects being dropped.
This code is also available at http://jsfiddle.net/reaglin/FUvT8/4/
Note - the real action occurs when
(1) draggable() is called to make objects droppable
(2) The image is created and added to the document body
(3) The handleDropEvent is called.
This is a good example for working with playing cards - but this example uses Dr. Who characters.
$ (init);
function image(id, image1) {
this.id = id;
this.image1 = image1;
}
$('#deal').click(function () {dealAll(
dealCard(randomCard()));
});
$(function() {
$( "#draggable" ).draggable({ containment: "#left"});
});
function init() {
$('.drop').droppable( {
drop: handleDropEvent
} );
$("img").draggable();
}
// global variables
var cardsInDeck = new Array();
var numberOfCardsInDeck = 15;
cardsInDeck[0] = "Ace";
cardsInDeck[1] = "Grace";
cardsInDeck[2] = "Susan";
cardsInDeck[3] = "Ian";
cardsInDeck[4] = "Barbara";
cardsInDeck[5] = "Brigadier";
cardsInDeck[6] = "Romana I";
cardsInDeck[7] = "K9";
cardsInDeck[8] = "Tegan";
cardsInDeck[9] = "Jamie";
cardsInDeck[10] = "Sarah Jane";
cardsInDeck[11] = "Jo";
cardsInDeck[12] = "Romana II";
cardsInDeck[13] = "Yates";
cardsInDeck[14] = "Leela";
var cardsDealt = new Array();
// deal 5 cards at once - works
function dealAll(){
var z=0;
for (z=0;z<5;z++) {
cardsDealt[z] = new Image(z,dealCard(randomCard()));
}
}
//deal cards - works
function dealCard(i) {
if (numberOfCardsInDeck == 0) return false;
var $img = new Image();
$img.src = "http://debsiepalmer.com/images/companions/" + cardsInDeck[i] + ".jpg";
// Here I set the ID of the object
$img.id=cardsInDeck[i];
$img.class='drag';
$("img").draggable();
document.body.appendChild($img);
removeCard(i);
return $img;
}
// deal randomly - works
function randomCard() {
return Math.floor(Math.random() * numberOfCardsInDeck);
}
// remove spent cards from pool -works
function removeCard(c)
{
for (j=c; j <= numberOfCardsInDeck - 2; j++)
{
cardsInDeck[j] = cardsInDeck[j+1];
}
numberOfCardsInDeck--;
numberOfCardsInDeck--;
numberOfCardsInDeck--;
}
// this is what to do when card drops in tardis
function handleDropEvent( event, ui ) {
// Here I want the id of the dropped object
}
Researching a bit more on the documentation for jqueryui - provides these answers;
First when the image is declared draggable - this needs to be done after it is added to the document.body - this is because jquery selector uses the DOM to create the list of objects - so the lines
$("img").draggable();
document.body.appendChild($img);
would be changed to
document.body.appendChild($img);
$("img").draggable();
also in this example all images are declared to be draggable, we can be more specific however and just make the appended image to the document draggable.
document.body.appendChild($img);
$('#'+$img.id).draggable();
Once the image is made draggable - the properties of the object passed to the handleDropEvent are event and ui - the draggable property of the ui object is a jquery object which does have access to the attributes of the object it contains;
function handleDropEvent( event, ui ) {
alert(ui.draggable.attr("id"));
}

jQuery drop function for appended elements

When I drag an element to a droppable area, it sends a socket to the server. Whenever I append a new element to the page, the drop: function() {} does not trigger when the appended element is dropped, it only works once the page has been refreshed, although the drag function works.
question
Is there a way to bind the drop function to the appending elements?
code
var UI = {
droppedArea: $('.dropArea'),
initialArea: $('#init-area')
};
UI.droppedArea.droppable({
drop: function(event, ui) {
var dropped = ui.draggable;
var droppedOn = $(this),
/* Rest of the code */
socket.emit('move', data);
}
});
// Element appended like this
function newItem(data) {
var html = '<li>'+data.content+'</li>';
UI.initialArea.append(html);
}
// From the server
socket.on('new', function(data) {
newItem(data);
});
Figured out why.
Under the drop: function(), I had an accept: option, which value were DOM elements, with a type of class, stored in a variable.
Therefore, any appended elements were not referenced by the variable, so they were not being captured by the function
var UI = {
var item = $('.the_item')
}
drop: function() {
accept: UI.item,
}

Knockout bindings are not working with jsTree

I'm trying to get jsTree (1.0-rc3) working with Knockout.js (2.2.1).
See example jsFiddle: http://jsfiddle.net/adeconsulting/qfr6A/
Note: I've included several JS resources in the Fiddle to match as close as possible my Visual Studio project, in case there's a conflict between libraries which might be causing this problem.
Run the Fiddle and navigate through the jsTree, it's a list of servers by their physical location and type. It helps to have Firebug's console open so you can see the ajax calls and responses. When you click a leaf node, an ajax call is made to retrieve the server details and display a form whose values use Knockout bindings. I hide the form when a non-leaf node is selected.
It works the first time you click a leaf node. After that, Knockout does not update the form for leaf node clicks. However, if you happen to click the Edit button, then all of a sudden the most recent server details ARE displayed.
I'm thinking that there's a conflict between jsTree and Knockout bindings, but don't know where to start troubleshooting what that might be.
Since stackoverflow apparently requires at least one code block, here's the JavaScript portion of the Fiddle:
// Global vars:
var prevJsTreeNodeId = null;
var serverModelBindingsApplied = false;
var serverLoadInProgress = false;
/*
* The knockout.js view model
*/
var ServerViewModel = function () {
// Data
var self = this;
self.IsReadOnly = ko.observable(true); // the form's input mode
self.btnEditSave = ko.observable("Edit"); // the Edit/Save button text
self.Server = ko.observable({}); // the Server object
// Operations
self.setEditable = function () {
self.IsReadOnly(false);
self.btnEditSave("Save");
};
self.setReadOnly = function () {
self.IsReadOnly(true);
self.btnEditSave("Edit");
};
self.doEditSave = function () {
var flag = self.IsReadOnly();
if (flag) {
// switch to Edit mode
self.setEditable();
}
else {
// switch back to readOnly
self.setReadOnly();
}
};
// use ajax to update the knockout.js view model's Server object for the specified server name
self.load = function (serverName) {
if (!serverLoadInProgress) {
serverLoadInProgress = true;
// use ajax to retrieve the server's details
var data = {
json: JSON.stringify({
ServerName: serverName,
PrimaryIP: "1.2.3.4",
BrandDesc: "Dell",
OSDesc: "Windows 2003 Server",
Location: "xyz"
}),
delay: 1
};
$.ajax({
url:"/echo/json/",
data:data,
type:"POST",
success:function(response)
{
console.log(response);
window.ServerViewModelInstance.Server = ko.mapping.fromJS(response);
// apply bindings the first time we retrieve a Server object
if (!serverModelBindingsApplied) {
ko.applyBindings(window.ServerViewModelInstance,
document.getElementById('servercontent'));
serverModelBindingsApplied = true;
}
else {
// hmmm... updating the view model's .Server property doesn't trigger the
// form to be updated, yet if we click the Edit button, the new values
// suddenly appear, so try emulating that here...
self.setReadOnly();
}
}
});
serverLoadInProgress = false;
}
};
}; // ServerViewModel
/*
* document load
*/
$(function () {
// configure the jsTree
$("#divtree")
.jstree({
"themes": { "theme": "default", "dots": true, "icons": true },
"plugins": ["themes", "html_data", "ui", "types"],
"types": {
"type_attr": "tag", // the attribute which contains the type name
"max_depth": -2, // disable depth check
"max_children": -2, // disable max children check
"valid_children": ["root"],
"types": {
"root": {
"valid_children": ["level1"]
},
"level1": {
"valid_children": ["level2"],
"start_drag": false,
"move_node": false,
"delete_node": false,
"remove": false
},
"level2": {
"valid_children": ["leaf"],
// use the theme icon for the level2 nodes
"start_drag": false,
"move_node": false,
"delete_node": false,
"remove": false
},
"leaf": {
"valid_children": "none"
}
}
}
});
// register to receive notifications from #divtree when a jsTree node is selected
$("#divtree").bind("select_node.jstree", function (event, data) {
// data.rslt.obj is the jquery extended node that was clicked
var key = data.rslt.obj.attr("key");
var id = data.rslt.obj.attr("id");
if (id == prevJsTreeNodeId) {
// user clicked the same node, nothing to do
return;
}
prevJsTreeNodeId = id;
// when a jsTree node is selected, reset the knockout.js view model to read only
window.ServerViewModelInstance.setReadOnly();
var idx = key.indexOf("Server");
if (idx === 0) { // "Server|servername"
// show the "servercontent" div
$("#servercontent").show();
// display the server details
var serverName = key.substr(idx + 7, key.length);
window.ServerViewModelInstance.load(serverName);
}
else {
// hide the "servercontent" div
$("#servercontent").hide();
}
});
// hide the "servercontent" div
$("#servercontent").hide();
// instantiate the knockout.js view model
window.ServerViewModelInstance = new ServerViewModel();
}); // document ready
// initialization timer routine to select the main jsTree node
setTimeout(function () {
// open the root node
$.jstree._reference("#divtree").open_node("#root");
}, 500);
Sorry for my bad formatting below - this editor is not my friend... :-/
If I understand you right, the detail panel for a clicked tree node isn't updated with the correct data - right?
Try to do the following:
change:
window.ServerViewModelInstance.Server = ko.mapping.fromJS(response);
to: window.ServerViewModelInstance.Server(response);
(e.g. not overwriting the initial ko.observable which you only binds once, instead updating its values)
and in view where you bind to the observables..
for instance instead of: ... "value: Server.ServerName, ...
change it to: ... "value: Server().ServerName, ...
(e.g executing the function before accessing the property)
It works and updates the form when clicking on a new server name node in the tree (tried in firefox)
A copy of your example with the modified code can be found at: http://jsfiddle.net/RZ92g/2/

Resources