Dojo Drag’n’Drop Redux

By on December 5, 2011 1:33 pm

dojo/dnd is one of Dojo’s core APIs and is designed to manage the process of dragging and dropping items between two or more containers. It offers advanced features like multiple selections, item acceptance filtering on drop targets, and other behavioral tweaks. Let’s learn how to use it!

This is an updated version of the original article, Dojo Drag and Drop, Part 1, published in 2008.

Simple Single Source DnD

Meet Dylan. Collecting junk is his passion. A while back, he decided that he needed to get rid of some of it to make room for more interesting junk, so he got a booth at the local farmers’ market and started a small junk outlet business. Like most people passionate about sharing their junk with the world, he decided to open an online storefront. Dylan’s Original Junk Outlet His brother, currently working towards a degree in Marketing, convinced him that he needed to brand himself as a form of differentiation; thus was born Dylan’s Original. He decided that his best bet would be to create a user experience so ridiculously awesome that people wouldn’t be able to help buying his junk. That’s where we come in. To demonstrate drag and drop techniques, we’ll help build a mockup of Dylan’s Original Junk Outlet.

Let’s start with the basics. Just about the easiest way to get drag and drop working is to demonstrate a single list that the user can reorder dynamically. First, we’ll create our page skeleton, using Dojo from the Google CDN spiced up with a bit of CSS. View the starting point.

As you can see, we’re starting with a simple wish list:

<div id="store">
<div class="wishlistContainer">
<h2>Wishlist</h2>
<ol id="wishlistNode" class="container">
	<li>Wrist watch</li>
	<li>Life jacket</li>
	<li>Toy bulldozer</li>
	<li>Vintage microphone</li>
	<li>TIE fighter</li>
</ol>
</div>
</div>

The DnD Workhorse, dojo/dnd/Source

An online store wishlist
To enable drag and drop, dojo/dnd gives us a class called Source, which is basically just what it sounds like: a source for dragged items (as well as a target for dropped items). To instantly turn a DOM node into such a source, create a dojo/dnd/Source out of it:

require([ "dojo/dnd/Source", 
	"dojo/domReady!" ],
    function(Source){
    var wishlist =
        new Source("wishlistNode");
    wishlist.insertNodes(false, [
        "Wrist watch",
        "Life jacket",
        "Toy bulldozer",
        "Vintage microphone",
        "TIE fighter"
    ]);
});

That’s all there is to it! View the single container DnD example. If you like to create UIs declaratively, instantiate the list in markup with data-dojo-type="dojo.dnd.Source" and use class="dojoDndItem" on draggable child nodes, like so:

<ol data-dojo-type="dojo.dnd.Source" id="wishlistNode" class="container">
	<li class="dojoDndItem">Wrist watch</li>
	<li class="dojoDndItem">Life jacket</li>
	<li class="dojoDndItem">Toy bulldozer</li>
	<li class="dojoDndItem">Vintage microphone</li>
	<li class="dojoDndItem">TIE fighter</li>
</ol>

Of course, when working declaratively, you have to make sure to add dojo/parser and dojo/dnd/Source to the loader’s deps list and enable parseOnLoad, but there you go. View the demo, declarative style.

What can you turn into a DnD source? Well sheesh, what can’t you turn into a DnD source? Take a look at the reference guide. The Source class will take into account the node type of your container when creating child nodes:

  • If the container is <div> or <p>, it will create <div> nodes.
  • If the container is <ul> or <ol>, it will create <li> nodes.
  • If the container is a <table>, it will create a set of <tr><td> and add it to the table’s <tbody>.
  • All other times, it will create <span> nodes.

So basically, turn whatever you want into a Source, and Dojo will intelligently set up your DOM. Pretty nifty! Out of the box, dojo/dnd/Source has quite a lot of functionality baked in:

  • Multiple selection. Each container has the notion of a selection; click on an item and it’s “selected”. ⌘-click/ctrl-click or shift-click do multiple selection, just like in a regular application.
  • Child node introspection. In addition to the insertNodes method demonstrated above, the Source provides a few methods to work with the list of child nodes:
    • getAllNodes() – returns a Dojo NodeList of the contained items.
    • forInItems(fn, ctx) – calls fn in the context of ctx for each contained node. Similar to dojo.forEach.
    • selectNone(), selectAll(), getSelectedNodes(), deleteSelectedNodes() – just what they sound like: methods for manipulating the selection state.
    • plus a few other things you can hook into for customizing the way the internal list gets handled. See the reference guide for details.
  • Copy vs. move semantics. By default, nodes are moved when you drag them around. However, holding ⌘/ctrl and dragging performs a copy operation instead, similar to most desktop file managers. This is useful when you don’t want your DnD source to change in response to a user’s drag operations.
  • Drag cancellation. This isn’t technically a property of the Source, but it’s worth noting here that pressing the Esc key cancels the current drag operation. You can do this programmatically, too, if you need to.
  • Automatic avatar creation. The dojo/dnd framework uses “avatars” to represent the nodes you drag around. It creates these for you automatically, based on the data itself. You can customize this, of course; more on that later.

