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

Drop-down Selects with dgrid

In this tutorial, we will show you how to use one of dgrid's lightweight base classes to create a drop-down select component that will handle thousands of options.

Getting started

The first thing we will need is some HTML to represent the <select> element. We'll use the following HTML:

<div id="select" class="mySelect">
	<div class="label button">Label</div>
	<div class="arrow button"></div>
</div>

And we'll style it with the following CSS:

.mySelect {
	border: 1px solid #b5bcc7;
	background-color: #fff;

	width: 200px;
	height: 17px;

	/* Make this position: relative so our arrow is positioned within */
	position: relative;
	padding: 0;
}
.mySelect .label {
	line-height: 17px;
	vertical-align: middle;
}
.mySelect .arrow {
	/* Position the arrow on the right-hand side */
	position: absolute;
	top: 0;
	right: 0;

	/* Use claro's arrow image */
	background-image: url("path/to/dijit/themes/claro/form/images/commonFormArrows.png");
	background-position: -35px 70%;
	background-repeat: no-repeat;

	/* 16x16 with a white border and a gray background */
	width: 16px;
	height: 16px;
	border: 1px solid #fff;
	border-top: none;
	background-color: #efefef;
}
View Demo

Adding a drop-down

The next task will be to add the drop-down. For our needs, dgrid/OnDemandList will work well because it doesn't have a header and is only concerned with rendering a row — it has no concept of columns. We'll mix in dgrid/Selection to add selection support to our list.

require([
	'dojo/_base/declare',
	'dgrid/OnDemandList',
	'dgrid/Selection',
	'dojo/dom-construct',
	'dojo/domReady!'
], function (declare, List, Selection, domConstruct) {
	var DropDown = declare([ List, Selection ]);

	var dropDown = new DropDown();
	domConstruct.place(dropDown.domNode, 'select');
	dropDown.startup();
});
We'll also add some styles to give our grid appropriate positioning and height, sizing automatically based on the data up to a certain limit:
.mySelect .dgrid {
	cursor: default;
	display: none;
	position: absolute;
	top: 17px;
	left: -1px;
	height: auto;
	max-height: 20em;
	width: 100%;
}
.mySelect .dgrid-scroller {
	position: relative;
}
View Demo

You may have noticed that we have appended the list to the select's node rather than the document body. This is so we don't have to position and size the list manually when it is opened; instead, we're doing it with CSS.

Hiding and showing the drop-down

Let's modify our CSS to make the dgrid initially hidden and add a class that will show it when applied to the parent node:

.mySelect .dgrid {
	cursor: default;
	display: none;
	position: absolute;
	top: 17px;
	left: -1px;
	height: auto;
	max-height: 20em;
	width: 100%;
}
.mySelect .dgrid-scroller {
	position: relative;
}

.mySelect .opened {
	display: block;
}

Now all we need to do to show the list is to add the opened class to its node. Recall that we added a button class to both the label and arrow nodes. We can use dojo/on's event delegation to easily respond to clicks on both of these nodes:

require([
	'dojo/_base/declare',
	'dojo/on',
	'dgrid/OnDemandList',
	'dgrid/Selection',
	'dojo/dom',
	'dojo/dom-construct',
	'dojo/dom-class',
	'dojo/query',
	'dojo/domReady!'
], function (declare, on, List, Selection,
		dom, domConstruct, domClass) {
	var DropDown = declare([ List, Selection ]);

	var dropDown = new DropDown();
	domConstruct.place(dropDown.domNode, 'select');
	dropDown.startup();

	var open = false;
	on(dom.byId('select'), '.button:click', function (e) {
		open = !open;
		domClass.toggle(dropDown.domNode, 'opened', open);
	});
});
View Demo

Adding items to the drop-down

Now that we have the basic behavior working, it's time that we add some items to our drop-down. This is accomplished by assigning a collection and a renderRow function to the list:

