Extending a widget to make it data-aware

In the cookie about dojo.data, I extolled the virtues of data-driven widgets, and how much I loved them. However, there are times when the widget you want to use is not data-aware. This tutorial will step you through how to quickly and easily add data capability to a widget.

To get started, let’s take a look at one of the new widgets in dojox.form that landed in release 1.1.0 - the DropDownSelect. A dojox.form.DropDownSelect is basically a stylable version of the HTML select. It works pretty well out-of-the-box, but isn’t data-aware. What would be really nice would be to have the available options automatically populated based on the values in a store.

In other words, instead of doing this:

<select dojoType="dojox.form.DropDownSelect">
  <option value="Opt1">Option 1</option>
  <option value="Opt2">Option 2</option>
  <option value="Opt3">Option 3</option>
</select>

I would like to do this:

<script>
var data = { identifier: "value",
             label: "label",
             items: [ {value: "Opt1", label: "Option 1"},
                      {value: "Opt2", label: "Option 2"},
                      {value: "Opt3", label: "Option 3"}
             ]
            };
var myStore = new dojo.data.ItemFileWriteStore({data:data});
</script>
<select dojoType="dojox.form.DropDownSelect" store="myStore"></select>

Yes, I know it looks more complex - but in the end, the goal is to have an automatically-updating select whose values are populated by the contents of *any* store.

Note: For the purposes of this tutorial, all files reside in a directory called “dojocampus” that is a peer to the dojo, dijit, and dojox directories. You also need to be using version 1.1.0 (or later) of dojo.

First, let’s create an HTML file. Since we have to create one anyway, it doesn’t cost us that much to make it a doh test-case file. This is generally a good practice…rather than writing throw-away html files for developing, at least structure it as a test case. You can always go back in later and write up more tests for your code. Place this file in tests/test_DataDropDown.html (in your dojocampus folder):

<html>
  <head>
    <script type="text/javascript"
      src="../../dojo/dojo.js"
      djConfig="isDebug: true, parseOnLoad: true">
    </script>
    <script type="text/javascript">
      dojo.require("doh.runner");
      dojo.require("dojo.parser");
      dojo.require("dojocampus.DataDropDown");
      dojo.require("dojo.data.ItemFileWriteStore");
      dojo.addOnLoad(function(){
          doh.register("tests", [
              function test_addItem(t){
                  t.t(true); // Fill this in later
              }
          ]);
          doh.run();
      });
      var data = { identifier: "value", label: "label",
                   items: [ {value: "Opt1", label: "Option 1"},
                            {value: "Opt2", label: "Option 2"},
                            {value: "Opt3", label: "Option 3"} ] };
      var myStore = new dojo.data.ItemFileWriteStore({data:data});
    </script>
<style>
      @import url(../../dojo/resources/dojo.css);
      @import url(../../dijit/themes/tundra/tundra.css);
      @import url(../../dojox/form/resources/DropDownSelect.css);
      @import url(../../dijit/tests/css/dijitTests.css);
    </style>
 
  </head>
  <body class="tundra">
<h1 class="testTitle">Test: dojocampus.DataDropDown</h1>
<h2>Select-based</h2>
<select dojoType="dojocampus.DataDropDown" value="Opt2">
      <option value="Opt1">Option 1</option>
      <option value="Opt2">Option 2</option>
      <option value="Opt3">Option 3</option>
    </select>
<hr/>
<h2>Store-based</h2>
<select jsId="dataWidget" dojoType="dojocampus.DataDropDown" value="Opt2"
            store="myStore"></select>
 
  </body>
</html>

And this stub file as DataDropDown.js (again, in your dojocampus folder):

dojo.provide("dojocampus.DataDropDown");
dojo.require("dojox.form.DropDownSelect");
dojo.declare("dojocampus.DataDropDown", dojox.form.DropDownSelect, {
});

At this point, you should be able to pull up your test file in your browser, and see the results. The first (select-based) dropdown will be working and fully functional, but the store-based one will be empty. We need to have a way to let the widget “know” about the store. Add the following line to DataDropDown.js (inside the dojo.declare block):

store: null,

This allows the “magic” of the dojo parser to pass in our store to our widget.

