Is it at all easy to use jQuery.sortable on ng-repeat elements in AngularJS?
It would be awesome if re-ordering the items automatically propagated that ordering back into the source array. I'm afraid the two systems would fight though. Is there a better way to do this?
Angular UI has a sortable directive,Click Here for Demo
Code located at ui-sortable, usage:
<ul ui-sortable ng-model="items" ui-sortable-update="sorted">
<li ng-repeat="item in items track by $index" id="{{$index}}">{{ item }}</li>
</ul>
$scope.sorted = (event, ui) => { console.log(ui.item[0].getAttribute('id')) }
I tried to do the same and came up with the following solution:
angular.directive("my:sortable", function(expression, compiledElement){
return function(linkElement){
var scope = this;
linkElement.sortable(
{
placeholder: "ui-state-highlight",
opacity: 0.8,
update: function(event, ui) {
var model = scope.$tryEval(expression);
var newModel = [];
var items = [];
linkElement.children().each(function() {
var item = $(this);
// get old item index
var oldIndex = item.attr("ng:repeat-index");
if(oldIndex) {
// new model in new order
newModel.push(model[oldIndex]);
// items in original order
items[oldIndex] = item;
// and remove
item.detach();
}
});
// restore original dom order, so angular does not get confused
linkElement.append.apply(linkElement,items);
// clear old list
model.length = 0;
// add elements in new order
model.push.apply(model, newModel);
// presto
scope.$eval();
// Notify event handler
var onSortExpression = linkElement.attr("my:onsort");
if(onSortExpression) {
scope.$tryEval(onSortExpression, linkElement);
}
}
});
};
});
Used like this:
<ol id="todoList" my:sortable="todos" my:onsort="onSort()">
It seems to work fairly well. The trick is to undo the DOM manipulation made by sortable before updating the model, otherwise angular gets desynchronized from the DOM.
Notification of the changes works via the my:onsort expression which can call the controller methods.
I created a JsFiddle based on the angular todo tutorial to shows how it works: http://jsfiddle.net/M8YnR/180/
This is how I am doing it with angular v0.10.6. Here is the jsfiddle
angular.directive("my:sortable", function(expression, compiledElement){
// add my:sortable-index to children so we know the index in the model
compiledElement.children().attr("my:sortable-index","{{$index}}");
return function(linkElement){
var scope = this;
linkElement.sortable({
placeholder: "placeholder",
opacity: 0.8,
axis: "y",
update: function(event, ui) {
// get model
var model = scope.$apply(expression);
// remember its length
var modelLength = model.length;
// rember html nodes
var items = [];
// loop through items in new order
linkElement.children().each(function(index) {
var item = $(this);
// get old item index
var oldIndex = parseInt(item.attr("my:sortable-index"), 10);
// add item to the end of model
model.push(model[oldIndex]);
if(item.attr("my:sortable-index")) {
// items in original order to restore dom
items[oldIndex] = item;
// and remove item from dom
item.detach();
}
});
model.splice(0, modelLength);
// restore original dom order, so angular does not get confused
linkElement.append.apply(linkElement,items);
// notify angular of the change
scope.$digest();
}
});
};
});
Here's my implementation of sortable Angular.js directive without jquery.ui :
https://github.com/schartier/angular-sortable
you can go for ng-sortable directive which is lightweight and it does not uses jquery. here is link ng-sortable drag and drop elements
Demo for ng-sortable
Related
I have an angular object(model) created in controller.
$scope.deletedres = [];
I am trying to append a new DOM to the html body along with the angular object(modal) as shown below.
$('body').append('<span>'+restaurant.name+' have been removed.</span><a class="btn-flat yellow-text" href="#"; ng-click="addRestaurant($scope.deletedres[$scope.deletedres.length-1])">Undo<a>');
When I view it with google chrome dev tools, it shows that $scope.deletedres as [object Object] and addRestaurant() function receive nothing.
Can anyone enlighten me on this issue?
Is there any other ways to reference/pass an angular modal to a newly created DOM?
The way you are adding the DOM is wrong. Add the html inside the scope of controller. Use ng-show to show or hide the dom. JQuery is not necessary.
Example
<span ng-show="restaurant.delete">{{restaurant.name}} have been removed.</span>
<a class="btn-flat yellow-text" href="#"; ng-click="restaurant.delete=false">Undo<a>
This is just an example you can improve on
When you use jQuery to add fragments of HTML there is no way for angular to parse it. Thats the reason your angular code inside the html is working.
You can use $compile service.
var html = '<span>{{restaurant.name}} have been removed.</span><a class="btn-flat yellow-text" href="#"; ng-click="addRestaurant(deletedres[deletedres.length-1])">Undo</a>';
var linkFn = $compile(html);
var content = linkFn(scope);
$('body').append(content);
Still as noted by Harish it's wrong. All manipulations with DOM must be done in directives. You can create directive that will be responsible for showing some message (or custom html template) on button click.
Dmitry Bezzubenkov is right. If you want to manipulate DOM with Angular, you should do that with your custom directive, rather than do that in your controller directly. And to do so, you may refer to $compile service. Here's the official document for that.
However, in your case, I believe what you actually want to do is remove the item from a list while enable the item to be recovered from deletion. In this sense, you may try this approach with Angular:
In your controller, create a array for original restaurant list and another for deleted restaurant list. (Let's say, $scope.res and $scope.deletedres)
Register a delete function and bind that to delete button with ng-click. In this function, you will remove the item from $scope.res and then push the item to $scope.deletedres
Register another undo function. Basically do the same thing as delete function but in reverse. That is, move a item from $scope.deletedres to $scope.res. Bind this item to UNDO text in your message box.
use ng-repeat to show your $scope.res list in the main container, and $scope.deletedres in the message box container.
Thanks to the 2-way data binding from Angular, now you can delete or undo the action by clicking to different item.
It would be something like this:
angular
.module('modelTest', [])
.controller('MainCtrl', function($scope) {
$scope.res = [
{id: 1, name: 'Restaurant1'},
{id: 2, name: 'Restaurant2'},
{id: 3, name: 'Restaurant3'}
];
$scope.deletedres = [];
$scope.delete = function(id) {
var item, obj, i, j;
for(i = 0, j = $scope.res.length; i < j; i++) {
obj = $scope.res[i];
if(obj.id === id) {
$scope.deletedres.push(obj);
$scope.res.splice(i, 1);
}
}
};
$scope.undo = function(id) {
var item, obj, i, j;
for(i = 0, j = $scope.deletedres.length; i < j; i++) {
obj = $scope.deletedres[i];
if(obj.id === id) {
$scope.res.push(obj);
$scope.deletedres.splice(i, 1);
}
}
}
});
Here's the sample code.
After trying out a number of methods, I have searched high and low for a good solution to a seemingly easy problem.
I have a sortable list of chapters and I'm using averaging to make jquery sortable work for a meteor application. The dragging and dropping sortable part is easy (using averaging), but applying the correct (whole number) order of the chapters has been tricky.
A common application would be chapter numbers applied to a table of contents.
What I've tried:
Rewriting the stop: function using a for loop that accounts for the different scenarios. Drag to front, drag to rear
This can get complex as the numbers of chapters increase. I felt it was too complex a solution.
Using jQuery to identify the order of the items and apply the numbers according to their sorted order in the browser. I think this will work, but I haven't been able to figure out what jquery functions to use and where after trying several out. Disclaimer: I'm new to Spacebars and haven't used much jquery.
chaptersList html:
<template name="chaptersList">
<div id="items">
{{#each publishedChapters}}
{{> chapterItem}}
{{/each}}
</div><!--items-->
</template>
chaptersList js:
Template.chaptersList.rendered = function() {
this.$('#items').sortable({
stop: function(e, ui) {
// get the dragged html element and the one before
// and after it
el = ui.item.get(0)
before = ui.item.prev().get(0)
after = ui.item.next().get(0)
if(!before) {
//if it was dragged into the first position grab the
// next element's data context and subtract one from the order
newOrder = 1;
} else if(!after) {
//if it was dragged into the last position grab the
// previous element's data context and add one to the order
newOrder = Blaze.getData(before).order + 1
}
else
//else take the average of the two orders of the previous
// and next elements
newOrder = (Blaze.getData(after).order +
Blaze.getData(before).order)/2
newOrder = Math.round(newOrder)
//update the Items' orders
Chapters.update({_id: Blaze.getData(el)._id}, {$set: {order: newOrder}})
}
})
}
Template.chaptersList.helpers({
publishedChapters: function() {
return Chapters.find({ published: true },
{ sort: {order: 1}
});
},
items: function() {
return Chapters.find({}, {
sort: {order: 1 }
})
}
});
chapterItem html:
<template name="chapterItem">
<div class="item">
<h3 class="headroom-10 chapter-title-small">
{{title}}
</h3>
<p class="chapter-text">{{#markdown}}{{chapterTease}}{{/markdown}}</p>
{{#if ownChapter}}
Edit
<span class="delete">
Delete
</span>
{{/if}}
</div>
</template>
Thank you for your valuable insight.
I went through this same issue and the trick to get it working cleanly in my case was to cancel the sortable action from inside the sort and let Blaze take over once the item was dropped. Otherwise the sortable ordering and Blaze ordering end up wrestling one another.
I save my new set of ordered items as a single batch, but that's not necessarily required.
Template.builder.rendered = function(){
var self = this;
this.$("ol.sortable").sortable({
axis: "y",
containment: "ol.sortable",
tolerance: "pointer",
update: function(event, ui){
var
items = $(this).find("li"),
picks = _.map(items, function(item){
var obj = Blaze.getData(item);
obj.position = $(item).index() + 1;
return obj;
});
self.$('ol.sortable').sortable('cancel');
Meteor.call(
"orderPicks",
team,
picks,
function(error, result){}
);
}
});
}
And on the server:
Meteor.methods({
orderPicks: function(team, picks){
Teams.update(team,
{ $set: {picks: picks} }
);
return true;
}
});
I believe I picked this up from another SO thread; I can't remember. In any case it works seamlessly for a modestly sized set of items.
HTML:
<template name="builder">
<ol class="builder sortable">
{{#each team.picks}}
<li>{{position}}. {{name}}</li>
{{/each}}
</ol>
</template>
This answer is a follow up to the conversation with #jeremy above, adapting a nested array approach to individual documents.
It is not currently working, but by posting it here, I hope to be able to gain insight, get it working and offer another solution.
Right now, the sorting order is not changing when an item is dragged and dropped. If you can see why, let me know or you can also edit the answer:
Add id to li:
chapterId: function() {
return Chapters.findOne(this._id)._id
}
HTML:
<li class="item headroom-20" id="{{chapterId}}">
Sortable JS:
Template.chaptersList.rendered = function(){
var self = this;
this.$("ol.items").sortable({
axis: "y",
containment: "ol.items",
tolerance: "pointer",
update: function(event, ui){
var items = $(".items").find("li");
var publishedChaptersCount = items.length;
var sortedIds = [];
for (var i = 0; i < publishedChaptersCount; i++) {
var obj = $(items[i]).attr('id');
sortedIds.push(obj);
}
self.$('ol.items').sortable('cancel');
Meteor.call(
"chapterOrderUpdate",
sortedIds,
function(error, result){}
);
}
});
}
Meteor Method:
Meteor.methods({
chapterOrderUpdate: function(sortedIds){
var publishedChaptersCount = sortedIds.length;
for (var i = 0; i < publishedChaptersCount; i++) {
var chapterId = sortedIds[i];
Chapters.update({_id: chapterId}, { $set: { order: i+1 } })
}
}
});
I've been using an implementation of this Drag and Drop with AngularJS and jQuery UI:
http://www.smartjava.org/examples/dnd/double.html
With AngularJS 1.0.8 it works flawlessly. With 1.2.11, it doesn't.
When using AngularJS 1.2 and dragging an item from the left list to the right one the model for the destination list updates correctly. However the DOM doesn't update correctly. Here is the directive that's being used from the example:
app.directive('dndBetweenList', function($parse) {
return function(scope, element, attrs) {
// contains the args for this component
var args = attrs.dndBetweenList.split(',');
// contains the args for the target
var targetArgs = $('#'+args[1]).attr('dnd-between-list').split(',');
// variables used for dnd
var toUpdate;
var target;
var startIndex = -1;
// watch the model, so we always know what element
// is at a specific position
scope.$watch(args[0], function(value) {
toUpdate = value;
},true);
// also watch for changes in the target list
scope.$watch(targetArgs[0], function(value) {
target = value;
},true);
// use jquery to make the element sortable (dnd). This is called
// when the element is rendered
$(element[0]).sortable({
items:'li',
start:function (event, ui) {
// on start we define where the item is dragged from
startIndex = ($(ui.item).index());
},
stop:function (event, ui) {
var newParent = ui.item[0].parentNode.id;
// on stop we determine the new index of the
// item and store it there
var newIndex = ($(ui.item).index());
var toMove = toUpdate[startIndex];
// we need to remove him from the configured model
toUpdate.splice(startIndex,1);
if (newParent == args[1]) {
// and add it to the linked list
target.splice(newIndex,0,toMove);
} else {
toUpdate.splice(newIndex,0,toMove);
}
// we move items in the array, if we want
// to trigger an update in angular use $apply()
// since we're outside angulars lifecycle
scope.$apply(targetArgs[0]);
scope.$apply(args[0]);
},
connectWith:'#'+args[1]
})
}
});
Does something need to be updated for this to work properly with Angular 1.2? I feel like it has something to do with the scope.$apply but am not sure.
I see this is an older question, but I recently ran into the exact same issue with the Drag and Drop example. I don’t know what has changed between angular 1.0.8 and 1.2, but it appears to be the digest cycle that causes problems with the DOM. scope.$apply will trigger a digest cycle, but scope.$apply in and of itself is not the issue. Anything that causes a cycle can cause the DOM t get out of sync with the model.
I was able to find a solution to the the problem using the ui.sortable directive. The specific branch that I used is here: https://github.com/angular-ui/ui-sortable/tree/angular1.2. I have not tested with other branches.
You can view a working example here:
http://plnkr.co/edit/atoDX2TqZT654dEicqeS?p=preview
Using the ui-sortable solution, the ‘dndBetweenList’ directive gets replaced with the ui-sortable directive. Then there are a few changes to make.
In the HTML
<div class="row">
<div class="span4 offset2">
<ul ui-sortable="sortableOptions" ng-model="source" id="sourceList" ng-class="{'minimalList':sourceEmpty()}" class="connector">
<li class="alert alert-danger nomargin" ng-repeat="item in source">{{item.value}}</li>
</ul>
</div>
<div class="span4">
<ul ui-sortable="sortableOptions" id="targetList" ng-model="model" ng-class="{'minimalList':sourceEmpty()}" class="connector">
<li class="alert alert-info nomargin" ng-repeat="item in model">{{item.value}}</li>
</ul>
</div>
</div>
Note the dnd-between-list directive is no longer needed and is replaced with the ui-sortable.
In the module inject the ui-sortable, and in the controller specify that sortable options. The sortable accepts the same options as the jquery sortable.
app.js
var app = angular.module('dnd', ['ui.sortable']);
ctrl-dnd.js
$scope.sortableOptions = {
connectWith: '.connector'
}
Only the additions to the controller are shown. Note that I added a .connector class on the ul. In the sortable I use .connector for the connectWith option.
I am new to Javascript and trying to use Gridster with Knockout. I have a database with items, and I use knockout foreach to bind them to a UL. The UL is styled with the Gridster library. Everything works great unless I try to add additional elements to the UL via the ObservableArray in the viewmodel.
Can anyone help me understand the scope and order of operations here? It feels like the Gridster library isn't doing its styling to the new widgets.
This jsfiddle shows a working demo of the issue. Notice when you double click on a widget, it creates a new one but doesn't place it in the grid. Instead, it just kind of hangs out behind.
Here is the HTML
<div class="gridster">
<ul data-bind="foreach: myData">
<li data-bind="attr:{
'data-row':datarow,
'data-col':datacol,
'data-sizex':datasizex,
'data-sizey':datasizey
},text:text, doubleClick: $parent.AddOne"></li>
</ul>
</div>
Here is the Javascript
//This is some widget data to start the process
var gridData = [ {text:'Widget #1', datarow:1, datacol:1, datasizex:1, datasizey:1},
{text:'Widget #2', datarow:2, datacol:1, datasizex:1, datasizey:1},
{text:'Widget #3', datarow:1, datacol:2, datasizex:1, datasizey:1},
{text:'Widget #4', datarow:2, datacol:2, datasizex:1, datasizey:1}];
// The viewmodel contains an observable array of widget data to be
// displayed on the gridster
var viewmodel = function () {
var self = this;
self.myData = ko.observableArray(gridData);
//AddOne adds an element to the observable array
// (called at runtime from doubleClick events)
self.AddOne = function () {
var self = this;
myViewModel.myData.push({
text: 'Widget Added After!',
datarow: 1,
datacol: 1,
datasizex: 1,
datasizey: 1
});
};
};
var myViewModel = new viewmodel();
ko.applyBindings(myViewModel);
$(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [140, 140]
});
Here is a full example in JSfiddle. Here, I have highlighted just the delete function
self.deleteOne = function (item) {
console.log(item);
var widget = $("#" + item.id);
console.log(widget);
var column = widget.attr("data-col");
if (column) {
console.log('Removing ');
// if this is commented out then the widgets won't re-arrange
self.gridster.remove_widget(widget, function(){
self.myData.remove(item);
console.log('Tiles: '+self.myData().length);
});
}
};
The work of removing the element from the observable array is done inside the remove_widget callback. See gridster's documentation. Consequently, the removeGridster hook executed before a widget is removed, does no longer need to do the actual remove_widget call.
Here's a working solution that I think is more in line with the MVVM pattern:
http://jsfiddle.net/Be4cf/4/
//This is some widget data to start the process
var gridData = [
{id: "1", text:'Widget #1', datarow:1, datacol:1, datasizex:1, datasizey:1},
{id: "2", text:'Widget #2', datarow:1, datacol:2, datasizex:2, datasizey:1},
{id: "3", text:'Widget #3', datarow:1, datacol:4, datasizex:1, datasizey:1},
{id: "4", text:'Widget #4', datarow:2, datacol:1, datasizex:1, datasizey:2}];
// The viewmodel contains an observable array of widget data to be
// displayed on the gridster
var viewmodel = function () {
var self = this;
self.myData = ko.observableArray(gridData);
self.nextId = 5;
self.gridster = undefined;
// AddOne adds an element to the observable array.
// Notice how I'm not adding the element to gridster by hand here. This means
// that whatever the source of the add is (click, callback, web sockets event),
// the element will be added to gridster.
self.addOne = function () {
myViewModel.myData.push({
text: 'Widget Added After!',
datarow: 1,
datacol: 1,
datasizex: 1,
datasizey: 1,
id: self.nextId++
});
};
// Called after the render of the initial list.
// Gridster will add the existing widgets to its internal model.
self.initializeGridster = function() {
self.gridster = $(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [140, 140]
}).data('gridster');
};
// Called after the render of the new element.
self.addGridster = function(data, object) {
// This bypasses the add if gridster has not been initialized.
if (self.gridster) {
var $item = $(data[0].parentNode);
// The first afterRender event is fired 2 times. It appears to be a bug in knockout.
// I'm pretty new to knockout myself, so it might be a feature too! :)
// This skips the second call from the initial fired event.
if (!$item.hasClass("gs-w"))
{
// This removes the binding from the new node, since gridster will re-add the element.
ko.cleanNode(data[0].parentNode);
// Adds the widget to gridster.
self.gridster.add_widget($item);
// We must update the model with the position determined by gridster
object.datarow = parseInt($item.attr("data-row"));
object.datacol = parseInt($item.attr("data-col"));
}
}
};
};
var myViewModel = new viewmodel();
ko.applyBindings(myViewModel);
I still need to think about the remove and move events (a move in gridster should update the item's x and y values in the viewmodel). I started using knockout yesterday, so any help would be appreciated.
I couldn't find a cdn for the latest version of gridster. JSFiddle points to a temporary website I've added in Azure, I'll leave it up for a few days, but feel free to update it with your own link.
/------------------------------ UPDATE ----------------------------------/
I've updated my code to support deletions and moving widgets (http://jsfiddle.net/Be4cf/11/) but there's a small caveat: there's an open issue (https://github.com/knockout/knockout/issues/1130) that knockout cleans out jquery data before calling the beforeRemove event. This causes gridster to crash since the data needed to move the other items around is kept in a data element. A workaround could be to keep a copy of the data and to re-add it to the element later, but I've chosen the lazy way and commented the offending line in knockout.
Add class="gs_w" to ur li in gridster it should work
You should do something like below. addNewGridElement is called - with the rendered DOM element which is important in Gridster's case as gridster.add_widget accepts a DOM element as its first argument - once you've added something to the Knockout observable. After this, it's just a matter of then adding domNode to Gridster.
view.html:
<div class="gridster">
<ul data-bind="foreach: { myData, afterAdd: $root.addNewGridElement }">
<li data-bind="attr:{
'data-row':datarow,
'data-col':datacol,
'data-sizex':datasizex,
'data-sizey':datasizey
},text:text, doubleClick: $parent.AddOne"></li>
</ul>
</div>
view.js:
self.addNewGridElement = function (domNode, index, newTile) {
// Filter non li items - this will remove comments etc. dom nodes.
var liItem = $(domNode).filter('li');
if ( liItem.length > 0 ) {
// Add new Widget to Gridster
self.gridster.add_widget(domNode, newTile.x, newTile.y, newTile.row, newTile.col);
}
};
I've got a template to manage important property in my collection. It's a simple list filtered by this property with a toggle that allows changing its value:
<template name="list">
{{#each items}}
<div class="box" data-id="{{_id}}">
{{name}}
<span class="toggle">Toggle</span>
</div>
{{/each}}
</template>
Template.list.items = function() {
return Items.find({property: true}, {sort: {name: 1}});
};
Template.list.events({
'click .toggle': function(e) {
var item = Items.findOne( $(e.target).closest('.box').data('id') );
Items.update(item._id, {$set: {
property: !item.property;
}});
},
});
Quite simple. Now, obviously, when I click the toggle for an item, this item property turns to false and it's removed from the list. However, to enable easy undo, I'd like the item to stay in the list. Ideally, until user leaves the page, but a postponed fadeout is acceptable.
I don't want to block the reactivity completely, new items should appear on the list, and in case of name change it should be updated. All I want is for the removed items to stay for a while.
Is there an easy way to achieve this?
I would store the removed items in a new array, and do something like this:
var removed = []; // Contains removed items.
Template.list.created = function() {
// Make it empty in the beginning (if the template is used many times sequentially).
removed = []
};
Template.list.items = function() {
// Get items from the collection.
var items = Items.find({property: true}).fetch();
// Add the removed items to it.
items = items.concat(removed)
// Do the sorting.
items.sort(function(a, b){ return a.name < b.name}) // May be wrong sorter, but any way.
return items
};
Template.list.events({
'click .toggle': function(e) {
var item = Items.findOne( $(e.target).closest('.box').data('id') );
Items.update(item._id, {$set: {
property: !item.property;
}});
// Save the removed item in the removed list.
item.property = !item.property
item.deleted = true // To distinguish them from the none deleted ones.
removed.push(item)
},
});
That should work for you, wouldn't it? But there may be a better solution.