An online store product list, mid-drag

Using Multiple Sources

Of course, if you’re only using a single Source in your application, the move/copy distinction is only useful for duplicating nodes in the list. Let’s help Dylan expand. Check out Dylan’s Original Junk Outlet, version 2 (and the declarative version).

What have we changed? Well, for starters, we now have three Sources: the Catalog, the Cart, and the Wishlist. Now you can drag items back and forth between them to see multiple-container dojo/dnd in action. Some items are marked as “out of stock” (more on this in a bit), and… hey, some of these items aren’t junk at all—they’re food! Yes, while we weren’t looking, Dylan merged his junk outlet with Dylan’s Nutritious Dietarium, the company he uses to unload what he doesn’t eat from his garden.

DnD Item Types

The biggest change here is the introduction of item types. Notice the new accept and type properties:

require([ "dojo/dom-class", "dojo/dnd/Source", "dojo/domReady!" ], 
		function(domClass, Source){
	var catalog = new Source("catalogNode", 
		{ accept: [ "inStock", "outOfStock" ] });
	catalog.insertNodes(false, [
		{ data: "Wrist watch",        type: [ "inStock" ] },
		{ data: "Life jacket",        type: [ "inStock" ] },
		{ data: "Toy bulldozer",      type: [ "inStock" ] },
		{ data: "Vintage microphone", type: [ "outOfStock" ] },
		{ data: "TIE fighter",        type: [ "outOfStock" ] },
		{ data: "Apples",             type: [ "inStock" ] },
		{ data: "Bananas",            type: [ "inStock" ] },
		{ data: "Tomatoes",           type: [ "outOfStock" ] },
		{ data: "Bread",              type: [ "inStock" ] }
	]);
	catalog.forInItems(function(item, id, map){
		domClass.add(id, item.type[0]);
	});

	var cart = new Source("cartNode", { accept: [ "inStock" ] });
	var wishlist = new Source("wishlistNode", 
		{ accept: [ "inStock", "outOfStock" ] });
});

In the declarative version, it looks like this:

<div class="catalogContainer">
    <h2>Catalog</h2>
    <ul data-dojo-type="dojo.dnd.Source" id="catalogNode" class="container"
		data-dojo-props="accept: [ 'inStock', 'outOfStock' ]" >
      <li class="dojoDndItem inStock" dndType="inStock">Wrist watch</li>
      <li class="dojoDndItem inStock" dndType="inStock">Life jacket</li>
      <li class="dojoDndItem inStock" dndType="inStock">Toy bulldozer</li>
      <li class="dojoDndItem outOfStock" dndType="outOfStock">
		Vintage microphone</li>
      <li class="dojoDndItem outOfStock" dndType="outOfStock">
		TIE fighter</li>
      <li class="dojoDndItem inStock" dndType="inStock">Apples</li>
      <li class="dojoDndItem inStock" dndType="inStock">Bananas</li>
      <li class="dojoDndItem outOfStock" dndType="outOfStock">
		Tomatoes</li>
      <li class="dojoDndItem inStock" dndType="inStock">Bread</li>
    </ul>
</div>

<div class="cartContainer">
    <h2>Cart</h2>
    <ol data-dojo-type="dojo.dnd.Source" id="cartNode" class="container"
		data-dojo-props="accept: [ 'inStock' ]" >
    </ol>
</div>
<div class="wishlistContainer">
    <h2>Wishlist</h2>
    <ol data-dojo-type="dojo.dnd.Source" id="wishlistNode"
		data-dojo-props="accept: [ 'inStock', 'outOfStock' ]"
		class="container">
    </ol>
</div>

Each DnD item can be given one or more types. In JavaScript, this is done by setting the type property of any object you pass to insertNodes. In markup, a comma-separated list of strings in the non-standard dndType attribute is used. Correspondingly, each DnD container can be given a list of item types to accept in the accept property. If left unspecified, the default type for all items and containers is “text”.

