
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">></button> <button type="button" data-dojo-type="dijit.form.Button" data-dojo-attach-event="onClick:remove" data-dojo-attach-point="removeButton"><</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.
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.
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 DemoAdding 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.
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.
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 DemoConclusion
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.