Notice: This is a tutorial for a previous version of dgrid and may be out of date.

TransferBox: A Tale of Two Lists

In this tutorial, we will show you how to use two instances of one of dgrid's lightweight base classes to create a transfer box widget that will handle thousands of options.

Getting started

We're going to start out with some boilerplate and we'll modify it as we go along. First, we're going to create some fake data, instantiate an observable store, and instantiate our TransferBox widget. We'll do all of this in the main page:

var transferbox;
require([
	"my/TransferBox",
	"dojo/store/Memory",
	"dojo/store/Observable",
	"dojo/domReady!"
], function(TransferBox, Memory, Observable){
	var data = [];
	for(var i=0; i<100; i++){
		data[i] = {
			id: i,
			name: "" + (i+1),
			value: i+1
		};
	}

	var store = Observable(new Memory({
		identifier: "id",
		data: data
	}));

	transferbox = new TransferBox({
		store: store
	}, "transferbox");
});

Our widget will use a template for its layout. In the template, we'll put two nodes where we will attach our lists; in between those nodes, we'll place a node with two buttons which will be used to swap items between the two lists:

<div>
	<div data-dojo-attach-point="fromNode"></div>
	<div class="dijitReset dijitInline buttons">
		<button type="button" data-dojo-type="dijit.form.Button"
			data-dojo-attach-event="onClick:add"
			data-dojo-attach-point="addButton">&gt;</button>
		<button type="button" data-dojo-type="dijit.form.Button"
			data-dojo-attach-event="onClick:remove"
			data-dojo-attach-point="removeButton">&lt;</button>
	</div>
	<div data-dojo-attach-point="toNode"></div>
</div>

Some simple styling will align the lists and buttons how we would like them:

.myTransferBox .dgrid {
	width: 200px;
	height: 200px;

	/* Taken from rule for dijitInline */
	display: inline-block;
	#zoom: 1;
	#display: inline;
	vertical-align: middle;
	#vertical-align: auto;
}

.myTransferBox .buttons {
	width: 27px;
}

Finally, the widget boilerplate. It's a very simple templated widget that initially disables the buttons once they have been instantiated from the template:

define([
	"dojo/_base/declare",
	"dijit/_Widget",
	"dijit/_TemplatedMixin",
	"dijit/_WidgetsInTemplateMixin",
	"dgrid/OnDemandList",
	"dgrid/Selection",
	"dgrid/Keyboard",
	"dojo/text!my/TransferBox.html",
	"dijit/form/Button"
], function(declare, _Widget, _TemplatedMixin, _WidgetsInTemplateMixin,
			List, Selection, Keyboard, template){
	return declare([_Widget, _TemplatedMixin, _WidgetsInTemplateMixin], {
		templateString: template,
		baseClass: "myTransferBox",

		postCreate: function(){
			this.inherited(arguments);

			this.addButton.set("disabled", true);
			this.removeButton.set("disabled", true);
		},
		add: function(){},
		remove: function(){}
	});
});
View Demo

Adding the lists

The first thing we need to do is instantiate our lists. We're going to use dgrid/OnDemandList with the dgrid/Selection and dgrid/Keyboard modules mixed in to add selection and keyboard navigation:

define([
	"dojo/_base/declare",
	"dijit/_Widget",
	"dijit/_TemplatedMixin",
	"dijit/_WidgetsInTemplateMixin",
	"dgrid/OnDemandList",
	"dgrid/Selection",
	"dgrid/Keyboard",
	"dojo/dom-construct",
	"dojo/text!my/TransferBox.html",
	"dijit/form/Button"
], function(declare, _Widget, _TemplatedMixin, _WidgetsInTemplateMixin,
			List, Selection, Keyboard, domConstruct, template){
	var TBList = declare([List, Selection, Keyboard]);
	return declare([_Widget, _TemplatedMixin, _WidgetsInTemplateMixin], {
		templateString: template,
		baseClass: "myTransferBox",

		postCreate: function(){
			var self = this;
			this.inherited(arguments);

			this.addButton.set("disabled", true);
			this.removeButton.set("disabled", true);

			var from = this.from = new TBList({
				store: this.store,
				renderRow: this.renderItem
			}, this.fromNode);

			var to = this.to = new TBList({
				store: this.store,
				renderRow: this.renderItem
			}, this.toNode);
		},
		add: function(){},
		remove: function(){},
		renderItem: function(item){
			return domConstruct.create("div", {
				innerHTML: item.name
			});
		}
	});
});

Since dgrid/OnDemandList has no concept of columns, it only is concerned with rendering items to rows; anything inheriting from dgrid/List (including dgrid/OnDemandList) uses the renderRow function to render data items to DOM nodes. Our renderItem method takes care of that for us.

