jquery-ui-logo.pngFor a project that I am working on Your-Scrum I need a lot of items in a list that are sortable. One Story is to have a backlog with stories that are sortable to reflect the importance. Using Domain Driven Design, I have created a rich domain model. Using the model, we can execute all stories related to sorting items in a list. At the front-end I want an easy way to handle the sorting. I found a library for jQuery that can do the job for a large part on the client. Still there are some server side components as well.

The following screen gives an idea about the look and feel of the sortable list of stories.

Screenshot_yourscrum.png

jQuery Sortable component

Let us start to have a look at the front end. I am using jQuery together with jQuery-ui. Everybody doing series web development (should) know jQuery. I have written about jQuery before, but other have as well. Just do a search on google and the amount of articles is massive. Dzone also contains a few items each day about top 5 plugins for jQuery. I think the power of jQuery-ui is not known that much. If you have not seen it before, be sure to check it out. It can help you a lot in creating websites that look sharp with icons and colors that are well balanced. The ui framework contains a lot of interaction components, widgets and effects. This blog posts focus is on the Sortable interaction component.

With the sortable component you can actually drag the components in order. You get notified of the change.

Setting op de component

Before we can use the component we need to download the jquery-ui component. The jQuery UI project contains a download function to create your customized download. Don’t forget to include at least the sortable function. You can also follow a long with the sources I use in the your-scrum project. Now include the js files in your page. I use the script.js file to set generic libraries. (webapp/includes folder). Now open up the stories.jsp file under webapp/WEB-INF/jsp/story folder.

We begin by creating the list of items and use jQuery to make a sortable list out of it.

<ul id="sortable">
    <c:forEach items="${stories}" var="story">
        <li id="story_${story.id}" class="ui-state-default"><span class="listItemLabel">${story.name}</span></li>
    </c:forEach>
</ul>

Within the script block we now have to initialize the sortable component.

var idsOldOrder;
$(function() {
    $("#sortable").sortable();
    idsOldOrder = $("#sortable").sortable('serialize').toString();
    $("#sortable").disableSelection();
    $("#sortable").bind('sortupdate', function(event, ui) {
        var ids = $("#sortable").sortable('serialize').toString();
        $.post('/story/reorder', {ids: ids, oldIds: idsOldOrder}, function(data) {
            // nothing special
            window.alert(data);
        });
        idsOldOrder = ids;
    });
});

This code might look more difficult than you did expect. That is because more is happening here than you expect. Within this code we need only the first line to show the sortable items. The other lines are for notification of changes. Don’t worry we explain them as well. Time to move on.

Capturing change events

Have a look at the previous code block. The second line stories a special form of presentation in a string. We need this on the server to actually determine the change. Than in line 4, we bind a function to the sortupdate event. Again a string containing the ids in the new order is created. Using the jQuery Ajax tooling we send a request to the server. The POST is done to the url /story/reorder with two parameters: ids and idsOldOrder. The result is shown in an alert box, which is not nice, but it works for now.

From the client side perspective, that is all there is to it. I actually wanted to have a real change from the library, like this item is moved before that item. But I could not find how to do just that. Therefore I had to do the trick with the old order and the new order. Now the server has to take care of finding the actual change. More on that later.

Add task button

One of the stories for our project is to add a new task to a story. If you have a look at the image containing a screen dump (the first image of this blog post) you see on each story the amount of tasks and an orange plus sign. The plus sign is something we use from the jquery-ui project as well. How to do this.

First we must add the content to the html using the jsp. So we have to extend the jsp with a text containing the number of tasks and a link that we can click to go to the add task form. The jsp now becomes:

<ul id="sortable">
    <c:forEach items="${stories}" var="story">
        <li id="story_${story.id}" class="ui-state-default"><span
                class="listItemLabel">${story.name}</span><span
                class="listItemAdd"><a
                href="/story/${story.id}/task/form" title="add task"><spring:message code="generic.add"/></a></span><span
                class="listItemChilds">tasks(${story.numberOfTasks})</span></li>
    </c:forEach>
</ul>

Have a good look at the classes of the span elements. We are going to use them to change the appearance of the add link into that nice plus sign button. We are replacing the link with an image, clicking the image will call the function myClick which uses the actual link from the a element and opens that url. Have a look at the changes in the code.

var myClick = function (event) {
    var location = $(event.target).children("a")[0].href;
    window.location = location;
};
$(function() {
    // only show changes, refer to previous code block for other lines
    $("#sortable li span.listItemAdd").addClass("ui-icon ui-icon-circle-plus").click(myClick);
});

Getting the icon with the plus sign is a jQuery trick as well. There are some style sheets that contain all the icons. You just need to find the right class to add to a span and you are done. I used the ui-icon ui-icon-circle-plus. More on the available icons can be found in the documentation.

Other jQuery things

There are other things I use from jQuery. Check the references section for documentation. I use the corner plugin for the navigation. If you like the navigation implementation have a look at the main.jsp file in webapp/decorators. The final jQuery thing I want to mention is the hotkeys plugin. You can create hotkeys to be used on your page. I created a hotkey ctrl+s to open up the new story screen. The following code block shows the implementation.

var newStory = function (event) {
    window.location = '/story/form';
    event.stopPropagation();
    event.preventDefault();
    return false;
};
$(function() {
    $(document).bind('keydown', 'ctrl+S', newStory);
});

Finding the change on the server component