Here, we’re using the DnD type to denote whether an item is in stock or not, which then determines which items can be dropped where. The Cart only accepts items that are in stock, while the Wishlist accepts anything. If you drag around multiple items at once, you’ll notice that you can only drop a set of items on a container that accepts every type of item in the set—no partial drops allowed!

This is a good start, but there are still a few issues with this demo that need to be addressed:

  • Unless you explicitly invoke copy semantics by pressing the appropriate key, dragging items removes them from the catalog, which doesn’t make much sense for this application.
  • You can do a copy/drag, but then it becomes easy to duplicate items, which shouldn’t be possible.
  • Using simple lists like this doesn’t really give a great user experience for an establishment as dignified as Dylan’s Original Junk Outlet / Dylan’s Nutritious Dietarium (DOJO/DND, get it? I kill me (groan)).

Let’s start with improving the appearance.

Customizing Item Creation

As was discussed above, the default drag and drop implementation is intelligent enough to create nodes according to the context in the DOM. However, if you want to display more than a string of text, the default can be lacking, since all it does is set the child node’s innerHTML property to whatever is assigned to data. Fortunately, dojo/dnd gives us a way to customize this: the creator function.

Since Dylan wants the product catalog to be both prettier and more informative, let’s give each item an image, short description, and quantity indicator. Our data structures will look like this:

{
    name: "Wrist watch",
    image: "watch.jpg",
    description: "Tell time with Swiss precision",
    quantity: 3
}

Instead of a string, we’ll pass these objects in the data property of the object passed to insertNodes. To create visual representations of this object, we’ll need a function we can pass to the dojo/dnd/Source’s constructor to transform it into a DOM node:

define([ "dojo/string", "dojo/dom-construct", "dojo/dom-class", 
		"dojo/dnd/Source", "dojo/text!./itemTemplate.html", 
		"dojo/text!./avatarTemplate.html" ], 
		function(stringUtil, domConstruct, domClass, Source, 
			template, avatarTemplate){
	// create the DOM representation for the given item
	function catalogNodeCreator(item, hint){
		var node = domConstruct.toDom(stringUtil.substitute(
			hint === "avatar" ? avatarTemplate : template, {
				name: item.name || "Product",
				imageUrl: "images/" + (item.image || "_blank.gif"),
				quantity: item.quantity || 0,
				description: item.description ? "<br><span>" 
					+ item.description + "</span>" : ""
			})),
			
			type = item.quantity ? [ "inStock" ] : [ "outOfStock" ];

		return { node: node, data: item, type: type };
	}
});

Our item template looks like this:

<tr>
	<td class="itemImg dojoDndHandle"><img src="${imageUrl}"></td>
	<td class="itemText">${name} ${description}</td>
	<td class="itemQty">${quantity}</td>
</tr>

Our avatar template looks like this:

<table>
	<tr>
		<td class="itemImg"><img src="${imageUrl}"></td>
		<td class="itemText">${name}</td>
	</tr>
</table>

Items of note from this code:

  • We’re going to be using tables for our DnD sources now to help improve the presentation.
  • We’re dynamically choosing the DnD item type based on the quantity provided.
  • The creator function also accepts a second optional parameter, hint. When this is set to “avatar”, we’re being asked to create a DOM representation of the avatar, so our function takes that into account. For us, this means that we skip displaying the description and quantity when we make an avatar, and we put the entire avatar into its own table, since the default node that contains the avatar(s) is itself a table (and we don’t want to ruin our DOM).

An online store product catalog

At this point, we can introduce version 3 of the demo (we’re doing it all programmatically from here on out, so there’s no declarative version). There’s a substantial overhaul in the appearance now thanks to our table-based DnD sources. We’ve also put the wishlist and shopping cart into a couple of dijit/TitlePanes so we can easily toggle their visibility. This change demonstrates a couple of concepts:

Creating Pure Targets

var cart = new Target("cartPaneNode", { accept: [ "inStock" ] });

We have a new class here: dojo/dnd/Target. This is just a thin wrapper around dojo/dnd/Source that sets this.isSource to false, making it a pure target. You can drop items on it, but can’t drag them back out again! Incidentally, you can freely manipulate this field at runtime on a regular Source object; we’ll do that later on.

Changing the “Drop Parent”