View Demo

Enabling buttons with selection

The next step to create this widget will be to associate the disabled status of the two buttons with the selection of its respective list. The dgrid-select and dgrid-deselect events will be our focus for this step. Both events decorate the event object with a rows array corresponding to the rows that were selected (or deselected) by the action that caused the event. This is important because not all of the selected or deselected rows will be in the rows array — only those that were selected or deselected by the action generating the event. This means that we will have to keep track of how many rows are currently selected to set the disabled state of our button:

function selectionToDisable(list, button){
	var selected = 0;
	list.on("dgrid-select", function(e){
		selected += e.rows.length;
		button.set("disabled", !selected);
	});
	list.on("dgrid-deselect", function(e){
		selected -= e.rows.length;
		button.set("disabled", !selected);
	});
}

var from = this.from = new TBList({
	store: this.store,
	renderRow: this.renderItem
}, this.fromNode);
selectionToDisable(from, this.addButton);

var to = this.to = new TBList({
	store: this.store,
	renderRow: this.renderItem
}, this.toNode);
selectionToDisable(to, this.removeButton);

Rather than duplicate code, selectionToDisable sets up our event handlers for us and keeps track of the number of items selected. We then associate each list with its corresponding button.

View Demo

Limiting results

One thing you may have noticed is that both lists contain all items from the store. Obviously this is not the desired behavior. If you remember from the page this will be instantiated on, we're using an Observable store; this means result sets are observable and will be notified of changes to items. Because of this fact, we are going to use an internal property (__selected) on items to signify if they are in the right hand list or not. To make the lists aware of this property, we need to pass a query to our list instances:

var from = this.from = new TBList({
	store: this.store,
	query: function(item){
		return !item.__selected;
	},
	renderRow: this.renderItem
}, this.fromNode);
selectionToDisable(from, this.addButton);

var to = this.to = new TBList({
	store: this.store,
	query: function(item){
		return item.__selected;
	},
	renderRow: this.renderItem
}, this.toNode);
selectionToDisable(to, this.removeButton);

The end result here is that items that are not "selected" will display in the left hand list, while items that are "selected" end up in the right hand list.

View Demo

Adding and removing items

Now that items are displaying in the correct lists, we need to write the code to add and remove items from the right hand list; the functions are already hooked up to the buttons from the template, but the code needs to be added. There are two things we are going to have to do in order to accomplish this: retrieve the items that have been selected in the list and set the __selected property to either true or false:

add: function(){
	for(var id in this.from.selection){
		var row = this.from.row(id);
		row.data.__selected = true;
		this.store.put(row.data);
	}
},
remove: function(){
	for(var id in this.to.selection){
		var row = this.to.row(id);
		row.data.__selected = false;
		this.store.put(row.data);
	}
},

Since we mixed dgrid/Selection into our list constructor, each list has an object with properties representing the ID of each item selected. By using List#row, we can retrieve a representation of the row, which includes a data property referencing the store item. From there, we set __selected to the proper value and use the store's put method to apply the change.

View Demo

Proper placement

If you played around with the previous demo, you might have noticed that after removing items from the right hand list, they don't return to the same place in the left hand list as they were before. In fact, they will be added back to the left hand list after the last rendered row (dgrid/OnDemandList only renders what has been scrolled to). In order to get items to return to their former position in the left hand list, we'll need to tell the lists to request sorted data from the store via the queryOptions.sort parameter:

var from = this.from = new TBList({
 	store: this.store,
 	query: function(item){
 		return !item.__selected;
 	},
 	queryOptions: {
 		sort: [{ attribute: "id" }]
 	},
 	renderRow: this.renderItem
 }, this.fromNode);
 selectionToDisable(from, this.addButton);

 var to = this.to = new TBList({
 	store: this.store,
 	query: function(item){
 		return item.__selected;
 	},
 	queryOptions: {
 		sort: [{ attribute: "id" }]
 	},
 	renderRow: this.renderItem
 }, this.toNode);
 selectionToDisable(to, this.removeButton);

queryOptions.sort is an array of objects detailing what properties to sort by. In our case, we only need to sort by the "id" property. Now the items of our lists return to their proper order.

View Demo

While this simple widget does what we set out to accomplish, it isn't very reusable: the selection mode of the lists is static, the property to sort on is hard-coded, and there's no way to easily get or set the value of the widget. These functions can be accomplished many different ways, but an example has been provided to get you started:

View Demo

Conclusion

As you can see, using two instances of dgrid/OnDemandList to create a transfer box widget is a bit involved, but simple once you know all of the pieces that need to be used. The modular nature of dgrid allows us to use a slimmed down version of a grid (a list) instead of the full-fledged grid, which simplifies the implementation immensely.

Resources