customing Blacklight: a limit checkbox

This post will be of interest to programmers, who work on Blacklight apps or are interested in Blacklight’s architecture.

Locally, we wanted to add a checkbox to the search form for “Show only items available online”. I will walk through how I implemented this in my local Blacklight application, over-riding standard Blacklight behavior.

This “Online only” search limit is already available after you do a search, by choosing from the facet limits on the left-hand side. But as this is the most-used limit, we wanted to offer a checkbox right next to the search form.

I decided I wanted this checkbox to result in the exact same URL as if you had chosen the Format=Online limit from the facets, so as to integrate as seamlessly as possible and keep using default Blacklight behavior for actually applying and displaying limits.

The URL parameters that Blacklight uses to express facet limits are a bit ugly. They use Rails (nee PHP) style for serializing nested data structures in a URL. To limit the ‘format’ facet to the value ‘Online’:  &f[format][]=Online.  Meaning, add the value ‘Online’ to an array that is the value of the ‘format’ key of the ‘f’ hash.  (Except even uglier URI-escaped).

But for now we don’t want to try to change this, we just want our checkbox to add that very same value when checked.

Customize search_form partial template

First step is creating a localization of the built-in Blacklight search_form partial, that displays the search form. In fact, I’ve already done that to add some other localizatios (such as link to ‘advanced search‘).

But if I hadn’t, I’d copy the file from blacklight’s app/views/catalog/_search_form.html.erb to my own application’s local app/views/catalog/_search_form.html.erb.  Now the running application will use the template found in my local app instead of the one supplied by Blacklight, and I can customize it.

I add this to the search form, to create the checkbox:

    <div>
      <%= check_box_tag 'f[format][]', 'Online', facet_in_params?(:format, "Online"), :id => 'online_only' %>
      <%= label_tag(:online_only, "Show only items available online", :id => "online_only_label") %>
    </div>

Add it in a ‘div’ just for an easy way to force it to be on it’s own line. Checkbox with name “f[format][]”, and value “Online” — now when it’s checked and the form is submitted, param f[format][]=Online will be added to the reuqest, great.

Note that we’re hard-coding in the fact that this weird URL key represents this facet limit. If Blacklight in the future figures out a better way to encode these, our code here won’t be forwards-compatible, we’ll have to go back and fix it. Oh well. We are using an existing Blacklight-supplied helper method to determine if the checkbox should be checked, <code>facet_in_params(:format, “Online”)</code> — we could have done that ourselves too, but the helper was already there, so convenient to re-use it, and that way we do get forwards-compatibility for that element anyway.

(I don’t think we need to escape it in the call — it does wind up un-escpaed in the <input name=..., but I think that’s appropriate, and the browser will escape on submit. At any rate it works, hopefully I’m not relying on the browser rescuing from me doing something wrong).

I also give it a linked <label>. And I put an :id on the label (as well as the input), so I can style em custom later — the default blacklight styles do some odd things to html labels for some reason, I’ll need to over-ride to make it more normal.

So this comes close to working, I can check the checkbox and it works, but I can’t ever remove the check again… what’s going on?

Customize search_as_hidden_fields

If you look at the source you copied over from _search_form.html.erb, you’ll see inside the <form>, there’s a call to:

<%= search_as_hidden_fields( :omit_keys => [:q, :search_field, :qt, :page]) &>

This adds the existing search context to the form as hidden fields, so the user doens’t lose their current facet limits when changing their query. The problem is that this is adding a hidden field for my &f[format][]=Online value. Then the checkbox adds this again if it’s checked, resulting in two of those in the resulting request. Or if the checkbox isn’t selected, well, there’s still one of em, that facet field was still ‘sticky’. Oops.

So when we generate the search form, we want to make sure those hidden fields don’t include the f[format][]=Online param, even if it is in the current search context — we’ll let the checkbox supply that (or not) instead.

And we can see from the call that the search_as_hidden_fields method (supplied by Blacklight) already let’s us tell it some keys to omit from the generated hidden fields. But we dont’ watn to omit a top-level key (don’t want to omit the entire f hash), we only want to eliminate one particular value in it if present. There’s no support for that.

This is probably a generally useful feature, asking search_as_hidden_fields to leave out a particular facet limit value. So maybe ideally I’d enhance Blacklight itself to do that, but for now, need to get this done and things are confusing with the transition in Blacklight (and my local app) from Rails2 to Rails3. So this will give us an opportunity on how to locally over-ride a helper method like that from Blacklight instead. (I might add this feature to Blacklight itself in the future making this customization, so if you’re reading this in the future, worth a check).

Now, this method is called probably all over the place in Blacklight, and we don’t want to change it to ALWAYS leave out the format=Online limit (or that limit would get lost from search context other places where there’s no checkbox — like the change-sort-order form!). Instead, we want the method to take an option telling it to leave this out, that we’ll excersize when calling it from the search form.

And heck, let’s make it a general purpose option where the caller specifies the particular facet and value to omit. We want our local version to apply this option, but then somehow call out to the original method for the actual implementation, because we dont’ want to duplicate that in oru local app — we want to call out to BL logic, so if the BL logic changes, we’re still good, we haven’t frozen it in our app.

Turns out this method, while it operates in the current request #params by default, takes an option where you give it the request params to serialize to hidden fields. So no problem, if our special option is called, we’ll make a copy of the current request params, remove the undesirable limit, and then pass that copy to the original implementation.

It turns out that the way you both over-ride a helper method but still call out to the original implementation is a bit different in Blacklight 2.x with Rails2, vs. Blacklight 3.x with Rails3. I’ll show you both ways.

In the Rails2 version, we need to create our own app/helpers/application_helper.rb, but have it require_dependency require_dependency 'vendor/plugins/blacklight/app/helpers/application_helper.rb', then re-define the method, using crazy alias_method_chain trickery to still have access to the original definition:

# app/helpers/application_helper
require_dependency 'vendor/plugins/blacklight/app/helpers/application_helper.rb'
module ApplicationHelper
  #....

    def search_as_hidden_fields_with_local(options = {})
    options[:params] ||= params
    if options[:omit_facets] &amp;&amp; options[:params][:f]
      options[:params] = options[:params].dup
      options[:params][:f] = options[:params][:f].dup

      options[:omit_facets].each_pair do |facet, values|
        facet = facet.to_sym
        values = [values] unless values.kind_of?(Array)

        next unless options[:params][:f][facet.to_sym]
        options[:params][:f][facet.to_sym] = options[:params][:f][facet.to_sym].dup
        options[:params][:f][facet.to_sym].delete_if do |v|
          values.include? v
        end
      end
    end
    search_as_hidden_fields_without_local(options)
  end
  alias_method_chain :search_as_hidden_fields, :local

end

The Blacklight3/Rails3 version is somewhat more straightforward, we can actually just call ‘super’ to access the original implementation, in an actual Object-Oriented over-ride, and don’t need that require_dependency business.

# app/helpers/application_helper.rb
module ApplicationHelper
  #....

  # Over-ride this helper from blacklight core, to provide
  # option to remove 'f[format][] = Online' from params, to
  # accomodate us displaying it as a checkbox in search form.
  def search_as_hidden_fields(options = {})
    options[:params] ||= params
    if options[:omit_facets] && options[:params][:f]
      options[:params] = options[:params].dup
      options[:params][:f] = options[:params][:f].dup

      options[:omit_facets].each_pair do |facet, values|
        facet = facet.to_sym
        values = [values] unless values.kind_of?(Array)

        next unless options[:params][:f][facet.to_sym]
        options[:params][:f][facet.to_sym] = options[:params][:f][facet.to_sym].dup
        options[:params][:f][facet.to_sym].delete_if do |v|
          values.include? v
        end
      end
    end
    super(options)
  end

end

The logic in both versions are the same

  1. If no :params option is defined, default to the current request params. (This is what the original implementation of search_as_hidden_fields does).
  2. If an :omit_facets option is provided, and there are facets in the supplied params, then:
  3. make copies of all relevant data structures before modifying. (Mutating the original request #params is bad, any other code that executes after will be mis-informed about the current request params itself. I’ve been bitten by that bug too many times. Take pains never to mutate Rails own controller#params hash!).  I’ve written it to accept an :omit_facets option, with either a single value or an array of values, either way specifying which facet the value should be omitted from, eg:   :omit_facets => {:format => "Online"}, or :omit_facets => {:format => ["Online", "Books"]}.
  4. Then call the ‘original’ search_as_hidden_fields implementation, passing our new params with the undesired facet values removed

Then to our localized _search_form.html.erb, we add our newly supported :omit_facets param to the search_as_hidden_fields call:

    <%= search_as_hidden_fields(:omit_keys => [:q, :search_field, :qt, :page], :omit_facets => {:format => "Online"}) %>        

Success!

So in just a handful of lines of code, this pretty much just works now. The limit applied by clicking the checkbox will be treated identically to actually selecting the limit from the facet sidebar.  They’re interchangeable, to the extent that if someone does select “Online” from the facet sidebar, the checkbox will become checked. And if you select the checkbox, the limit will subsequently be echo’d as usual for limits in the sidebar, and in the constraints display below the search box — and can be removed either by unselected the checkbox, or by the usual means of removing facet constraints.

Some of this could be considered a bug or a feature, in particular I’m not sure about the “double display” of the online limit, in both the checkbox and the constraints display below the searchbox.

If we wanted to eliminate it from the ordinary constraints display, it would take some more customizing — but at present, I’m not even sure if it’s undesirable rather than desirable at all, and even if it is undesirable it’s probably good enough.

Coming up in a subsequent blog post, I’ll demonstrate some search UI customization that requires a bit more work, requiring intrustion into Blacklight’s routines for creating the Solr request, to support a feature that wasn’t there at all before.

Advertisement

One thought on “customing Blacklight: a limit checkbox

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s