Medryx Observations

September 10, 2008

dijit.Search Part 2

Filed under: dojo, javascript — Tags: , — maulin @ 7:27 pm

Well its been a while since my first post about a search widget for dojo.data stores. There are a lot of reasons — the beautiful Portland, Oregon summers being the most significant. But now the kids are back in school, life is normalizing, and its time to show you how I finished what I think is a very nifty widget.

First a recap: My goal was to make a pretty widget that simply allowed you to search one or more dojo.data stores. You could use this in a lot of ways. I’ve posted a demo here that uses several dojox.data stores to allow you simultaneously search Google, Yahoo!, and Wikipedia from your site. While this is one use case, the use case I find most interesting is doing a “global” search on your own dojo.data stores that are driving your web application. But the sky is the limit! You can use this to build OSX Searchlight-style functionality into your app. You can “skin” the widget itself to make it match your site’s style. You an change the prompt from “Search” to anything you want, that may be more useful in your app. You can even include a dropdown menu that appears upon clicking the magnifying glass to the left of the search box that allows your users to choose which stores (or which methods) they would like to use to search. You can define a template for your result to make it display in just about any way you can imagine. (The default simply shows the “label” text from the resulting items.)

Before going any further, I want to make it clear that this works, but is clearly alpha-level code. And I think there are some simplifications that can be made in the usage of the widget in the future if this is useful to more people than me. But for now, think of it as the 0.9 dojox.grid — way too complicated, but really functional. And hopefully this widget will evolve into a simpler and more elegant widget just as the grid has. FYI, this widget is dependent on dojo 1.2 (primarily because it uses the 1.2 dojox DataGrid).

So lets dive into how to use the widget. The first concept we need to go over is how to create an interface into your stores to allow searching them in various ways. To solve this, you need to create a SearchStore. A SearchStore is really a composite of interfaces into other dojo.data stores. As is my usual practice, sometimes things are explained more easily with code, so here goes. Lets build the demo widget. We’ll start with just the WikipediaStore:

dojo.require("medryx.widget.Search");
dojo.require("dojox.data.WikipediaStore");
 
//create the WikipediaStore
var wikiStore = new dojox.data.WikipediaStore();
 
//create the SearchStore
var searchStore = new medryx.widget.SearchStore();
 
//add the wikiStore to the SearchStore
searchStore.addStore({
  name:"Wikipedia", // what will this search be called (useful when there is more than one store
                    // in your SearchStore (or more than one method or more than one method of
                    // searching the same store
  store:wikiStore, //the source store
 
  //this is the most important piece. given a search string (searchTerms) and
  //and a requested searchType, return the query that should be passed to the
  //the store (in this case to wikiStore.fetch())
  queryBuilder:function(searchTerms, searchType) {
    //if the user specifies "Wikipedia" search, or any search, then use this store
    if (!searchType || searchType == "Wikipedia" || searchType == "*") {
      return {query:{text:searchTerms, action:"query"}}; //see WikipediaStore for how to query Wikipedia
    }
  }
});

Okay, that does seem a little complicated. But conceptually its pretty simple. When a user types a string into the search box, the widget needs to know how to handle it with respect to your source stores — so you provide a “queryBuilder” method that converts generic search terms to your specific query. When there is just one member of you SearchStore, this seems like overkill. So lets add some more stores. I used the dojox.data.ServiceStore to create a Google store and Yahoo store that can search those services. The code for how to create those services is a bit beyond the scope of this article, but you can see the full code in the demo. For now, lets take it on faith that there is a yahooStore and a googleStore that I have created. So lets wire them up to our SearchStore:

searchStore.addStore({
  name:"Google",
  store:googleStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Google" || searchType == "*") {
      return {query:{q:searchTerms}}; //see google smd for format
    }
  }
}); 
 
searchStore.addStore({
  name:"Yahoo",
  store:yahooStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Yahoo" || searchType == "*") {
      return {query:{query:searchTerms}}; //see yahoo smd for format
    }
  }
});

So now you can see how we can create a very complicated method for searching multiple different stores with the same search string. There are several more features to the SearchStore, but I’ll come back to those. Right now, lets change gears and see how we can actually use this widget with this SearchStore. In the simplest case we simply declare the following in our markup:

<div dojoType="medryx.widget.Search" store="searchStore">
  <script type="dojo/connect" event="onHitSelected" args="item, store, hit">
    console.debug("Hit selected:", item, store, hit);
  </script>
  <script type="dojo/connect" event="showAllResults" args="terms">
    alert("search for more results for \"" + terms + "\" yourself, you lazy bum!");
  </script>
</div>

You can see that you can easily connect to the “onHitSelected” event to decide how you will handle the user selecting one of your search results. This event passes the original item, and its original source store as parameters. It also provides access to the “hit” itself, which is an item in the SearchStore that maps to the original item from your source store. This can be very useful, once you know about some more features of the SearchStore. We’ll get there, I promise.

Notice also that you can connect the the “showAllResults” event, which is really expected to behave as a link to your “complete search” page (or dijit.Dialog, or whatever). The “Complete search results” link is at the bottom of the search results in all cases.

Lets look at some ways to customize the widget a little more:

<div dojoType="medryx.widget.Search" store="searchStore">
  <div dojoType="dijit.Menu">
    <div dojoType="dijit.MenuItem" searchType="Google" iconClass="Google">Google</div>
    <div dojoType="dijit.MenuItem" searchType="Yahoo" iconClass="Yahoo">Yahoo</div>
    <div dojoType="dijit.MenuItem" searchType="Wikipedia" iconClass="Wikipedia">Wikipedia</div>
  </div>
  <script type="dojo/connect" event="onHitSelected" args="item, store, hit">
    console.debug("Hit selected:", item, store, hit);
  </script>
  <script type="dojo/connect" event="showAllResults" args="terms">
    alert("search for more results for \"" + terms + "\" yourself, you lazy bum!");
  </script>
</div>

If you include a dijit.Menu inside your declaration, then this menu will be displayed when you click the magnifying glass to the left of the search box. You can see there is a special (REQUIRED) attribute on each MenuItem called “searchType”. This is the searchType that is sent to your “queryBuilder”, and can be any string that your queryBuilder understands. If a user does not expand the menu, and simply presses “Enter”, then the searchType is by default set to “*” (though you can change this via the defaultSearchType attribute).

There are plenty of attributes to help you customize your search widget, and they are all documented in the source of medryx.widget.Search.

So now lets look at how we might do something useful with our “onHitSelected” event. Let say we want to open an iframe with the url of the hit (as in the demo). First add the iframe to your markup:

<div align="center">
  <iframe style="width:90%; height:600px; border:1px solid black; display:none;" id="preview"></iframe>
</div>

So one way to get to the “right” url regardless of which store your result came from would be to make a big, complicated event handler (can you tell this will not be the preffered method?)

var hitHandler = function(item, store, hit) {
  var src = "";
  if (store == yahooStore) {
   src = store.getValue("ClickUrl");
 } else if (store == googleStore) {
   src = store.getValue(item, "unescapedUrl", store.getValue(item, "url", defaultValue));
 } else if (store == wikiStore) {
   src = "http://en.wikipedia.org/w/index.php?title=" + store.getValue(item, "title");
 }
  dojo.byId("preview").src = src;
  dojo.byId("preview").style.display = "";
}

This is perfectly reasonable, but its quite “spaghetti-like”. And every time you add a store, you have to change you handler. What if we could define for each store how to obtain the “src” string, and then use a common way of accessing it? Much cleaner IMHO. So SearchStore has another attribute you can include when calling addStore — “map”. A map is a list of pseudo-fields that will be accessible for each hit from the SearchStore itself. Each pseudo-field begins with “search”. So lets upgrade our searchStore as follows, adding in the maps:

searchStore.addStore({
  name:"Google",
  store:googleStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Google" || searchType == "*") {
      return {query:{q:searchTerms}};
    }
  },
  map:{
    searchUrl:function(item,attribute,defaultValue) {
	return this.getValue(item, "unescapedUrl", this.getValue(item, "url", defaultValue));
    }
  }
}); 
 
searchStore.addStore({
  name:"Yahoo",
  store:yahooStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Yahoo" || searchType == "*") {
      return {query:{query:searchTerms}};
    }
  },
  map:{
    searchUrl:"ClickUrl" //simple! so no "function" needed, just map from "searchUrl" to yahoo's "ClickUrl"
  }
}); 
 
searchStore.addStore({
  name:"Wikipedia",
  store:wikiStore,
  queryBuilder:function(searchTerms, searchType) {
    if (!searchType || searchType == "Wikipedia" || searchType == "*") {
      return {query:{text:searchTerms, action:"query"}};
    }
  },
  map:{
    searchUrl:function(item, attribute, defaultValue) {
      return "http://en.wikipedia.org/w/index.php?title=" + this.getValue(item, "title");
    }
  }
});

Now the event handler can be simplified, using the “hit” parameter:

var hitHandler = function(item, store, hit) {
    dojo.byId("preview").src = searchStore.getValue(hit, "searchUrl");
    dojo.byId("preview").style.display = "";
}

Much nicer, don’t you think? And you could add more stores to this widget or more ways to search the same store, and never have to touch the event handler.

Last but not least, you can customize how you “hits” appear on the dropdown list, as well as how the “category headers” appear. Simple include a child of your widget with an attribute of template=”category” or template=”hit”. You can then access any of  the attributes (or pseudo-attributes) of your item with the familiar ${attributeName} notation.

Building this widget taught me a LOT about dojo widgets. Unfortunately it wasn’t until I was almost done with the widget, that I read about the dojo 1.2 attributeMap. So obviously, to really take this widget to the next level, some work would need to be done. But right now it is quite functional, and can quickly and easily add a lot of robust functionality to your applications with very little additional work.

For now, you’ll need to grab the files you need from my dev server at http://dev.medryx.org/lib/js/medryx/. I’ll try to submit this to dojo as a dojox.widget sometime soon.

Let me know what you think, and if I can answer any questions!

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

Powered by WordPress