Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Dragging, Dropping, and Sorting With observableArrays

| Comments

Update: take a look at an updated version of this functionality here.

Using features from jQuery UI with KnockoutJS seems to be a fairly common scenario. Frequently, I have seen discussions about using custom bindings to initiate the jQuery UI widgets. There are several libraries of bindings for this purpose discussed in this thread. However, I haven’t seen much information about using some of the interactions like sortable, and droppable with Knockout.

It is easy enough to have a binding that initiates the draggable and sortable behaviors, but I think that this really becomes powerful when you are able to connect it to your view model. After all, once a user sorts some objects, it is not very useful unless you can save the state back to the server. I believe that we are in the best position when our view model is the source of truth.

To make this connection, we can use a custom binding. The jQuery UI interactions provide many events that we can tap into. However, within those events we are always dealing with DOM elements. So, our event handler needs to be able to get back to our data from an element. There are several ways to accomplish this task. For simply sorting a single list, we can use the jQuery Template plugin’s tmplItem function to get back to our data item. To get to the parent of the item, we can use the value that we pass to our binding.

Update: as of Knockout 2.0, there is now an API, ko.dataFor that will work universally.

So, a simple sortableList binding that you can place on the parent of your sortable elements could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//connect items with observableArrays
  ko.bindingHandlers.sortableList = {
      init: function(element, valueAccessor) {
          var list = valueAccessor();
          $(element).sortable({
              update: function(event, ui) {
                  //retrieve our actual data item
                  var item = ui.item.tmplItem().data;
                  //figure out its new position
                  var position = ko.utils.arrayIndexOf(ui.item.parent().children(), ui.item[0]);
                  //remove the item and add it back in the right spot
                  if (position >= 0) {
                      list.remove(item);
                      list.splice(position, 0, item);
                  }
              }
          });
      }
  };

Then, you would use the binding on the parent like:

1
<div class="container" data-bind="template: { name: 'taskTmpl', foreach: tasks}, sortableList: tasks"></div>

So, we identify the list from what is passed to the binding. We set up a handler for the update event triggered by jQuery UI when an item is dropped. The actual item in our array is retrieved using the tmplItem function on the element that was dropped. Then, we find the new index of the element and move our data item to the corresponding spot in its array. Now when a user drops an element in a new location, it will be reflected in our observableArray.

Here is a sample:

Link to full sample on jsFiddle.net

If you would want to pass additional options to the sortable function, then you could pass an object to the binding that has both your parent and the options to pass to jQuery UI.

Allowing items to be dropped between arrays

The next feature that I wanted to support was allowing an item to be dropped between multiple arrays. The logic necessary to make this happen is quite similar, but we need to know a little more information when an element is dropped. We need to know the data item, the item’s original parent array, and the item’s new array.

Making this connection with strictly tmplItem and the value passed to the binding was not working out properly for me. I decided to be a bit more explicit about the relationship between the elements and the data on my view model. Instead of using just the sortableList binding, I decided to also attach a sortableItem binding on the children of the elements.

The sortableItem binding only has an init function that uses jQuery’s data method to attach some meta-data to the element. I track the underlying data and the current parent of the item, which are passed to the binding.

1
2
3
4
5
6
7
8
//attach meta-data
ko.bindingHandlers.sortableItem = {
    init: function(element, valueAccessor) {
        var options = valueAccessor();
        $(element).data("sortItem", options.item);
        $(element).data("parentList", options.parentList);
    }
};

Additionally, the sortableList binding was tweaked a little bit to relate the container with its underlying array. Also, the update event handler now will remove the item from the original parent and add it to the new parent. This also works out properly for sorting within the same list, as the original and new parent will just be the same.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//connect items with observableArrays
ko.bindingHandlers.sortableList = {
    init: function(element, valueAccessor, allBindingsAccessor, context) {
        $(element).data("sortList", valueAccessor()); //attach meta-data
        $(element).sortable({
            update: function(event, ui) {
                var item = ui.item.data("sortItem");
                if (item) {
                    //identify parents
                    var originalParent = ui.item.data("parentList");
                    var newParent = ui.item.parent().data("sortList");
                    //figure out its new position
                    var position = ko.utils.arrayIndexOf(ui.item.parent().children(), ui.item[0]);
                    if (position >= 0) {
                        originalParent.remove(item);
                        newParent.splice(position, 0, item);
                    }
                }
            },
            connectWith: '.container'
        });
    }
};

Here is a sample that allows both sorting and dragging items between arrays:

Link to full sample on jsFiddle.net

The next steps with this binding would probably be to make it a bit more generic and accept additional options to be passed to sortable.

Seems like this would be an easy and useful way to add some nice functionality to a list of items. Being able to connect the changes directly to your view model allows you to persist the new state back to the server without any further logic.

Comments