Creating a jQuery Searchbox Widget

In my last post I used RequireJS to load my search box. It allowed me to abstract away all of the dependency tracking to someone else. I can go one step further with this and turn our Searchbox object into a jQuery widget. The advantage is that my code within the initialization becomes very simple indeed. Here is the entire initialization code:

requirejs(['jquery', 'components/searchbox', 'bootstrap'], function($, searchbox) {
  $(document).ready(function() {
    $('#nav-search').mySearchBox();
  });
});

I don’t even need a temporary variable to hold my object. I just associate the search box code with my icon. It’s a much cleaner interface. Let’s look at how I did this.

Most of my work is going to be in the components/searchbox.js file. I need to write a “jQuery plugin”. You can find out a whole bunch about this topic on the jQuery website. I’ll go through the very basics here. I’m still going to be using RequireJS so my project depends on both RequireJS and jQuery. Here is my basic layout:

define([
  'jquery', 
  'components/utils'
], function($, utils) {
  $.fn.mySearchBox = function() {
    return $(this).each(function() {
      $(this).data('my-searchbox', new $.MySearchBox($(this)));
    })
  };
});

This is a model for a jQuery plug-in with no options. The $.fn.mySearchBox defines my plugin name – in this case mySearchBox – and I’ll call my plugin with $(selector).mySearchBox(). The context of the jQuery element is passed in so it’s available as $(this). I can use $(this).data(key, value) to associate some data with our element in the DOM – in this case that’s an instance of the class with all my search box code in it. I can access this element using $('#nav-search').data('my-searchbox') later on if I need to.

Now I need a class. I’ve placed MySearchBox in the jQuery namepsace since it always exists when using jQuery. However, you may want to create your own namespace for this as well. I just need to be able to call new on it to create a reference to a new object based on the class. I have not added it yet, but I’ll do that now – here is my complete components/searchbox.js file:

define([
  'jquery', 
  'components/utils'
], function($, utils) {
  
  $.fn.mySearchBox = function() {
    return $(this).each(function() {
      $(this).data('my-searchbox', new $.MySearchBox($(this)));
    });
  };

  $.MySearchBox = function(pElement) {
    var clickElem = pElement,
        searchId = utils.generateGUID();

    // Click Handler definition for this Searchbox
    function onClick(evt) {
      var elem = $(evt.currentTarget), data = evt.data, srch = $('#' + data.searchId);

      // If the search box exists, remove it and move on.
      if (srch.length > 0) {
        srch.remove();
        return;
      }

      // Calculate the best location for the box.  It needs to align
      // with the left edge of the search icon (-8 pixels) and be
      // 4 pixels below the bottom edge.
      var left = elem.position().left - 22,
        top = elem.position().top + elem.height() + 12,
        maxWidth = 350;

      // Calculate the best width for the box.  It should not go
      // over the size of the screen
      var width = ((left + maxWidth) < $(window).width()) ? maxWidth : $(window).width() - left;

      // IF the search box does not exist, then we need to create it.
      var div = utils.createDIV(data.searchId, 'bubble', left, top, width)
        .html("<div class='search'><input type='search' name='search' placeholder='Search'></div>");

      // Focus on the search box
      $('input[type=search]', div).focus();
    }

    function onBodyClick(evt) {
      var s = $('#' + searchId);
      if (s.length > 0) {
        if (utils.isLocatedIn(evt.pageX, evt.pageY, clickElem) || utils.isLocatedIn(evt.pageX, evt.pageY, s))
          return;
        s.remove();
      } 
    }

    // Register the click handler on our icon
    pElement.css({ 'cursor': 'pointer' }).click({ 'searchId': searchId }, onClick);

    // Public Interface
    return {
      // Return a jQuery reference for the search box, or null
      getSearchBox: function () {
        var s = $('#' + searchId);
        return (s.length > 0) ? s : null;
      },
    };
  };
});

Notice anything about the class? Well, aside from some purely cosmetic changes in the code, this is the same code as the object-ified version of the Searchbox. This is the beauty of reusable code – I’ve used it straight-up, in a RequreJS environment and now as a jQuery widget. I haven’t fundamentally changed the code since I created it.

There are some more things you can do here. For instance, let’s say you wanted to pass some options to the MySearchBox() – something like:

$('#nav-search').mySearchBox({
    bubbleClass: 'bubble-search'
});

Here I am creating an options object. I need to pass that into the class as a parameter, and I’ll want some default options. Firstly, let’s alter $.fn.mySearchBox to handle options:

  $.fn.mySearchBox = function(pOptions) {
  
    var options = $.extend({
      bubbleClass: 'bubble'
    }, pOptions || {});
  
    return $(this).each(function() {
      $(this).data('my-searchbox', new $.MySearchBox($(this), options));
    });
  };

The $.extend call merges the default options with what I supplied in the embedded object. Using pOptions || {} means I don’t have to pass anything – it will just merge nicely and at the end the options object will be the defaults I have established. If I do provide something then it will overwrite what is in the defaults. Now I can pass the options object into theclass and be assured that all the right properties will be there. Of course I have to update the class to account for this. Firstly I need to store it for later access:

  $.MySearchBox = function(pElement, pOptions) {
    var clickElem = pElement,
        options = pOptions,
        searchId = utils.generateGUID();

I also need to update my createDIV call to use the established options:

      // IF the search box does not exist, then we need to create it.
      var div = utils.createDIV(data.searchId, options.bubbleClass, left, top, width)
        .html("<div class='search'><input type='search' name='search' placeholder='Search'></div>");

Let’s create a dark-background version of the bubble CSS classes. Edit the site.css file and add the following:

.dark-bubble {
  background-color: blue;
  border: 1px solid #C0C0C0;
  border-radius: 8px;
  box-shadow: 2px 2px 2px 2px gray;
  color: white;
}

.dark-bubble::before {
  content: '';
  width: 0;
  height: 0;
  position: absolute;
  left: 20px;
  top: -16px;
  border-left: 16px solid transparent;
  border-right: 16px solid transparent;
  border-bottom: 16px solid blue;
}

.dark-bubble .search {
  background: url("../img/search-32.png") no-repeat;
  padding-left: 40px;
  height: 32px;
  margin-top: 8px;
  margin-left: 8px;
  background-color: blue;
}

.dark-bubble .search:before {
  content: '';
  width: 0;
  height: 0;
  position: absolute;
  left: 22px;
  top: -14px;
  border-left: 14px solid transparent;
  border-right: 14px solid transparent;
  border-bottom: 14px solid blue;
}

.dark-bubble .search input {
  font-size: 18px;
  border: 0;
  margin-top: 2px;
  color: white;
  background-color: blue;
}

I also need to adjust the init.js call to provide the bubbleClass property:

requirejs(['jquery', 'components/searchbox', 'bootstrap'], function($, searchbox) {
  $(document).ready(function() {
    $('#nav-search').mySearchBox({ bubbleClass: 'dark-bubble' });
  });
});

When I run this page now I get a blue background instead of the white background. I could adjust other properties of my search box as well – see if you can add a width property that sets the width of the popup search box.