Tuesday, June 29, 2010

ArcGIS Server JavaScript API For Beginners: Populating DOJO FilteringSelect

The more I work with it, the more I like it, that is, the ArcGIS Server JavaScript API.  It has some real funkyness that is hard to get use to at first (loose or no typing, dynamic nature, callbacks, etc) but once you get going, it is pretty slick.  Given this, I am going put up a few posts in the coming weeks that illustrate very simple samples that I think are useful.  These aren't elegant, OO examples, just examples so all JS pros out there will probably find these post of little use.  They are based on the ArcGIS Server JavaScript API 2.0 (and ArcGIS 10).

Generally speaking, usability is often the single most important factor dictating the success or failure of your geoweb application. Make it simple and fool proof and guide the user through the workflow or task. If they get confused, you have failed. One component that can be used to help accomplish this is a pre-populated drop-down/search box that auto-completes. DOJO, the toolkit that the ArcGIS Server JavaScript API is built on, provides a lot of nice components for doing just this. One of these is dijit.form.FilteringSelect. It does a lot of cool stuff which assist in fool proof usability, much more that what is mentioned here. This post addresses how to populate a FilteringSelect dijit with the results of a query, in this case, the query of an ArcGIS Server map service layer’s field value. It is assumed that you know the very basics of using the ArcGIS Server JavaScript API, if not, have a look.

Instantiating a FilteringSelect dijit can be done declaratively or programmatically. In this case, we will declare it:

<body class="tundra">
    <input dojoType="dijit.form.FilteringSelect"
           id="lineid"
           searchAttr="name"
           name="widgetName"
           onChange="doSomething(this.value)">
</body>



where the id attribute is used to identify the component in the document, the onChange attribute defines the event handler when the component is changed and the searchAttr attribute, well, we will discuss that later. Remember to define your component before declaring it using dojo.require("dijit.form.FilteringSelect"). Now that the component has been declared, lets populate it. A best practice for initializing web maps along with selected stuff that goes along with them (such as our FilteringSelect component) is to define a function that is called when the the page is loaded. This is handled nicely using the dojo.addOnLoad() function. Something like:



    //Our main initialization function, called at just the right time
    function init () {
        //Create your query
        var queryTask = new esri.tasks.QueryTask(<query task rest endpoint>);
        //set the onComplete event handler, in this case, when the query is complete, call initLineID,
        //production code would handle handle the error callback as well
        dojo.connect(queryTask, "onComplete", initLineID);
       
        //build and execute your query
        var query = new esri.tasks.Query();
        query.outFields = [<name of the field you want>];
        query.text = "all";
        query.returnGeometry = true;
        queryTask.execute(query);

     }
     
     dojo.addOnLoad(init);


In the snippet above, we are basically building a query task and executing it.  For details on the queryTask works, have a look at the queryTask object (everything is an object is JS, another kind of weird thing to get use to).

Now the good stuff, populating the FilteringSelect component.  We first need to bind the completion event of the query to some logic that will populate the FilteringSelect component.  This is done using a handy little dojo function:

     dojo.connect(queryTask, "onComplete", initLineID);

which basically says once the queryTask fires the onComplete event, take the results and run with them in a function called initLineID.  Here is initLineID:

function initLineID(features) {
        var lineIdObjects = [];
        dojo.forEach(features.features, function(feature) {
            lineIdObjects.push({"name": feature.attributes.field_name});;
        });
       
        //Build the appropriate data object for our data component
        var data = {
              "identifier": "name",
              "items": lineIdObjects
        }
       
        //bind the data object to the datastore
        var lineDataStore = new dojo.data.ItemFileReadStore({data: data});
   
        //bind the data store to the FilteringSelect component
        dijit.byId("lineid").store = lineDataStore;
     }


Basically, we take the queryTask results, in this case referred to as "features" and refactor them into a ItemFileReadStore which is then bound to the FilteringSelect component.  Most of the UI components in DOJO work best consuming data from one of the DOJO data stores, in this case we are using the ItemFileReadStore.  ItemFileReadStores basically house JSON data formatted in a specific way.  Unfortunately, direct REST requests made to the ArcGIS Server Rest API don't return JSON formatted in this way which would have made things very easy but we will leave that for another time.

Because of this, we basically need to refactor our queryTask's returned features in a way that the ItemFileReadStore likes (see, "Reading JSON Data with DOJO").  We do this by creating an array object and populating it by stepping through each queryTask feature and adding it to the array as a name/value pair (one of the ItemFileReadStore format requirements).  We then build a generic object called "data" with 2 properties, "identifier" and "items".  The identifier property tells the ItemFileReadStore what handle to look for in the name/value pairs collection (our array).  In our case, we named each result record "name."  The second property called "items" is assigned our array.  Upon completion of this generic data object, we then simply bind it to the ItemFileReadStore using some named property.  In this case, we are calling it "data" as well.  Our final step is to find our FilteringSelect component in the document using the dojo.byId function and to assign its store property the ItemFileReadStore we created.  Thats it...

One final note on what I skipped earlier.  Remember that we assigned each of our queryTask feature values a attribute name called "name" and assigned our required "identifier" property and value of  "name" as well:
   
        var lineIdObjects = [];
        dojo.forEach(features.features, function(feature) {
            lineIdObjects.push({"name": feature.attributes.field_name});;
        });
       
        //Build the appropriate data object for our data component
        var data = {
              "identifier": "name",
              "items": lineIdObjects
        }


this is the handle that is used by the FilteringSelect component to find where to look for the data.  We tell the FilteringSelect component where to look for the data values using the searchAttr attribute:

    <body class="tundra">
    <input dojoType="dijit.form.FilteringSelect"
           id="lineid"
           searchAttr="name"
           name="widgetName"
           onChange="doSomething(this.value)">
     </body>


You can download this sample here