Wilsonhut

Deal with it or don't

Knockout + jQueryUI Draggable/Droppable Follow-up

[Edit: The final edition in the Knockout + jQueryUI Draggable/Droppable series is in!]

This is the follow-up to my last post which described the simplest implementation possible of drag and drop with knockout.  In that example, the items in the list never moved, they were just cloned when dragged and dropped on the target. The goal accomplished was to drag data from one binding to another while allowing knockout to display the addition of data in the drop target.

Goals for This Iteration

  • I wanted to allow knockout to display the addition of the data in the drop target (as in the last post) and also display the removal of the data from the source when dragging begins.
  • I wanted the data item that is dragged to either be an element from an observable array OR a stand-alone observable – the former forcing the entire array’s UI to be redrawn without the element and the latter allowing just the space where the element was to be redrawn, allowing an empty space where the element was to remain, if you choose.

How it Works

The chain of events in this implementation is: 1) user clicks and begins a drag, then 2) JQueryUI clones the DOM element that will be used during the visual drag, then 3) the bound dragged data item is removed from the source. When the drop event happens on a droppable, 4a) the droppable’s bound data value is set to the dragged data item. If the drop event doesn’t happen, 4b) the dragged data item is added back to where it was removed in #3.

Code

(function ($, ko) {

  var _dragged, _hasBeenDropped, _draggedIndex;

  ko.bindingHandlers.drag = {

    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {

      var dragElement = $(element);

      var dragOptions = {

        helper: ‘clone’,

        revert: true,

        revertDuration: 0,

        start: function () {

          _hasBeenDropped = false;

          _dragged = ko.utils.unwrapObservable(valueAccessor().value);

          if ($.isFunction(valueAccessor().value)) {

            valueAccessor().value(undefined);

            dragElement.draggable(“option”, “revertDuration”, 500);

          } else if (valueAccessor().array) {

            _draggedIndex = valueAccessor().array.indexOf(_dragged);

            valueAccessor().array.splice(_draggedIndex, 1);

          }

        },

        stop: function (e, ui) {

          if (!_hasBeenDropped) {

            if ($.isFunction(valueAccessor().value)) {

              valueAccessor().value(_dragged);

            } else if (valueAccessor().array) {

              valueAccessor().array.splice(_draggedIndex, 0, _dragged);

            }

          }

        },

        cursor: ‘default’

      };

      dragElement.draggable(dragOptions).disableSelection();

    }

  };

  ko.bindingHandlers.drop = {

    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {

      var dropElement = $(element);

      var dropOptions = {

        tolerance: ‘pointer’,

        drop: function (event, ui) {

          _hasBeenDropped = true;

          valueAccessor().value(_dragged);

          ui.draggable.draggable(“option”, “revertDuration”, 0);

        }

      };

      dropElement.droppable(dropOptions);

    }

  };

})(jQuery, ko);

How to use it

In the example, the user will be sorting items, and when I think sorting, I think laundry. Go ahead and try it out

The elements on which you put the data-bind=”drag: {value: whatever}” can be done in a couple of ways:

Using an observable:

In the viewModel:

myArray: [

  { item: ko.observable({ name: “Willis” }) },

  { item: ko.observable({ name: “Arnold” }) },

  { item: ko.observable({ name: “Mr. D” }) }

],

In the html:

<ul data-bind=“foreach: myArray”>

  <li>

    <div data-bind=“drag: {value: item}”>

      <!– ko if:item –>

        <div data-bind=“text: name”></div>

      <!– /ko –>

    </div>

  </li>

</ul>

In this example, myArray doesn’t need to be an observable array unless items will be added to the list. The observables don’t even have to be in a list at all. This is the preferred way and allows for the most flexibility. If you want the space that the element occupied to remain after the element is ripped out (or you want control over that), use this method.  The jsfiddle example using this method will provide more details.

Using an obserableArray:

In the viewModel:

myArray: ko.observableArray([

  { name: “Willis” },

  { name: “Arnold” },

  { name: “Mr. D” }

]),

In the html:

<div data-bind=“foreach: myArray”>

    <div data-bind=“drag: {value: $data, array: $root.myArray}”>

        <div data-bind=“text: name”></div>

    </div>

</div>

If you use it in this way, the element that has the data-bind=”drag: …” needs to be the child of the element with the data-bind=”foreach: …”. If it is not, strange things happen when knockout re-renders the list. I have a laundry sorting jsfiddle example using this method, too, but still prefer using observables over observableArrays. You have to tell it what array the items are in (see the array: $root.myArray in the sample code above.). Sorry.

Conclusion

In my next post, I’ll put a bow on it and add a few more features, like data binding for other draggable and droppable properties. Maybe that’ll be cool.

2 responses to “Knockout + jQueryUI Draggable/Droppable Follow-up

  1. Pingback: Knockout + jQueryUI draggable/droppable – the end « Wilsonhut

  2. Pingback: Knockout + jQueryUI Draggable/Droppable « Wilsonhut

Leave a comment