Using the scriptaculous library creating drag & drop sortable lists is a breeze. The example uses brail and based on a recent patch added to the Binder Trunk.
Code
View.brail
<ul id="PhoneNumber_list"> <?brail i = 0; for ph in PhoneNumbers : ?> <li id="phonenumber_${ph.Id}" class="sortable"> ${Form.HiddenField("phonenumbers[${i}].id")} ${Form.TextField("phonenumbers[${i}].phone")} <img class="handle" style="cursor:move; padding-left: .25em;" alt="reorder item" src="${siteroot}/Assets/img/reorder-icon.png" width="14" height="14" title="Re-Order Items" /> </li> <?brail i++; end ?> </ul> <script type="text/javascript"> Sortable.create("PhoneNumber_list", { handle:"handle", only:"sortable", onUpdate:function(ul) { new Ajax.Updater("", "${siteroot}/Contact/ContactMethods/ReOrder.rails?type=PhoneNumber" + "&contactId=${contact.Id}", {parameters:Sortable.serialize(ul), method:'get', evalScripts:true}); } }); </script>
The above view creates an unordered list that will allow the user to use drag and drop to reorder the elements in the list. Upon each change in order an ajax request will be sending the id's of the phonenumbers in the new order.
Laying out the Container
It is recommended to use an unordered list for the drag and drop to work properly in all browsers. There are known browser restrictions preventing the use of table. The only real key here is setting the id of the container. The id will be used in the ajax request as the name of parameter which will contain the values in the sorted order.
Creating the Items
When creating the items there a couple options. First is the id of the item. Using prototype serialization the id should have a prefix, an underscore, then the value you want sent back to the server. In this example we are creating an id "phonenumber_Guid" where Guid is being replaced by the unique Id of the PhoneNumber. Next we add a class name of "sortable". We use this to restrict scriptaculous to only allowing items tagged sortable to be drag and droppable. This is optional. Finally we add an image with the class "handle". This is also optional tells scriptaculous to create a drag and drop handle on this element. By default the entire list item element would be draggable, but since we allow editing we want to give the user a specific handle to use to reorder.
Initializing the Drag & Drop
The final piece of the view is making the container sortable. This achieve by calling Sortable.create and passing any options you want. See the script.aculo.us documentation for more help. This initialization also uses a callback function when the list changes. The callback function simply serializes the list and sends the values back to the server in an Ajax request.
ReOrder.brailjs
page.VisualEffect('Highlight', "${contactMethodType}_list")
Uses JSGeneration to highlight the container indicating a successful save.
ContactMethodsController.cs
public class ContactMethodsController : BaseController { private readonly IContactMethodsService contactMethodsService; public ContactMethodsController(IContactMethodsService contactMethodsService) { this.contactMethodsService = contactMethodsService; } public void View() { PropertyBag["PhoneNumbers"] = null; // A Collection of phone numbers } public void ReOrder(string type, Guid contactId, Guid[] phonenumber_list) { contactMethodsService(type, contactId, phonenumber_list); PropertyBag["contactMethodType"] = type; } }
View
Just fill a collection of phonenumbers to display.
ReOrder
The ReOrder method is what will handle the ajax request. This simple binds the parameter values, notice the name of the Guid array. Then delegates the responsibility of persisting the changes to the service. This controller assumes you are using Windsor and injecting the service in the Controller constructor. For sake of simplicity I have not done any error handling.
PhoneNumber.cs
public class PhoneNumber { private Guid id; private string phone; public Guid Id { get { return id; } set { id = value; } } public string Phone { get { return phone; } set { phone = value; } } }
The domain model class used to represent the phone number.
More...
To Serialize the all the data in the container you can try this function. There is probably a better way but this is what I started playing with.
function serializeContainer(container)
{
var s = serializeContainer(container);
if (s.length > 0)
return s.substring(0, s.length - 1);
return s;
}
function serializeContainerR(el, childs, index, data)
{
if(!data) data = "";
var e = $(el);
if(!e) return "";
if(e.serialize)
{
var s = e.serialize();
if (s && s.length > 0)
data += s + "&";
}
var echilds = e.childElements();
if (echilds && echilds.length > 0)
data += serializeContainerR(echilds[0], echilds, 0);
if(childs && index > -1 && index < childs.length - 1)
data += serializeContainerR(childs[index + 1], childs, index + 1);
}
Its been a while since I had do recursion... please fix this terriable code!!