require([
	'dojo/_base/declare',
	'dojo/on',
	'dgrid/OnDemandList',
	'dgrid/Selection',
	'dstore/Memory',
	'dojo/dom',
	'dojo/dom-construct',
	'dojo/dom-class',
	'dojo/query',
	'dojo/domReady!'
], function (declare, on, List, Selection,
		Memory, dom, domConstruct, domClass) {
	var DropDown = declare([ List, Selection ]);

	var store = new Memory({
		idProperty: 'id',
		data: [
			{ id: 0, name: 'One', value: 1 },
			{ id: 1, name: 'Two', value: 2 },
			{ id: 2, name: 'Three', value: 3 },
			{ id: 3, name: 'Four', value: 4 }
		]
	});

	var dropDown = new DropDown({
		selectionMode: 'single',
		collection: store,
		renderRow: function (item) {
			var divNode = domConstruct.create('div', {
				className: item.name
			});
			domConstruct.place(document.createTextNode(item.name),
				divNode);
			return divNode;
		}
	});
View Demo

Unlike dgrid/Grid, dgrid/List (and by inheritance, dgrid/OnDemandList) has no concept of columns and only cares about rendering rows. The renderRow function receives a store item and needs to return a DOM node; in our case, it's simply a <div> with the name of the item as its contents and a CSS class added based on the item's name property. You can get as creative as you'd like with the rendering of your items.

Selecting items

We have items rendering, but nothing happens when we select them. The next step is to hook up the list's dgrid-select event, hide the list on selection, and update the label node with the text from the item. In our case, we want the text in the label to match what is in the drop-down. We'll need to break out some of the functionality we've written into separate functions so we can re-use it:

function renderItem(item) {
	var divNode = domConstruct.create('div', {
		className: item.name
	});
	domConstruct.place(document.createTextNode(item.name), divNode);
	return divNode;
}

var dropDown = new DropDown({
	selectionMode: 'single',
	collection: store,
	renderRow: renderItem
});
domConstruct.place(dropDown.domNode, 'select');
dropDown.startup();

var open = false;
function toggle(state) {
	open = typeof state !== 'undefined' ? state : !open;
	domClass.toggle(dropDown.domNode, 'opened', open);
}
on(dom.byId('select'), '.button:click', function () {
	toggle();
});

var label = query('.label', dom.byId('select'))[0];
dropDown.on('dgrid-select', function (event) {
	var node = renderItem(event.rows[0].data);
	domConstruct.place(node, label, 'only');
	toggle(false);
});
View Demo

To be notified when a row is selected, we must listen for the dgrid-select event; the event object will have a rows property which is an array of the row objects that are currently selected. Since the list is set up in single selection mode, there will only ever be one item in the array. We use the data property from the row object to render a node and then replace the contents of the label node with that node. Finally, the list is toggled closed.

One more exercise that can be undertaken is to widgetize this component: as it stands, it's not very reusable. This can be done many different ways, but here's an example to get you started:

define([
	'dojo/_base/declare',
	'dojo/on',
	'dojo/dom',
	'dojo/dom-construct',
	'dojo/dom-class',
	'dijit/_WidgetBase',
	'dijit/_TemplatedMixin',
	'dgrid/OnDemandList',
	'dgrid/Selection',
	'dojo/text!./Select.html'
], function (declare, on, dom, domConstruct, domClass,
		_WidgetBase, _TemplatedMixin, List, Selection, template) {
	var DropDown = declare([ List, Selection ]);

	return declare([ _WidgetBase, _TemplatedMixin ], {
		baseClass: 'mySelect',

		name: '',
		value: null,

		templateString: template,

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

			this.list = new DropDown({
				selectionMode: 'single',
				showHeader: false,
				collection: this.collection,
				renderRow: this.renderItem
			}, this.listNode);

			var self = this;
			this.on('.button:click', function () {
				self.toggle();
			});

			this.list.on('dgrid-select', function (event) {
				self.set('value', event.rows[0].id);
				self.toggle(false);
			});

			this.own(this.list);
		},

		startup: function () {
			if (this._started) {
				return;
			}

			this.inherited(arguments);

			if (!this.value && this.collection) {
				this.set('value',
					this.collection.getIdentity(this.collection.fetch()[0]));
			}
		},

		_setValueAttr: function (value) {
			var item = this.collection.get(value);
			if (item) {
				domConstruct.place(this.renderItem(item),
					this.labelNode, 'only');
				this._set('value', value);
			}
		},

		toggle: function (state) {
			if (typeof state === 'undefined') {
				state = !this.opened;
			}
			this.opened = state;

			domClass.toggle(this.list.domNode, 'opened', this.opened);
			if (state && !this.list._started) {
				this.list.startup();
			}
		},

		renderItem: function (item) {
			var divNode = domConstruct.create('div', { className: item.name });
			domConstruct.place(document.createTextNode(item.name), divNode);
			return divNode;
		}
	});
});
View Demo

Conclusion

As you can see, using dgrid/OnDemandList as a drop-down list is quite simple. The modular nature of dgrid allows us to pick and choose which features to include in our application without adding any more than we absolutely need.

Resources