The current status is that we have a POST to the server with two string containing ids in the old and the new order. Now we have to find the change. On the server I use Spring 3 to handle requests. I am not going to explain all the spring 3 configuration in this post. If you want to learn about spring 3 I advise you to go over the reference documentation.

Handling the POST consists of two parts. The first part is finding the change from the two mentioned strings. The second part is communicate the change to the domain and persisting the change. In a next post I’ll focus more on the domain and the persistence part. For now have a look at the complete method that accepts the POST and handles it.

    @RequestMapping(value = "/story/reorder", method = RequestMethod.POST)
    public String reorderStories(@RequestParam("ids") String ids,
                                 @RequestParam("oldIds") String oldIds, ModelMap modelMap) {
        ItemSorter itemSorter = new ItemSorter();
        ChangedSortOfItems changedSortOfItems = itemSorter.determineChangeInStoryOrdering(strip(ids), strip(oldIds));

        Long movedItemId = null;
        try {
            if (StringUtils.hasText(changedSortOfItems.movedItem())) {
                movedItemId = Long.parseLong(changedSortOfItems.movedItem());
            }
        } catch (NumberFormatException e) {
            logger.warn("Number format exception of id from story to be planned while nog expected : "
                    + changedSortOfItems.movedItem());
        }

        Long beforeItemId = null;
        try {
            beforeItemId = Long.parseLong(changedSortOfItems.beforeItem());
        } catch (NumberFormatException e) {
            logger.warn("NUmber format exception while nog expected");
        }
        backlogService.plan(movedItemId, BEFORE, beforeItemId);

        modelMap.addAttribute("message", "story.order.changed");
        return "message";
    }

This code should not be to hard to follow, now lets have a look at the ItemSorter class. This class uses the string to find differences. An instance of ChangedSortOfItems is returned that contains information about the items that have changed. In the end it is all about going through the list of items to find the first that changed from position. The first item that is different does not have to be the actual item that is moved. We can check that by looking at the next item. Have a look at the code if you can see what happens.

public ChangedSortOfItems determineChangeInStoryOrdering(String ids, String oldIds) {
validateInput(ids, oldIds);

if (ids.equals(oldIds)) {
return ChangedSortOfItems.nothingChanged();
}

String[] arrayOfCurrentIds = StringUtils.delimitedListToStringArray(ids, delimiter);
String[] arrayOfOldIds = StringUtils.delimitedListToStringArray(oldIds, delimiter);

// A change must be found since the items are not equal
ChangedSortOfItems change = null;
int indexFirstChangedItem = findchangeInItems(arrayOfCurrentIds, arrayOfOldIds);
// If you start at the beginning, the last item can never be the first item to change position
String nextOfFirstChangedItem = arrayOfCurrentIds[indexFirstChangedItem + 1];

if (nextOfFirstChangedItem.equals(arrayOfOldIds[indexFirstChangedItem])) {
change = ChangedSortOfItems.movedBefore(arrayOfCurrentIds[indexFirstChangedItem],
arrayOfOldIds[indexFirstChangedItem]);
} else {
int lastitem = arrayOfCurrentIds.length – 1;
for (int indexMovedBefore = indexFirstChangedItem; indexMovedBefore < arrayOfCurrentIds.length; indexMovedBefore++) { String movedItem = arrayOfCurrentIds[indexMovedBefore]; if (indexMovedBefore == lastitem) { change = ChangedSortOfItems.movedToEnd(movedItem); break; } else { String aNextOldItem = arrayOfOldIds[indexMovedBefore + 1]; if (!movedItem.equals(aNextOldItem)) { change = ChangedSortOfItems.movedBefore(movedItem, aNextOldItem); break; } } } } return change; } private int findchangeInItems(String[] arrayOfCurrentIds, String[] arrayOfOldIds) { for (int i = 0; i < arrayOfCurrentIds.length; i++) { String possibleMovedItem = arrayOfCurrentIds[i]; String originalItem = arrayOfOldIds[i]; if (!possibleMovedItem.equals(originalItem)) { return i; } } throw new NoChangeInSortFoundException(); } [/sourcecode]

I hope the code is understandable. If not, questions can be asked in the comments :-).

Concluding

Again I have to conclude that jQuery is a fantastic framework for client side development. I also have to conclude that the documentation and available samples are not always thorough enough. I am still not sure if there is no easier way to find the actual change. But maybe one of the readers of this blog out there can help me out. I do love the extendibility of the jQuery components.

If you liked this post, please push me up at dzone, if you have questions or remarks do not hesitate to use the comments.

References

Creating a sortable list of items using jQuery
Tagged on:             

4 thoughts on “Creating a sortable list of items using jQuery

  • December 17, 2009 at 1:58 am
    Permalink

    Bah, ignore the above. I did not realize that when you were returning “message” you WANTED spring to resolve the request for a message.jsp and utilize that in the response. I get it now. It helps to actually download your source and look through it >_<.

  • December 16, 2009 at 9:43 pm
    Permalink

    jettro, great post. I tried to utilize method with a few differences but I cannot get the controller not to attempt to resolve a view from the string that is returned. So in your case, for instance, DefaultRequestToViewNameTranslator attempts to resolve WEB-INF/jsp/message.jsp to a view. Any idea how to get around this when using AJAX posts?

  • September 14, 2009 at 8:47 am
    Permalink

    Thanks 4 the post, Jettro!
    js frameworks are nice

  • September 14, 2009 at 1:10 am
    Permalink

    Yay jQuery! It really is the best, eh 🙂

Comments are closed.