Medryx Observations

August 14, 2008

A Dojo Search Widget: dijit.Search

Filed under: dojo, javascript — Tags: , , , — maulin @ 6:33 am

Search has become ubiquitous on web sites and applications. Its long since replaced categorization or filing. On my new mac, I can’t remember if I have ever dug through my Applications folder looking for an app. I just type a few letters in Spotlight, and voila. On the dojo website its the same story.

But what is conspicuosly missing (IMHO) from the dojo widget set (dijit) is a robust “search” widget. While you can certainly use the widgets available, or easily create new widgets to make a search field and present the results, this seems to me like it would have a lot of broad applicability throughout the dojo community. And honestly, without someone spending a lot of time up front, most of these quickly put together solutions don’t look great.

In my opinion the search feature on apple.com is an ideal “Web 2.0″ flavored search. Type a few letters and you get pertinent results, almost instantly, and without a page refresh. The results are present in a categorized fashion, and there is a link to see all of your results or do a more complete search of the apple website. Aside from its functionality, I think its just plain pretty. A nice rounded text box, that is very intuitive to use, and declares it purpose without any real prompting or explanation.

So I’ve embarked on creating a search widget for dojo — dijit.Search. (Well that is awfully presumptious of me. Obviously , none of this is sanctioned Dojo code, nor has it even been submitted, let alone accepted as a part of Dojo. So for now, I’ll keep it in my own namespace — medryx.widget.Search).

So let’s get started. First up — lets describe what the widget will do:

  1. A pretty search field that gives feedback about what’s going on, and allows the user to select a specific type of search to use if desire (similar to the Firefox 3 search field).
  2. Allow searching via a few mechanisms: perhaps searching more than one data store on the client, or more than one field in the same store for a match. But also obviously provide the ability to perform the search on the server and return pertinent results. I think this is an important point — I think search has really come into its own as a “navigation tool” since it has been able to search all over the place for a text string, and provide relevant results with links to either launch a function, view a page, or view result details.
  3. Create a nice looking, categorizable search result window that pops up in some fashion on the screen, behaves correctly from an A11y standpoint, and provides the ability to request a more detailed search if the “preliminary” search results have not delivered satisfactory results.

Okay, so its not exactly a “spec”, but its something to work with. I always find it easiest to start really thinking about the “backend” of a widget, by first fleshing out the front end. So lets get to work on the pretty search field.

I want the field to have rounded edges, a magnifying glass that can be clicked to reveal search options, a “prompt” inside the text field that disappears/re-appears correctly, and an animated gif of some sort inside the field to tell you when the search is in progress.

The easiest way to accomplish this will be with thee background images (that may eventually merged into the same sprite) — one for the left semicircle with magnifying glass, one for the right semicircle, and one for the text field portion. So I want my final search box to look like this:

A couple notes: obviously not everyone will be working with a gray background onto which to put the search box. But I don’t want to create an infinite number of colors. So I want to use PNG images which have excellent transparency support. That way you cut put the box on any color background with good effect:

Then only other configuration I will need will be if there is no background (white or transparent background). But I’ll deal with that later.

So what’s the catch? IE6 (of course). It does not support transparent PNG’s natively. Thankfully there is a nifty fix available via LGPL called IEPNGFix that solves this problem rather well. And with very little effort you can dojo-ize this solution (which I have done). Now I just make a call to dojo.require("medryx.util.png") and I can reliably use transparent PNG’s in a cross-browser compliant way. (That piece in and of itself took me a whole night! See why it would be great to have an even richer set of dijits? If someone else had solved that problem or the pretty search widget problem already, I could’ve spent my time doing better things last night! Nah, who am I kidding? I would’ve been doing something else nerdy…)

So we want to be able to use these boxes anywhere on the page, and allow them to be of any size. I think the best solution is to have them adhere to the dimensions of their original DOM node as such:

<div dojoType="medryx.widget.Search" style="width:400px;"></div>

So here is the CSS and html that can put this together to make the images above:

<div class="medryxSearch">
	<label for="search_${id}">
		<div class="medryxSearchBox" style="width:${paddedWidth}px">
			<span class="left" dojoAttachEvent="onmousedown:_ignoreBlur, onclick:onOptionsClicked" dojoAttachPoint="optionsNode" id="searchOptions_${id}"></span>
			<div style="width:${width}px"><input style="width:${inputWidth}px" id="search_${id}" autocomplete="off" dojoAttachPoint="inputField" dojoAttachEvent="onkeyup:_onInputKeyUp, onblur:_onInputBlur, onclick:clearPrompt" /></div>
			<span class="right"></span>
			<span class="searching" dojoAttachPoint="searchingNode"></span>
		</div>
	</label>
</div>