cart.parent = dom.byId("cartNode");

Here, we’re using the dojo/dnd/Source’s parent property to change which element dragged items are actually inserted into. In this case, we’re changing so that when users drop items on the TitlePane, they’ll actually be inserted to the enclosed <table id="cartNode">.

Our store is starting to look pretty good, but there are still some other things we can do to demonstrate a few more concepts.

Handling Events

The drag and drop framework uses Dojo’s pub/sub system to handle event communication. We can polish up a few things if we take advantage of this. For example, it would be nice if we could show the number of items in the wishlist and cart when they’re closed so users don’t need to open them to check. It would also clean things up visually if we cleared the selection states of our containers when we drop an item. By listening for topics, we can do both.

Building a Drop Handler

Let’s hook into the drop notification. We can either listen for the DnD pub/sub topics directly or we can connect to event handlers on DnD objects depending upon what sort of action we are performing.

// sets the count of items in a TitlePane
function setListCount(){
	query(".count", this.node)[0].innerHTML = this.getAllNodes().length;
}

// update the cart’s displayed item count when dropped on
aspect.after(cart, "onDrop", setListCount);

// update the wishlist’s displayed item count when dropped on
aspect.after(wishlist, "onDrop", setListCount);

Here, we connect to the cart’s and wishlist’s onDrop event to update the number of items in the respective list when an item is dropped. Unlike onDndDrop, which fires for all DnD elements on the page regardless of whether or not they were dropped on, the onDrop event only fires for the target of the drop, so is suitable when you only want to perform an action on a single target.

Listening Directly to the Topics

Since I mentioned that you can subscribe to the topics yourself when it makes sense, let’s cook up an example. When you start dragging items around, it’s not completely intuitive where you’re allowed to drop them; you have to keep dragging until the avatar turns green. On top of that, there’s no immediate feedback that your drop was successful. We can create a better experience than that. First, we create some functions to highlight valid targets:

// highlights available drop targets
function highlightTargets(show, source, nodes){
	domClass.toggle("wishlistPaneNode", "highlight", show);
	domClass.toggle("cartPaneNode", "highlight", 
			show && arrayUtil.every(nodes, function(node){
		return domClass.contains(node, "inStock");
	}));
}

// glows the target of a drop
function glowTarget(source, nodes, copy, target){
	domClass.add(target.node, "glow");
	setTimeout(lang.hitch(domClass, "remove", 
		target.node, "glow"), 1000);
}

Then, in our initialization function, we hook them up to the DnD system:

// highlight valid drop targets when a drag operation is started
// (/dnd/start)
topic.subscribe("/dnd/start", lang.partial(highlightTargets, true));

// remove the highlight when a drag operation is finished 
// (/dnd/cancel or /dnd/drop)
topic.subscribe("/dnd/cancel, /dnd/drop", 
	lang.partial(highlightTargets, false));

// give the target of a drop an extended glow
topic.subscribe("/dnd/drop", glowTarget);

Since we’re listening to the topic broadcast itself, we know these functions will only run once per event. Manipulating a bit of CSS with dojo/dom-class helps make the drag and drop system a bit friendlier for users, and that’s always a good thing.

Armed with knowledge of how to run code at various times during the drag and drop lifecycle, we can see that it wouldn’t be difficult to extend this further. For example, our item quantities currently only determine whether or not you can drop something on the shopping cart; they could easily be updated when they are dropped on the Cart (but not the Wishlist!). And of course, this little storefront doesn’t have any prices! In a real store you’d want to add that, and probably upgrade setupCartTitle to calculate and display a subtotal.

Avoiding Duplicate Items

One thing I hadn’t pointed out before was that in version 3 of our demo, in addition to specifying the node creator function when instantiating our Source objects, we also passed a parameter copyOnly: true. This overrides the default drag semantics and performs a copy operation by default instead of a move. This is nice because it means we can avoid removing items from the catalog(s) when we drag them around. The downside is that if you copy an item on the container where it already lives, it duplicates the item.

Logically, this shouldn’t happen because we’re not specifying the accept type on the catalogs, so they should default to "text" and keep us from dropping the products on them (we’re explicitly giving them different types, remember). If you dig into the Dojo source, however, you’ll see that the function that checks for matches between item types and container accept values automatically accepts “self drops”, short circuiting the item type check. Often, that’s the correct behavior, but for our copyOnly–style DnD here, this is backwards. Fortunately again, overriding this is easy: just set the selfAccept property to false.

