I'm creating a list editor using Angular and jQuery UI. The goal is to add/edit/remove/sort items.
Here's my HTML code to display the list.
<ul id="sortable" >
<li ng-repeat="cat in admin.catalogues">
<label for="title">Title</label>
<input type="text" name="title" ng-model="cat.title" />
<label for="image">Image</label><input type="text" name="image" ng-model="cat.image" istype="image" />
<button ng-click="admin.removeCatalogue(cat)">Remove</button>
</li>
</ul>
Then I have these Angular methods (I have just copied what should be useful).
this.catalogues = [];
this.addCatalogue = function () {
var newcat = {
title: "",
image: "",
pdf: ""
};
this.catalogues.push(newcat);
};
this.removeCatalogue = function (item) {
this.catalogues.splice(this.catalogues.indexOf(item), 1);
};
this.sort = function(fromIndex, toIndex){
var element = this.catalogues[fromIndex];
this.catalogues.splice(fromIndex, 1);
this.catalogues.splice(toIndex, 0, element);
};
And finally this jQuery script for the sortable part - it works using jQuery UI, and I call the Angular controller to update the list. I have probably found this snippet somewhere on Stackoverflow.
$(document).ready(function(){
startIndex = -1;
newIndex = -1;
$('#sortable').sortable({
placeholder:'placeholder',
start: function (event, ui) {
startIndex = ($(ui.item).index());
},
stop: function(event, ui){
newIndex = ($(ui.item).index());
angular.element(document.getElementById('content')).controller().sort(startIndex, newIndex);
}
});
});
Adding items, removing and sorting are all working fine. But after sorting, the list becomes crazy. New items are added more or less randomly inside the list, and the remove button sometimes removes more than one item.
I have created a Fiddle for testing: https://jsfiddle.net/Neow85/jbg8kdv4/
Thanks for any help!
Related
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 } })
}
}
});
Initial Situation
TL;DR
I want to filter a nested array with a custom AngularJS filter. Unfortunately not only the data for ng-repeat are filtered but the original object from $scope too.
I have a small demo as a fiddle for you: http://jsfiddle.net/7XRB2/
E.g. search for Test and the list will getting shorter. Delete your search and you'll see what happens: nothing. The filter don't filters the list, he filters the original data!
This is my filter:
app.filter('searchFilter', function() {
return function(areas, searchTerm) {
if (!angular.isUndefined(areas)
&& !angular.isUndefined(searchTerm)) {
var searchExp;
searchExp = new RegExp(searchTerm, 'gi');
var tmpAreas = [];
angular.forEach(areas, function(area) {
var tmpTopics = [];
angular.forEach(area.topics, function(topic) {
var tmpVideos = [];
angular.forEach(topic.videos, function(video) {
if (video.title.match(searchExp)) {
tmpVideos.push(video);
}
});
topic.videos = tmpVideos;
if (topic.videos.length > 0 || topic.title.match(searchExp)) {
tmpTopics.push(topic);
}
});
area.topics = tmpTopics;
if (area.topics.length > 0) {
tmpAreas.push(area);
}
});
return tmpAreas;
}
else {
return areas;
}
}
});
I already implemented another custom search filter that works great and doesn't have this issue:
app.filter('mainNavigationFilter', function () {
return function (elements, selectedElement) {
if (!angular.isUndefined(elements) && !angular.isUndefined(selectedElement) && selectedElement.length > 0) {
var tempElements = [];
angular.forEach(selectedElement, function (id) {
angular.forEach(elements, function (element) {
if(angular.equals(element.id, id)
tempElements.push(element);
}
});
});
return tempElements;
}
else {
elements
}
}
});
What I think is the problem
I'm using topic.videos = tmpVideos; and area.topics = tmpTopics; in my filter. I guess, that due to javascripts object reference these new assignments are inherited to the original object - my scope object.
After some research I found angular.copy() and added following block in my filter:
var cpAreas;
cpAreas = angular.copy(areas);
In addition I changed the first angular.forEach to
angular.foreach(cpAreas, function(area) {[...]});
Here is an updated fiddle: http://jsfiddle.net/dTA9k/
Infinite $digest loop
Yippi! The filter works as expected - unless you open the console ...
Now the filter causes a new problem:
Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [...]
I found a few questions on SO about this error and it looks like it's thrown because the filter is returning a new object each time due to angular.copy.
Any ideas how I can solve this problem?
Try using a custom ng-repeat.
So i tried your code and use $index instead. For topic.title and video.title, I don't know. Try a custom ng-repeat.
<div ng-app="videoList">
<input ng-model='search' />
<ul ng-controller='AreasController' class="video-list-areas">
<li ng-repeat='area in filtered = (subject.areas | searchFilter:search)'>
{{subject.areas[$index].title}}
<ul>
<li ng-repeat='topic in area.topics'>
<ul>
<li ng-repeat='video in topic.videos'>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
Your filter works, I tried having just this in your HTML:
<div ng-app="videoList">
<input ng-model='search' />
<ul ng-controller='AreasController' class="video-list-areas">
{{subject.areas | searchFilter:search}}
</ul>
</div>
I have an ExtJS DataView with following template:
<ul>
<tpl for=".">
<li class="report-field" id="{listId}">
{[this.getReorderLinks(values, xindex, xcount)]}
<span class="field-title small" data-qtip="{displayName}">{displayName}</span>
<i class="field-remove" title="' + deleteLabel + '"></i>
</li>
</tpl>
</ul>
Which makes each list of items look like this:
Where user can click on different icons and perform related action, moving up/down in order and remove.
Note that these items are added to dataview using Drag and Drop, where there's another source dataview container from which I drag the items and add here. While these up/down arrows are working fine with reordering them, I want to reorder these items using drag-n-drop internally.
So, to make each individual item draggable and droppable in the same region, I used refresh event of dataview and registered DNDs there as follows:
listeners: {
'refresh': function(dataview, eOpts) {
var fieldsList = Ext.query('.added-field');
// Iterate over the list and make each item draggable/droppable.
Ext.each(fieldsList,function(field){
var dragSource,
fieldId;
fieldId = field.id;
dragSource = new Ext.dd.DragSource(field, {
isTarget : false
});
dropZone = new Ext.dd.DropTarget(field);
dragSource.dragData = {
record: me.viewStore.findRecord('listId', fieldId),
fieldId: fieldId
};
dropZone.notifyDrop = function(source, event, params) {
var targetRecord = me.viewStore.findRecord('listId', fieldId),
targetRecordIdx = me.viewStore.indexOf(targetRecord),
sourceRecordIdx = me.viewStore.indexOf(params.record);
//Perform rearrangement in the store.
me.viewStore.removeAt(sourceRecordIdx);
me.viewStore.insert(targetRecordIdx, params.record);
return true;
};
});
}
But it is giving me weird behaviours; when I try to drag "Person Email" on top of "Person City", DataView gets broken to look like following:
Also, I get Uncaught TypeError: Cannot read property 'internalId' of undefined when drop operation completes. I even tried to defer calls to removeAt() and insert() by certain ms, but still no luck, moreover, ExtJS has no documentation or working example available for Drag n Drop to Reorder DataView items.
Any help would be appreciated, thanks.
This reminds me a bad experience I had with drag drop in ExtJS 4. Anyway, you may try this plugin. Otherwise, here is something I have tried (scroll is not handled yet) (jsFiddle) :
new Ext.view.DragZone({
view: view,
ddGroup: 'test',
dragText: 'test'
});
new Ext.view.DropZone({
view: view,
ddGroup: 'test',
handleNodeDrop : function(data, record, position) {
var view = this.view,
store = view.getStore(),
index, records, i, len;
if (data.copy) {
records = data.records;
data.records = [];
for (i = 0, len = records.length; i < len; i++) {
data.records.push(records[i].copy(records[i].getId()));
}
} else {
data.view.store.remove(data.records, data.view === view);
}
index = store.indexOf(record);
if (position !== 'before') {
index++;
}
store.insert(index, data.records);
view.getSelectionModel().select(data.records);
}
});
I’m trying to add sort options to a JQM list which is backed by a backbone.js collection. I’m able to sort the collection (through the collection’s view) and rerender the list, but JQM isn’t refreshing the list.
I’ve been searching and I found several questions similar to mine (problems getting the JQM listview to refresh) but I’ve been unable to get it to work.
I’ve tried calling $(‘#list’).listview(‘refresh’) and $(‘#list-page’).page() etc. to no avail. I suspect that Perhaps I’m calling the refresh method in the wrong place (to early), but I’m not sure where else I should put it (I’m just starting out with backbone).
Here’s the markup and js.
HTML:
<div data-role="page" id="Main">
<div data-role="header"><h1>Main Page</h1></div>
<div data-role="content">
<ul data-role="listview">
<li>Page 1</li>
</ul>
</div>
<div data-role="footer"><h4>Footer</h4></div>
</div>
<div data-role="page" id="Page1">
<div data-role="header">
Back
<h1>Items</h1><a href="#dvItemSort" >Sort</a></div>
<div data-role="content">
<div id="dvTest">
<ul id="ItemList" data-role="listview" data-filter="true"></ul>
</div>
</div><div data-role="footer"><h4>Footer</h4></div></div>
<div data-role="page" id="dvItemSort">
<div data-role="header"><h4>Sort</h4></div>
<a href="#Page1" type="button"
name="btnSortByID" id="btnSortByID">ID</a>
<a href="#Page1" type="button"
name="btnSortByName" id="btnSortByName">Name </a>
</div>
Javascript:
$(function () {
window.Item = Backbone.Model.extend({
ID: null,
Name: null
});
window.ItemList = Backbone.Collection.extend({
model: Item
});
window.items = new ItemList;
window.ItemView = Backbone.View.extend({
tagName: 'li',
initialize: function () {
this.model.bind('change', this.render, this);
},
render: function () {
$(this.el).html('<a>' + this.model.get('Name') + '</a>');
return this;
}
});
window.ItemListView = Backbone.View.extend({
el: $('body'),
_ItemViews: {},
events: {
"click #btnSortByID": "sortByID",
"click #btnSortByName": "sortByName"
},
initialize: function () {
items.bind('add', this.add, this);
items.bind('reset', this.render, this);
},
render: function () {
$('#ItemList').empty();
_.each(items.models, function (item, idx) {
$('#ItemList').append(this._ItemViews[item.get('ID')].render().el);
}, this);
$('#ItemList').listview('refresh'); //not working
// $('#ItemList').listview();
// $('#Page1').trigger('create');
// $('#Page1').page(); //also doesn't work
},
add: function (item) {
var view = new ItemView({ model: item });
this._ItemViews[item.get('ID')] = view;
this.$('#ItemList').append(view.render().el);
},
sortByName: function () {
items.comparator = function (item) { return item.get('Name'); };
items.sort();
},
sortByID: function () {
items.comparator = function (item) { return item.get('ID'); };
items.sort();
}
});
window.itemListView = new ItemListView;
window.AppView = Backbone.View.extend({
el: $('body'),
initialize: function () {
items.add([{ID: 1, Name: 'Foo 1'}, {ID:2, Name: 'Bar 2'}]);
},
});
window.App = new AppView;
});
EDIT: I realized that the first line of html markup I posted wasn't displaying in my post so I pushed it down a line.
EDIT 2: Here's a link to a jsfiddle of the code http://jsfiddle.net/8vtyr/2/
EDIT 3 Looking at the resulting markup, it seems like JQM adds some of the classes to the list items. I tried adding them manually using a flag to determine whether the list was being reRendered as a result of a sort and the list then displays correctly.
However, besides being somewhat of an ugly solution, more importantly my backbone events on the “item” view no longer fire (in the code example I posted I didn’t put the code for the events because I was trying to keep it as relevant as possible).
EDIT 4 I sort of got it working by clearing my cache of views and recreating them. I posted my answer below.
EDIT 5
I updated my answer with what i think is a better answer.
I'm not sure if this should be its own answer or not (i did look through the FAQ a bit), so for now I’m just updating my previous answer.
I have now found a better way to sort the list using my cached views. Essentially the trick is to sort the collection, detach the elements from the DOM and then reattach them.
So
The code now would be
$list = $('#ItemList')
$('li', $list ).detach();
var frag = document.createDocumentFragment();
var view;
_.each(item.models, function (mdl) {
view = this._ItemViews[item.get('ID')];
frag.appendChild(view.el);
},this);
$list.append(frag);
OLD ANSWER
I sort of solved the problem. I was examing the rendered elements and I noticed that when the elements were “rerendered” (after the sort) they lost the event handlers (I checked in firebug). So I decided to clear my cache of views and recreate them. This seems to do the trick, though I’m not really sure why exactly.
For the code:
Instead of:
$('#ItemList').empty();
_.each(items.models, function (item, idx) {
$('#ItemList').append(this._ItemViews[item.get('ID')].render().el);
}, this);
$('#ItemList').listview('refresh'); //not working
I clear the view cache and recreate the views.
$('#ItemList').empty();
this._ItemViews = {};
_.each(items.models, function (item, idx) {
var view = new ItemView({ model: item });
this._ItemViews[item.get('ID')] = view;
this.$('#ItemList').append(view.render().el)
}, this);
$('#ItemList').listview('refresh'); //works now
I think it would probably be better if I didn’t need to regenerate the cache, but at least this is a working solution and if I don't get a better answer then I'll just accept this one.
I had some luck in solving this, but the reason remains obscure to me.
Basically, at the top of my render view after establishing the html() of my element, I call listview(). Then, any further items I might add to a list call listview('refresh').
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