I’ll explain some of the template stuff a bit later. Lets focus on the “medryxSearchBox”: This is a fixed-width box, whose size is taken from the contentBox of the declaring node. The width is then padded to include the width of the left/right portions of the widget. The first span is the left bit, the next is the text field, and the next span is the right bit. The “searching” span is where the animated gif is created, and displayed as needed. The reason that the node after the “left” span is wrapped in a div has to do with the way different browsers render background images on a text field. Suffice it to say it is incosistent, and much more easily managed on a div.

So here is the CSS:

.tundra .medryxSearch {
	margin: 0;
	padding: 0;
}

.tundra .medryxSearchBox {
	position:relative;
	margin:0;
	padding:0;
	left:19px;
}

.dj_ie .tundra .medryxSearchBox .left {
	top:-1px;
}

.tundra .medryxSearchBox .left {
	background: transparent url('../themes/images/searchLeft.png') no-repeat scroll left;
	width: 19px;
	height: 24px;
	top:0;
	margin:0;
	left:-19px;
	padding:0;
	position: absolute;
	cursor:pointer;
}

.tundra .medryxSearchBox .right {
	background: transparent url('../themes/images/searchRight.png')
		no-repeat scroll right;
	height: 24px;
	top:0;
	right: 19px;
	width: 10px;
	margin:0;

	padding:0;
	position: absolute;

}

.tundra .medryxSearchBox .searching {
	background: transparent url('../themes/images/searching.gif') no-repeat scroll right;
	width:16px;
	height:16px;
	padding-left:16px;
	position:absolute;
	top:-9999px;
	right:25px;
	margin-top:4px;

}

.dj_ie .tundra .medryxSearchBox .right {
	top:-1px;
	right:18px;

}

.tundra .medryxSearchBox div {
	background: transparent url('../themes/images/search.png') repeat-x	scroll bottom;

	margin:0;
	padding:0;
	height: 24px;
}

.dj_ie .tundra .medryxSearchBox input {
	font-weight: 500;
}

.tundra .medryxSearchBox input {
	color: white;
	background: transparent;
	border: 0;
	font-size: 13px;

	margin:2px 0 0 5px;
	padding:0;

	height: 24px;
	outline-color: -moz-use-text-color;
	outline-style: none;
	outline-width: medium;
	text-shadow:0 1px 1px #323232;
}

Okay, that’s a lot of CSS. Let’s break it down. First off, it uses the standard dojo method of “namespacing” classes by prefixing each declaration with “.tundra”. (On a side note, I think I should probably break this out into pieces that apply to any theme vs those that are more themeable. Those that apply regardless of theme should not be prefixed, but this is a work in progress…)

A couple cool tricks you may not know about. Instead of beating yourself over the head with the different ways browsers render background images, and fighting the different interpretations of margin and padding, you can use a .dj_ie prefixed class to tweak you CSS class for IE. Dojo quietly adds the dj_ie class to the HTML tag of the page, so this is always the top-most class. Brilliant!

Notice also that I have nested the meat of my widget within my DOM node. This is so I can allow the positioning of the widget to be relative (actually absolute) to the DOM node and get positioning anywhere on the page. CSS position:absolute interprets “absolute” as placement on the page not within the containing element, unless the containing element is relatively positioned.

Okay, now we have complete the first step of widget creation — we’ve made a pretty widget that loads nicely when placed on a page.

Now lets handle the “prompt” bit. I want some variable text presented to the user, but when they click on the field it should disappear, but if they leave the field empty, it should re-appear. So on my widget declaration we add prompt:"Search", and users can easily override this. Then in postCreate, we set this.inputField.value = this.prompt. Finally we wire the onclick event from the inputField to the clearPrompt method on the widget, which clears the input field of the prompt via a string.replace — this.inputField.value.relplace(new RegExp(”^” + this.prompt), “”). Then when a search is done, we could reinstate the prompt. Certainly if there is a blur-event and the field is empty, we should reinstate the prompt.

The last “easy” thing to do before we have to really have to start thinking hard about the “backend” of this widget is to connect the magnifying glass icon to a menu of options. Since I want this to be pretty flexible, I simply check if the declaration has a dijit.Menu as a child, and if it does, it wires it up correctly. So the usage would be something like:

<div dojoType="medryx.widget.SearchBox" style="width:300px;">
  <div dojoType="dijit.Menu">
    <div dojoType="dijit.MenuItem">Google</div>
    <div dojoType="dijit.MenuItem">Yahoo</div>
  <div>
</div>

So how do I wire this up? This took me a few minutes to figure out. The first thing to realize is that the child widgets have not been instantiated yet when the parent is being instantiated by dojo.parser.parse(). So I can’t attach to this yet. My options are to wait until my child widget is instantiated (by connecting to the menu in some way in startup() and perhaps making this widget a “Container”). But that seemed like a lot of work. My first attempt was to simply call dojo.attr and add the targetNodeIds value and leftClickToOpen values to the DOM node of the dijit.Menu before it has been parsed. And it works! In FF3. Unfortunately IE6 doesn’t like this and throws an “Unexpected method or property access” exception. Damn!

So here is my trick: I parse the dijit.Menu myself! So, do a dojo.query(”[dojoType='dijit.Menu']“, this.srcNodeRef) to get the DOM node. Then call dojo.parser.parse on the returned node. Then use the return value from the the call to parse to get a handle to my Menu widget. Then use widget.attr to change leftClickToOpen to true, and then call bindDomNode to my optionsNode (the magnifying glass). So the code looks like this:

var menuWidgets = dojo.query("&gt; [dojoType='dijit.Menu']", this.srcNodeRef);
var menuNode = menuWidgets.length &amp;&amp; menuWidgets[0];
if (menuNode) {
// DOESN'T WORK IN IE6!
//	dojo.attr(menuNode, "targetNodeIds", dojo.attr(this.optionsNode, "id"));
//	dojo.attr(menuNode, "leftClickToOpen", "true");
//parse the menu now so we can get a handle for it
	this.menu = dojo.parser.instantiate([menuNode])[0];
	this.menu.attr("leftClickToOpen", true);
	this.menu.bindDomNode(this.optionsNode);
 
	this.connect(this.menu, "onItemClick", "menuItemClicked");
}

And now you have a “search menu”.

Okay, enough procrastination, time to think about the model. But wait, before we do, we should think through how the results might be displayed. With the almost infinite flexibility of the dojox.grid, and its inherent connection to dojo.data via DataGrid, I think it would be silly to roll my own table to display the results of the search. I imagin wrapping the grid into some kind of popup node that display once a search is initiated. That is work to be done.

But if I want to use dojo.data, how will I meet my need for allowing searching multiple different fields of a store, or multiple different data sources? I pondered this for a day or two, with lots of complicated ideas. And then it came to me: a composite data store. Call it medryx.data.SearchStore. This store takes other dojo.data stores as part of its initialization parameters. And for each store, you can describe a way to create a “search” query on the store. Heck you can even supply the same store multiple times, each with a different “search strategy”. When the Search widget calls fetch, the query will be {searchTerms:"foo", searchMethod:"*"} where searchMethod is the value of any item that was selected from your menu of search methods, the searchTerms are the just the text from the search box. The composite SearchStore will initiate a fetch on each of its member stores, using the “search strategy” to create store-specific queries. As each store returns we’ll glue the results together, with some indication as to where they came from, and the DataGrid will know how to display them. Its actually quite flexible.

So, now all I have to do is built the SearchStore, subclass the DataGrid to display these results flexibly but correctly, add in “more…” type functionality, and I’m done. Jeez. All this just for a lousy little search field. Man how I wish someone had done this before me! Well, I’ll finish writing this widget, and then post another entry on how it goes, and hopefully include the source so you all won’t have to go through what I have!

4 Comments »

  1. Great write up :) Can’t wait to see what you’re coming up with.
    Searching client data store is a great idea ! I wouldn’t have thought about it !

    Comment by Tim — August 14, 2008 @ 10:46 am

  2. Great work, Maulin. I’m actually also mulling over a composite store, but not for search so much as to feed a ‘multi-Grid’ component. In my current project we’re accessing GAE models using REST, and some of those models have properties that are references to other objects (as one has), which makes a Grid kind of awkward. When editing a cell which is of type reference to another object, I’d like another Grid to pop up where an existing object (row) can be selected or created. If one of its cells is also a reference, the process repeats.

    Anyway, I think that there might be a more generic need for a ’store wrapper’, of which both my ‘multi-Grid’ and your smart search can be two earl targets.

    Cheers,
    PS

    Comment by Peter Svensson — August 14, 2008 @ 12:30 pm

  3. @Peter: I have a similar need with the “multi-grid” concept for my object model. My small concern is that from a usability perspective nested grids are pretty cumbersome (see M$ Access’s approach to opening references — yuck!). But automagically creating a “Master-detail” style grid where if a cell is an object that is an item in a store (store.isItem()), then it provides a link that will open some kind of popup grid. But with JsonRestStore, you get a store that can really open *any* object. So if you have two stores — fooStore and barStore — and foo objects in fooStore and bar objects in barStore. If you call fooStore.isItem(someBarObect), it will be true, and if you do a fooStore.fetchItemByIdentity(”/bar/1″) you will get a bar object. What that means, is that it seems to me that as long as you can create nested grid (or popup, or some other ui), the dojo.data mechanism to do this is already available. Or am I missing something?

    Comment by maulin — August 14, 2008 @ 5:35 pm

  4. [...] its been a while since my first post about a search widget for dojo.data stores. There are a lot of reasons — beautiful Portland, OR summers being the most [...]

    Pingback by Medryx Observations » dijit.Search Part 2 — September 10, 2008 @ 7:47 pm

RSS feed for comments on this post. TrackBack URL

Leave a comment

Powered by WordPress