Now, when we start our widget, we want to fetch the items in the store, and shove them into our widget. The DropDownSelect widget provides an addOption function for that purpose. Add the following function to your DataDropDown.js file (after your store definition):

startup: function(){
    this.inherited(arguments);
    if(this.store){
        var store = this.store;
        var fx = function(item){
            this.addOption(store.getIdentity(item), store.getLabel(item));
        };
        store.fetch({onItem: fx, scope: this});
    }
}

Reloading your test file, you will now see that the second dropdown is populated based on the contents of your store! But there is a small problem - our value isn’t getting set! DropDownSelect, as part of its initialization selects the option specified as “value”. However, since our DropDownSelect was initialized with no options, the value got clobbered. We need to save our value, and set it after we have started up. Add the following function to your DataDropDown.js file (before startup):

constructor: function(kwArgs){
    if(kwArgs.value){
        this._origVal = kwArgs.value;
    }
},

and replace the line in your startup function which reads “store.fetch({onItem: fx, scope: this});” with the following:

var fxComp = function(items){
    if(this._origVal){
        this.setAttribute("value", this._origVal);
    }
};
store.fetch({onItem: fx, onComplete: fxComp, scope: this});

And we now have a fully-functional read-only data-driven widget! However, we would like to take it to the next level, and want to support the dojo.data.api.Notification API as well. To test for this, let’s extend our test case and write an automated test to add the option. Replace the line in tests/test_DataDropDown.html which reads “t.t(true); // Fill this in later” with the following (you can ignore the ugly hackery for the quoted string):

myStore.newItem({value: "Opt4", label: "Option 4"});
t.is(4, dataWidget.options.length);
var d = new doh.Deferred();
var cb = function(item){
    try{
        myStore.setValue(item, "label", "Changed Option");
        t.is("Opt2", dataWidget.value);
        var dwOpt = '<' + 'div class=" dojoxDropDownSelectLabel">' +
                    'Changed Option' +
                    '</div' + '>';
        t.is(dwOpt, dataWidget.label);
        myStore.deleteItem(item);
        t.is(3, dataWidget.options.length);
        t.is("Opt1", dataWidget.value);
        d.callback(true);
    }catch (e){
        d.errback(e);
    }
};
myStore.fetchItemByIdentity({identity: "Opt2", onItem: cb});
return d;

Basically, what this does is adds an option, changes the label of an option, and deletes an option (the currently-selected option) - all the while checking that the tests succeeded. Running this in your browser will give errors in your console (if you are using firebug) letting you know that the test cases are failing - which is good, since we haven’t implemented anything yet.

The dojo.data.api.Notification API defines three main functions that we want to be concerned with - and, luckily, DropDownSelect has fairly equivalent functions (in parenthesis). onNew (addOption), onDelete (removeOption), and onSet (setOptionLabel). Add these handlers to your DataDropDown.js file (before your constructor):

_onNewItem: function(item){
    var store = this.store;
    this.addOption(store.getIdentity(item), store.getLabel(item));
},
_onDeleteItem: function(item){
    var store = this.store;
    this.removeOption(store.getIdentity(item));
},
_onSetItem: function(item){
    var store = this.store;
    this.setOptionLabel(store.getIdentity(item), store.getLabel(item));
},

add a new function before your startup function to make the connections:

postCreate: function(){
	this.inherited(arguments);
	if(this.store && this.store.getFeatures()["dojo.data.api.Notification"])
	{
		var store = this.store;
		this.connect(store, "onNew", "_onNewItem");
		this.connect(store, "onDelete", "_onDeleteItem");
		this.connect(store, "onSet", "_onSetItem");
	}
},

Pretty easy, huh? Now, loading your file in your browser will run the tests (and they should, hopefully, pass). Your second dropdown (the data-driven one) will be set to “Option 1″, since we deleted Option 2. It will also have the newly-added Option 4.

I hope that this gets you excited to work with dojo.data. It really is pretty magical stuff. Play around with it a bit. Try creating multiple widgets, and seeing how everything can be updated at the same time by updating the single store.

Files can be downloaded here: DataDropDown.js and test_DataDropDown.html

Tags: