joestelmach.com

Live Search With Ruby On Rails Part 2

In a previous post, I outlined the steps necessary to implement live search functionality in a Ruby On Rails application. That post brought forth some reservations I had regarding the semantics and accessibility nuances associated with the ajax-style search that I hope to (mostly) remedy in this post. Let's review the major problems I had with the original implementation:

  1. We use an <input> tag that isn't wrapped with a <form> tag.
  2. Rails provides a text_field html helper that seems unusable.
  3. The observe_field kind of just hangs out on the page.
  4. The search results are not persisted in the session.
  5. A client's lack of Javascript or XMLHttpRequest support will cause serious confusion and a lack of any search functionality.

After reviewing these things for a couple of days, I was able to eliminate points 1, 4, and most of point 5. I'm happy with this since I feel that these are the biggest showstoppers (especially point 5,) as I've come to realize that point 2 and 3 are debate able, subtle, and probably not that important in the grand scheme of things. That being said, let's jump in and give our search functionality some enhanced accessibility.

As I said previously, I'm not a big fan of web sites that limit the core functionality (especially those that just sit there and act stupid,) when the user has Javascript disabled. There are lots of reasons why people would disable Javascript: corporate office policy, too many pop ups asking to buy porn, or maybe Grandma did it by accident when she was tinkering with the IE preferences trying to make the font size bigger. Whatever the reason, I feel that we need to accommodate non-Javascript users with very basic site functionality such as a search feature. I'm not advocating that we stop innovating and pushing the envelope of Javascript-only features like auto-spellcheck or auto-save, I'm just saying that being able to search never required Javascript in the past, and it shouldn't require it now or in the future.

Let's go ahead and iterate through each point and see what we can do to correct things.

- We use an <input> tag that isn't wrapped with a <form> tag.

This is easy enough to fix. Just wrap the thing in a form element. No harm done, and this will also set us up for point number 5. While we're at it, let's go ahead and add an action and a submit button to the form, as we'll need those for step 5 as well.

<%= start_form_tag :action => 'search_no_javascript' %>
  <input type="text" id="search" name="criteria" />
  <%= submit_tag 'Go' %>
<%= end_form_tag %>

- Rails provides a text_field html helper that seems unusable.

I'm still at a loss on this one. I suppose the Rails framework assumes that all form elements will correspond to something in the data model. In our example here, a search function should certainly not be a part of the data model. I'm sure there are other exceptions, so either I'm missing something that the framework provides, or I'm being a bit too anal here and need to stop looking so deeply into these things.

- The observe_field kind of just hangs out on the page.

Once again, I'm probably looking a bit too deep into this, but I do feel that the observers for an ajax-heavy site should probably be declared in a central, easily maintainable place, rather than in the actual markup. However, I admit that the jury is still out on this one, and we are dealing with only a single observer here, so let's forget about this for now.

- The search results are not persisted in the session.

This could be regarded as a matter of personal preference, but I believe that a persistent list of search results will make it easier for the person searching to find what they are looking for. This is especially true in our case here, where we are showing the titles of blog entries that contain the search criteria in the blog's actual contents, but not in the title.

The fix for this is pretty obvious: use session variable instead of instance variables. The only thing we need to be careful about here is handling the difference between no results, and not submitting any search criteria. The subtlety is in the fact that submitting no search criteria will in fact return no results (admittedly, this is based on your search implementation,) but we probably don't want to show the user any information regarding the results if no criteria has been sent.

To better coincide with our next topic, I've implemented the search results as a partial named _search.rhtml:

<%if session[:items] != nil &&
  (session[:mark_term] == nil||session[:mark_term].length> 0)%>
  <dl id="searchResultsDl" class="linkList">
    <dt>
      <%= session[:items].length %>
      Result(s) for
      <% session[:mark_term] %>
    </dt>
    <% for blog in session[:items] %>
      <dd>
        <% session[:mark_term] ? highlight(blog.title,
          session[:mark_term]) : h(blog.title) %>
      <dd>
    <% end %>
  </dl>
