Friday, June 22, 2012

Beware of Dojo Selects, Stores, and numeric IDs

A handy feature of Dijit Select (or an extending widget such as FilteringSelect or ComboBox) is the integration with Dojo Data stores. You can populate your Select/FilteringSelect/ComboBox with any data that can be accessed through a Dojo Data store, including JSON REST requests, HTTP queries, CSV files, or even Wikipedia.

However, there is a limitation to this that has caused me problems, even though it's documented and I've run into it a couple times: Selects do not play nicely with stores that have non-string IDs (like integers). To quote from a Dojo tutorial:
dijit.form.Select possesses an important limitation: it is implemented in such a way that it does not handle non-string item identities well. Particularly, setting the current value of the widget programmatically via select.set("value", id) will not work with non-string (e.g. numeric) identities.
To drive this point home, I'll give a concrete example illustrating the problem. You can also see the example in action on OrionHub or download the source code and run it yourself.

The goal

We're going to create a pair of Selects that will display a list of ships: one backed by a store with String IDs, and one backed by a store with numeric IDs. The generated page will look like this:


The data

Our store will be a dojo/data/ItemFileReadStore, which will load JSON from a given file. The String ID JSON looks like this:
{
    "identifier": "shipId",
    "label": "shipName",
    "items": [
     { "shipId": "1", "shipName": "Constitution" },
        { "shipId": "2", "shipName": "Enterprise" }, 
        // You get the point...
        { "shipId": "6", "shipName": "Yorktown" }
    ]
}
And the numeric ID JSON looks like this:
{
    "identifier": "shipId",
    "label": "shipName",
    "items": [
     { "shipId": 1, "shipName": "Constitution" },
        { "shipId": 2, "shipName": "Enterprise" }, 
        // You get the point...
        { "shipId": 6, "shipName": "Yorktown" }
    ]
}

The HTML

The HTML is pretty uninteresting; it basically loads the JavaScript files and throws in some DIV placeholders for the widgets:
<html>
 <head>
  <link rel="stylesheet"
   href="http://ajax.googleapis.com/ajax/libs/dojo/1.7/dijit/themes/claro/claro.css">
  
  <script src="//ajax.googleapis.com/ajax/libs/dojo/1.7.2/dojo/dojo.js"
   data-dojo-config="async: true"></script>
  <script src="//ajax.googleapis.com/ajax/libs/dojo/1.7.2/dijit/dijit.js"
   data-dojo-config="async: true"></script>
  <script src="selectStoreExample.js"
   data-dojo-config="async: true"></script>
 </head>
 
 <body class="claro">

  
  <h1>Using String IDs</h1>
  <div>
   shipSelectString: 
   <div id="shipSelectString"></div>
   
   <div id="selectStringEnterpriseButton"></div>
   
   <div id="selectStringYorktownButton"></div>
  </div>
  
  <h1>Using Numeric IDs</h1>
  <div>
   shipSelectNumber: 
   <div id="shipSelectNumber"></div>
   
   <div id="selectNumberEnterpriseButton"></div>
   
   <div id="selectNumberYorktownButton"></div>
  </div>
 </body>
</html>

The JavaScript

Here's where all the Dijit bindings happen. For both the String and numeric ID stores, we create a Select and a couple buttons that call select.set("value", [the ID]):
require(["dojo", "dijit", "dojo/data/ItemFileReadStore", "dijit/form/Select", 
   "dijit/form/Button", "dojo/domReady!"
  ], function(dojo, dijit, ItemFileReadStore, Select, Button) {
 //****** Using Strings for IDs ******
 //Create the data store using a JSON file
 var shipStoreString = new ItemFileReadStore({
  url: "ships-string.json"
 });
 
 //Create and start the Select, using the ItemFileReadStore as its store
 var selectString = new Select({
  name: "shipSelectString",
  store: shipStoreString
  }, "shipSelectString");

 selectString.startup();
 
 //Create buttons that will use the Select widget's set() function to set the value
 new Button({
  label: "Set shipSelectString to Enterprise",
  onClick: function() {
   selectString.set("value", "2");
  }
 }, "selectStringEnterpriseButton");
 
 new Button({
  label: "Set shipSelectString to Yorktown",
  onClick: function() {
   selectString.set("value", "6");
  }
 }, "selectStringYorktownButton");
 
 //****** Using numbers for IDs ******
 //Create the data store using a JSON file
 var shipStoreNumber = new ItemFileReadStore({
  url: "ships-number.json"
 });
 
 //Create and start the Select, using the ItemFileReadStore as its store
 var selectNumber = new Select({
  name: "shipSelectNumber",
  store: shipStoreNumber
  }, "shipSelectNumber");

 selectNumber.startup();
 
 //Create buttons that will use the Select widget's set() function to set the value
 new Button({
  label: "Set shipSelectNumber to Enterprise",
  onClick: function() {
   selectNumber.set("value", 2);
  }
 }, "selectNumberEnterpriseButton");
 
 new Button({
  label: "Set shipSelectNumber to Yorktown",
  onClick: function() {
   selectNumber.set("value", 6);
  }
 }, "selectNumberYorktownButton");
});
Now if you fire up the HTML page in your browser, you'll see the screen shown above in your browser. Click on the buttons, and you'll see that the Select with Strings as IDs changes when you click the buttons, but nothing happens to the Select with numeric IDs. This is the limitation (bug?) that prevents you from using numeric IDs in a store that's backing a Select.

Resources