jQuery UI Sortable with React.js buggy - jquery-ui

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

Related

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

Set Umbraco Property Editor Input to jQueryUI Datepicker

I'm close but still can't quite get this to work.
I have a new custom property editor that is loading correctly and is doing almost everything expected until I try to set the text field to be a jQuery UI element.
As soon as I add a directive in Angular for setting it to call the jQuery UI datepicker function, I get the following error suggesting it hasn't loaded the jQueryUI script library correctly:
TypeError: Object [object Object] has no method 'datepicker'
Trouble is, I can't see where I should be adding it as the logical places (to my mind, at least) seem to make no difference. Here is the code in full:
function MultipleDatePickerController($scope, assetsService) {
//tell the assetsService to load the markdown.editor libs from the markdown editors
//plugin folder
//assetsService
// .load([
// "http://code.jquery.com/ui/1.10.4/jquery-ui.min.js"
// ])
// .then(function () {
// //this function will execute when all dependencies have loaded
// });
//load the seperat css for the editor to avoid it blocking our js loading
assetsService.loadCss("/css/jquery-ui.custom.min.css");
if (!$scope.model.value) {
$scope.model.value = [];
}
//add any fields that there isn't values for
//if ($scope.model.config.min > 0) {
if ($scope.model.value.length > 0) {
for (var i = 0; i < $scope.model.value.length; i++) {
if ((i + 1) > $scope.model.value.length) {
$scope.model.value.push({ value: "" });
}
}
}
$scope.add = function () {
//if ($scope.model.config.max <= 0 || $scope.model.value.length < $scope.model.config.max) {
if ($scope.model.value.length <= 52) {
$scope.model.value.push({ value: "" });
}
};
$scope.remove = function (index) {
var remainder = [];
for (var x = 0; x < $scope.model.value.length; x++) {
if (x !== index) {
remainder.push($scope.model.value[x]);
}
}
$scope.model.value = remainder;
};
}
var datePicker = angular.module("umbraco").controller("AcuIT.MultidateController", MultipleDatePickerController);
datePicker.directive('jqdatepicker', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
$(function () {
element.datepicker({
dateFormat: 'dd/mm/yy',
onSelect: function (date) {
scope.$apply(function () {
ngModelCtrl.$setViewValue(date);
});
}
});
});
}
}
});
I faced the same problem when adapting a jQuery Date Range Picker for my Date Range Picker package for Umbraco 7. It's frustrating! The problem (I think) is that Angular's ng-model listens for "input" changes to trigger events and so doesn't pick up on a jQuery triggered event.
The way around it I found was to force the input event of the element you wish to update to fire manually, using jQuery's .trigger() event.
For example, the date picker I was using had this code for when a date was changed:
updateInputText: function () {
if (this.element.is('input')) {
this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format));
}
},
I just adapted it to force an input trigger by adding this.element.trigger('input') to the code block, so it now reads:
updateInputText: function () {
if (this.element.is('input')) {
this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format));
this.element.trigger('input');
}
},
This forces Angular to "see" the change and then ng-model is updated. There may well be a more elegant way (as I'm an Angular newbie), but I know this worked for me.
Got it. This is probably a bit of a hack, but it's simple and effective so it's a win nonetheless.
The assetsService call is the key, where I've put code into the deferred .then statement to call jQueryUI's datepicker on any item that has the "jqdp" CSS class:
//tell the assetsService to load the markdown.editor libs from the markdown editors
//plugin folder
assetsService
.load([
"/App_Plugins/Multidate/jquery-ui.min.js"
])
.then(function () {
//this function will execute when all dependencies have loaded
$('.jqdp').datepicker({ dateFormat: 'dd/mm/yy' });
});
I've then gone and added that class to my view:
<input type="text" jqdatepicker name="item_{{$index}}" ng-model="item.value" class="jqdp" id="dp-{{model.alias}}-{{$index}}" />
Finally, I've added a directive to ensure that dynamically-added items also display a datepicker:
datePicker.directive('jqdatepicker', function () {
return function (scope, element, attrs) {
scope.$watch("jqdatepicker", function () {
try{
$(element).datepicker({ dateFormat: 'dd/mm/yy' });
}
catch(e)
{}
});
};
});
As I said, this is possibly a bit hacky but it achieves the right result and seems like a simple solution.

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

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

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,
}

jQuery UI Autcomplete - hyperlink results

By default the jQuery U Autocomplete produces a list of results, upon clicking on a result it will populate the text field with the clicked result text.
I would like to change this behaviour, so that when clicking on a result it will take you to that result's page. To generate the hyperlink I can pass in the ID of the result.
I'm using PHP JSON to bring back the resultset:
$return_arr = array();
while ($row = mysql_fetch_array($fetch, MYSQL_ASSOC)) {
$row_array['id'] = $row['id'];
$row_array['value'] = $row['name'];
array_push($return_arr, $row_array);
}
echo json_encode($return_arr);
And here is my current jQuery:
$(function() {
$("#searchcompany").autocomplete( {
source: "companies.php",
minLength: 2
});
});
Think you need to hook into the select event and supply your own function.
See here for more information.
Supply a callback function to handle the select event as an init option.
$("#searchcompany").autocomplete( {
source: "companies.php",
minLength: 2,
select: function(event,ui) { //Do your code here...
event.preventDefault();
}
});
or Bind to the select event by type: autocompleteselect.
$( "#searchcompany" ).bind( "autocompleteselect", function(event, ui) {
...
});
and to change the matching items to include a hyperlink that can be clicked use the Open event :-
open: function(event, ui) { $( 'li.ui-menu-item a').each( function() {
var el = $(this);
el.attr('href', el.html());
}
); }
This will add an href="[item value]" to each <a> element.
Edit: The code below will allow you to use the open event to change the items to include a href so they show the link in the window and when clicked they will take you to the specified location :-
open: function(event, ui) {
$("ul.ui-autocomplete").unbind("click");
var data = $(this).data("autocomplete");
for(var i=0; i<=data.options.source.length-1;i++)
{
var s = data.options.source[i];
$("li.ui-menu-item a:contains(" + s.value + ")").attr("href", "directory/listing/" + s.id);
}
}
Using this also means that you don't need to use the select event.

Resources