<% else %>
  &nbsp; #this represents the subtlety mentioned above
<% end %>

- A client's lack of Javascript or XMLHttpRequest support will cause serious confusion and a lack of any search functionality.

We now arrive at the heart of the matter. How do we gracefully degrade this thing? We started by wrapping our search criteria input field with a form element, assigning it an action of search_no_javascript, and adding a submit button. As you've probably guessed, we're going to implement an action in our controller to be used when the user has Javascript disabled. The search functionality will be identical to the standard Javascript-enabled search, so you can go ahead and extract that implementation into a separate private method called something like 'get_search_results'. What concerns us here is the final rendering decision of the action.

Previously, we rendered our search results directly into the searchResults div element with no layout. We're gonna change that a bit here so the Javascript and non-Javascript versions will play nice together. As I stated before, the search results rhtml has been moved to a partial named '_search.rhtml', instead of being inside a 'search.rhtml' file to coincide with the 'search' function:

<div id="searchResults">
  <%= render :partial => "search" %>
</div>
Note that this implementation is also a bit more semantically correct than our previous implementation, which displayed an empty div in the markup.

Now let's take a look at our two search actions in our controller:

def search #Javascript-enabled search
  get_search_results
  render :partial => "search"
end

def search_no_javascript #Javascript-disabled search
  get_search_results
  redirect_to :back
end
When Javascript is enabled, the search method is invoked from the observer, and the results are rendered using the partial _search.rhtml within the searchResults div. But wait, the _search.rhtml partial has already being rendered within the searchResults div, since we specifically placed it there. However, this rendering happens only on the page load. Once you give an observer a target element, the contents of that element will be replaced with the result of the action given to the observer. So in our case here, its kind of like we are hitting the refresh button on just searchResults div (which I must admit, is quite bizarre when you step back and think about it.) When we throw persistent search results into the mix, this method works beautifully, as hitting refresh will render the most current search results within the searchResults div.

Now, what happens when grandma turns off Javascript while trying to make her fonts bigger? Well, since we added a form and a submit button, she can go ahead and submit her search criteria the old-fashioned way: by hitting the submit button (I can hear the 'When I was your age' stories now!) This will invoke the search_no_javascript method in our action, which will gather the search results in the same way as before, but instead of rendering the results instantly within the searchResults div (which we realize is impossible without Javascript,) we simply re-direct her back to whatever page she was looking at when the search was submitted. A re-direct will cause the browser to make a new request for the page, thereby re-rendering the search results we just stuck in the session.

We're almost done here, but we have a couple more things to discuss. We don't want our users to see a search button if they have Javascript enabled. Similarly, we might want the label of the search field to say 'Live Search' when Javascript is present, and just plain 'Search' when it is not present. This can easily be achieved by initially setting the label to 'Search' and adding a small script just after the form element:

<script type="text/javascript">
  document.getElementById("searchSubmit").style.display = "none";
  document.getElementById("searchLabel").innerHTML = "Live Search";
</script>
This piece of script will run only when Javascript is enabled (obviously,) and will swap the label to 'Live Search' and hide the submit button. Some would argue that this should be placed in the body's onload attribute, as it should run when the page is loaded. However, the onload method requires the entire page to be loaded before the script is invoked, which causes a delay in its execution. This means that the user can actually see the label change and the button disappear.

One last point and then I'll shut up. We still haven't touched on the scenario of having Javascript enabled but no XMLHttpRequest functionality. As we all know, most recent browsers do in fact have this support, so I don't view this as being a big deal. However, since this is a post about accessibility, I should add that those users will still be able to use our search by entering their criteria and hitting enter, which will in turn submit the form. The only thing we lose is the visibility of the submit button, and the generic 'Search' label. I think I can live with that.


Update - 5/1/2006: I've added a followup to this post here.

Comments
blog comments powered by Disqus