Take a look at version 4 of our demo to see it all working together. Some of the code has been reorganized and/or moved to an external file to reduce clutter, but the new stuff is all there if you view the page source.

An online storefront

Tweaking DnD Behavior

The final thing left to talk about for this demo is the set of buttons we introduce in version 4. We have buttons to clear the wishlist and shopping cart now, but notice the buttons at the bottom of the page: these demonstrate different ways to change the way DnD behaves. You can read the code to see how they work, but here’s what they do:

  • Enable DnD (sets isSource) – If you’ve ever wondered how to turn DnD on and off completely, here’s one way. This toggles the isSource member of each of our sources. I mentioned this earlier, but recall that when this is false, the manager won’t initiate any drag operations. Objects will still accept drops, but if there’s nothing acting as a source, we’ve effectively disabled the DnD system.
  • Drag via handles only (sets withHandles) – If you give a DOM node the dojoDndHandle class, dojo/dnd will consider it a handle. Each Source has a member variable withHandles that determines whether you can drag any part of an item, or just the handle. This demo sets up the product images as the handles, so if you toggle the button, you’ll see the drag behavior change accordingly.

Finishing Up

For reference, here are the steps we’ve taken so far:

There’s quite a lot left to discuss in dojo/dnd that we haven’t touched on. For example, there’s been little discussion about the CSS classes that are used by DnD (you can see what this demo uses in dnd.css). We also didn’t discuss how to cleanly set up your own custom DnD sources by extending dojo/dnd/Source using dojo/_base/declare. A lot of dojo/dnd’s internals are specifically set up to be easy to override with your own code so you can customize just about any part necessary. These are all areas that you should explore on your own to get a feel for how DnD can be useful in your own projects. You can download an archive of the entire demo so you can play with it yourself locally.

Happy dragging and dropping!

Comments

  • Mark

    Has DND been updated to properly hand touch? From memory in 1.6 it could only handle the drag side of things, not the drop. We got it working nicely for touch using a modified dojox/mdnd but wondering what the long term solution is

    Cheers

  • Christoph

    Nice tutorial!

    The third code snippet misses the data-dojo-type=”dojo.dnd.Source” attribute in the ol tag.

    The images of the items in the wishlist and shopping cart still act as handles (the cursor changes) but the items can’t be moved. How can this be inhibited?

  • Colin Snover

    @Mark – There has been some work to improve dnd support on touch devices in 1.7, but it is not 100% there yet. Improved mobile user experience is a focus for 1.8.

    @Christoph – Thanks for the note; that snippet has been fixed. With regards to your question about inhibiting the cursor display, that’s all handled by CSS, so the easiest way is to add some class to your <body> element like “dndDisabled” when you disable DnD, then override .dndDisabled .dojoDndHandle and .dndDisabled .dojoDndItemOver pointer styles.

  • Shabeer

    Hi,

    Is it possible to add a grid, instead of simple span to cart, so that each row can contain with item name, quantity, price and delete icon on it?

    Any hints on this??

  • @Shabeer: I’d suggest taking a look at dgrid. I believe Chris Barrett has an example he did for the Web-5 conference that might be similar to what you’re trying to do.

  • Sunny

    Hello,

    I am new to Dojo, and this blog has been *very* helpful to me. Thank you for all the great work!

    Related to the dnd example shown above, if my multiple dnd sources span several pages, how do I record the data in the shopping cart (or the wishlist) so that the items in it do not disappear after loading different pages and users can continue to drop more items? Any hints would be greatly appreciated!

  • Hi!

    I have a ContentPane containing tags. Can I convert this to a dnd Source?
    I’ve been trying to follow this article, but didn’t taste any success. :(

    Very well written article though, otherwise!

    Cheers,
    S

  • Hi!

    I have a ContentPane containing “” tags. Can I convert this to a dnd Source?
    I’ve been trying to follow this article, but didn’t taste any success. :(

    Very well written article though, otherwise!

    Cheers,
    S

  • Hi!

    I have a ContentPane containing img tags. Can I convert this to a dnd Source?
    I’ve been trying to follow this article, but didn’t taste any success. :(

    Very well written article though, otherwise!

    Cheers,
    S

  • Hey,

    In the node creator method, can the ‘node’ be a custom widget? Something like :

    var node = hint === “avatar” ? domConstruct.toDom(stringUtil.substitute(avatarTemplate, data)): new CustomWidget(data);

    Cheers,