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
Then, you would use the binding on the parent like:
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:
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.
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
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
Here is a sample that allows both sorting and dragging items between arrays:
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.