Earlier today my coworker mentioned the need for a way to easily move items up and down on a web page. In this case the idea was to sort a list of documents. We've probably all done this before. You list out each item and the one on top has a down arrow, the one on the bottom has an up arrow, and all the rest have both up and down controls. Turns out - and no big surprise here - there is actually a cool little jQuery utility to make this a bit simpler - the Sortable control.
If you follow the link to the demo (and please do, it's rather slick!) you will see how nice this control works. Instead of slowly clicking items up and down, a user can simply drag and drop the order they want. A big +1 for usability here (in my opinion). What isn't so obvious though is how you persist these changes. Let's dig a bit into the control and I'll show you how you can tie it to ColdFusion.
First, let's look at a simple version that just does the sorting:
<html>
<head>
<script src="jquery-1.3.1.js"></script>
<script src="jquery-ui-personalized-1.6rc6.js"></script>
<script>
$(document).ready(function(){
$("#sortable").sortable();
});
</script>
</head>
<body>
<h1>Test</h1>
<ul id="sortable">
<li>First</li>
<li>Second</li>
<li>Third</li>
</ul>
</body>
</html>
I absolutely love this. Seriously. I want to form a domestic partnership with jQuery and never look at another framework again. You can see a demo of this here. But this is just a front end demo. Simple and sexy, but we need something real, and a lot of times that is where things can break down. I began by making the data dynamic.
<cfset data = queryNew("id,title","integer,varchar")>
<cfloop index="x" from="1" to="5">
<cfset queryAddRow(data)>
<cfset querySetCell(data, "id", x)>
<cfset querySetCell(data, "title", "Title #x#")>
</cfloop>
Normally that would be set in my controller code or a CFC at least, but you get the idea. I then changed my UL/LI:
<ul id="sortable">
<cfoutput query="data">
<li id="item_#id#">#title#</li>
</cfoutput>
</ul>
Ok, not rocket science, but you get the idea. I then added a button:
<input type="button" id="saveBtn" value="Persist">
and modified my document.ready:
$(document).ready(function(){
$("#sortable").sortable();
$("#saveBtn").click(persist)
});
My persist function will take care of saving the data. The docs for Sortable are pretty extensive. What we want is the serialize function. As you can guess, it will serialize the sortable data. It does this by grabbing the id values from the items you had marked as sortable. My persist function looks like so:
function persist() {
console.log('running persist....')
var data = $("#sortable").sortable('serialize')
console.log(data)
}
When run, the console reports:
item[]=3&item[]=2&item[]=1&item[]=4&item[]=5
Kind of an odd format. You can change it up a bit by passing additional parameters to the sortable call, but you get the basic idea. A demo of this version may be found here. Obviously you need Firebug installed and open to see the console messages.
Alright, so let's take it a step further. As I said, I thought that format was a bit odd. Sortable supports serializing to arrays as well:
var data = $("#sortable").sortable('toArray')
and we can tie this to an Ajax call
$.post('data.cfc?method=saveData',{order:data},function(res,txtStatus) {
console.log(txtStatus)
})
At this point I ran into a bit of trouble. I forgot that you can't send a complex data structure 'as is' over the wire. jQuery was smarter than me in this case and simply converted the data back into a list, much like the first serialize example. This time though it was just a list of values:
item_N,item_X,item_Z
My CFC would have to parse the list and note both the position and the ID value from each item:
<cffunction name="saveData" access="remote" returnType="void" output="false">
<cfargument name="order" type="any" required="true">
<cfset var x = "">
<cfset var id = "">
<cfset var item = "">
<!--- loop through and make a new order --->
<cfloop index="x" from="1" to="#listLen(arguments.order)#">
<cfset item = listGetAt(arguments.order, x)>
<cfset id = listGetAt(item,2,"_")>
<cflog file="ajax" text="setting id #id# to position #x#">
</cfloop>
</cffunction>
Normally this would actually run a query, but I think you get the idea. I've included my sample code as an attachment.
I have to admit - I thought the 'interactions' section of jQuery UI wasn't that exciting, but I'm beginning to see some real benefit here.
Archived Comments
Hi Ray,
You'll run into trouble if you setup more than one list of items to sort between as the serialize method will not pick up the parent 'ul' in it's output.
Just a tip if you get more advanced with your lists..
I may be slow - but what do you mean more than one list of items to sort? You mean 2 sortable lists? Wouldn't they have different IDs?
I have been playing aroudn with sortables too.
I like that you can also format your HTML like this
<div id="beers">
<div>Yuengling</div>
<div>Tenentt's</div>
<div>JW Dundee's Homey Brown</div>
</div>
And still have the innder <div>s be the items that can be sorted.
Ray, just a quick note to say how useful I've found your series of posts concerning jQuery - I've tried a few AJAX/JS libs over the years before settling on jQuery. In recent weeks, perhaps spurred on by what I've been reading here, it's become an obsession! :-) Please keep up the good work. J.
Ray,
I think Bill is saying that if you have a nested lists, there may be a problem, just a guess
I didn't play with nested lists. If that did work fine - I could see it being a bit confusing though. May be a bit too much for the casual user.
p.s. Working on a Progress Bar demo today.
Having taken a stab at sorting nested lists in a production app, I can tell you that while it's possible to do under the right conditions, it's a pain to do and it was hard for my users to tell where they needed to drag the list item in order to drop it into another nesting level.
For that situation, I ended up replacing the nested sort with a technique (also powered by jQuery) I originally developed for reordering items on a web page when using an iPhone or iPod Touch (since drag-and-drop isn't really an option there) that lets my users move an item with just two mouse clicks. You can find the original blog post about the technique here:
http://www.swartzfager.org/...
While you're on the 'sortable' topic, there's a nifty tablesorter plugin at http://tablesorter.com/docs/ that adds clickable sort headers to standard HTML tables. It will even zebra-stripe the tables for you in the process.
I've been using jQuery a lot in my applications, also, I find it very easy to use, even insanely simple at times. I did some earlier work with Spry, but jQuery just seems so much easier to code and maintain.
I wonder is you could use this to re-order the links on my fisheye menu?
http://home.cfproject.co.uk
Jonny
@Joel Cox: Just checked out the table sorter - that is pretty hot.
Hey Ray,
Just thought I'd point out that this is a fairly sweet table sorter:
http://kryogenix.org/code/b...
using what the author calls unobtrusive javascript. its very customizable and fairly fast.
regards,
larry
Very nice script!
I have a problem with database. Could you please provide a little CF code, which would save reordered list into database. I can't figur eit out.
Plaese help! Many thanks!
You would modify line 11 in the last code listing to be an insert query.
OK, first off, I am a javascript dummy. I get your comment about changing line 11 to an insert query, but what if I want it to be an update query?
Unless I'm misreading you, wouldn't you just put in a cfquery?
My apologies, I wasn't very detailed in my question. I thought maybe you were going to use a Jedi mind trick to know what I was thinking :) My initial thought on this scenario is having a database with a ID, title and sortValue. Once the database call is made I would assign list id with the value of the sortValue or the ID? When the data is returned to the cfc and the UPDATE query executes, what would you use in the WHERE clause, the ID of the list to the sortValue? I also get a 'java.lang.String cannot be cast to java.util.Map' error with my current attempt....like I said, I am a javascript dummy.
Well remember you are passed the ID the record and the new sort, right? So your update would simply do something like so
update tblFoo
set sortorder = #NEWORDER#
where id = #THEID#
neworder would be the new sort order, and THEID the primary key. In my example above, it's #id# and #x# in the cflog. Oh, and you would use cfqueryparam of course.
very cool - I had 500 errors not passing order using jQuery 1.4.2 but no issues with 1.3.1 (from your download)... any thoughts?
500 errors? I assume you mean HTTP Status Code 500. That's an error. As to why, it depends on what the root error is. :) If you tried jQuery 1.4.2,, also ensure you are using the latest jQuery UI library.
I'm also having trouble getting sortable to work using jQuery 1.4.2 with the latest jQuery UI library and it seems to come down to how "toArray" passes the ids.
In jQuery 1.3.1, toArray produces the parameters
sortedIdArr 2504
sortedIdArr 2471
sortedIdArr 2472
Source
sortedIdArr=2504&sortedIdArr=2471&sortedIdArr=2472
(From Firebug)
and now with 1.4.2 it's change to
sortedIdArr[] 27
sortedIdArr[] 26
sortedIdArr[] 28
Source
sortedIdArr%5B%5D=27&sortedIdArr%5B%5D=26&sortedIdArr%5B%5D=28
The brackets seem to break my coldfusion code that loops through the list of values. I get an error that says "Element SORTEDIDARR is undefined"
This was an "improvement" to jQuery 1.4.2 to work well with "modern" scripting languages. They call it "deep serialization". I call it "breaking things that weren't broke."
add
jQuery.ajaxSettings.traditional = true;
to your document ready function.
Joel!
YES!
Thank you. You just made my day.
Nando
JOEL COX YOU ARE MY HERO
i fought with this problem for hours yesterday. Thanks for sharing your solution.
And thanks Raymond, for this excellent tutorial (and the rest of your blog as well).
This traditional ajaxSettings saved my butt too! Raymond, I love the sortable feature but was having to use old version of jQuery to implement. Thanks for the tip Joel!!