How and why I built SearchAreaControl plugin

Published May 03, 2018Last updated May 23, 2018
How and why I built SearchAreaControl plugin

About me

My name is John Kapantzakis and I am a web developer from Greece. I work as a full time developer for the last 4 years in the beautiful town of Thessaloniki, using technologies like ASP.NET/C#, Javascript/Typescript and HTML/CSS.

The problem I wanted to solve

Working on a product that handles HR data, there is often the need to display complex data structure in a form that the user should be able to filter and select specific items from a collection. Such a structure might be the relationship between the departments and the employees of a company. Each employee belongs to a department and each department belong to other department, except from one, the top node. This kind of structure can be described as a tree.

There are some available controls that can be used to display a tree data structure, like Telerik's RadDropDownTree control, which offers search functionality, multiselection, child nodes selection, check all and clear selection and many more.

But RadDropDownTree control did not fully cover our requirements. We wanted to be able to achieve some extra functionality like:

  • Display the tree structure in a broader view
  • Revert selection functionality
  • Search by regular expression
  • Ability to add custom functionality

What is SearchAreaControl?

SearchAreaControl is a jQuery plugin that lets you display, search and select one or multiple items from a tree data stucture. The plugin has been designed to be initialized on button elements. Once the plugin is initialized on a button, it bounds the click event with the appearance of a popup that holds the datasource that the user defined in the options on the initialization process. The following picture illustrates a typical use case of the plugin.
searching.gif

Building the plugin

Lets start with a simple jQuery plugin that, when initialized on a jQuery collection, applies some css rules to the selection.

; (function ($, window, document, undefined) {
    var pluginName = 'searchAreaControl';
    function Plugin(element, options) {        
        this.$el = $(element);
        this.opt = options;
        this.init();
    }
  
    Plugin.prototype = {
      init: function() {
          this.$el.css('color',this.opt.color);
      }
    }
  
    $.fn[pluginName] = function (options) {
        return this.each(function() {
            new Plugin(this, options);
        });
    }

})(jQuery, window, document);

Initializing the plugin on a HTML element, like this

$('#myElem').searchAreaControl({ color: 'green' });

will result on applying the css rule color:green (as passed through the plugin's options object) to this element. You can see a working demo here.

Now, lets explain the above code step by step. First, take a look at the following piece of code.

$.fn[pluginName] = function (options) {
    return this.each(function() {
        new Plugin(this, options);
    });
}

According to the jQuery Learning Center, this is the way to write a basic jQuery extension, or plugin. Just add a function with the desired plugin name to the $.fn (equivalent to $.prototype) and you are ready to go! Note that inside the function body, we return the this.each(...) method to maintain jQuery's chainability.

The next step is to wrap the above code inside a IIFE in order to create a lexical scope for our plugin and prevent global namespace pollution. As you can see, we pass jQuery, window and document as arguments to the self executing function, assigning their values to $, window, document and undefined, respectively.

; (function ($, window, document, undefined) {
    var pluginName = 'searchAreaControl';
    $.fn[pluginName] = function (options) {
        return this.each(function() {
            new Plugin(this, options);
        });
    }
})(jQuery, window, document);

But, wait a minute, we have passed 3 arguments, but we assign values to 4 variables, leaving the undefined variable with no value. Exactly! That's what we are trying to achive here, we try to protect the undefined property from a potentialy modified value of undefined from the global scope (Someone would just have declared that undefined = 'test') by assigning nothing to the undefined property, so it woulb be truly undefined. The leading ; protects against not properly closed scripts that may be present before our script.

Finally, we can add the Plugin function declaration and add the init method using the Object.prototype property. This is the code base that we are going to extend in order to end up with the final plugin.

Declaring default options

At the current state, if we try to initialize our plugin without providing an options object, we will get a Uncaught TypeError: Cannot read property 'color' of undefined error. We need to provide a fallback in case our plugin gets initialized with no options object.

; (function ($, window, document, undefined) {
    var pluginName = 'searchAreaControl';
    function Plugin(element, options) {        
        this.$el = $(element);
        this.opt = $.extend(true, {}, $.fn[pluginName].defaults, options);        
        this.init();
    }
  
    Plugin.prototype = {
      init: function() {
          this.$el.css('color',this.opt.color);
      }
    }
  
    $.fn[pluginName] = function (options) {
        return this.each(function() {
            new Plugin(this, options);
        });
    }
    
    $.fn[pluginName].defaults = {
        color: 'green'
    }

})(jQuery, window, document);

Now, we can initialize the plugin with no options object and still get a green text. We can achieve that in two steps. First, we add the defaults property to the $.fn.searchAreaControl, declaring it's value to be our default options object:

$.fn[pluginName].defaults = {
    color: 'green'
}

Then, we have to merge the recieved options object with the default options declared inside the plugin. We can achieve this with jQuery's extend() method like this:

this.opt = $.extend(true, {}, $.fn[pluginName].defaults, options);

Declaring public methods

Once the plugin has been successfully initialized, we might want to get the selected values, or clear the selected items etc. This could be achieved using some public methods (the API) exposed by the plugin like this:

let selectedNodes = $('#myElem').searchAreaControl('getSelectedNodes');

In this case, getSelectedNodes is a public method, exposed by the plugin. But how do we tell the plugin to read the method name passed as an argument in the plugin constructor? We have to change the code in order to perform the following check, each time we call the plugin constructor:

  1. If the plugin is called without arguments (options === undefined) or if we pass an object (typeof options === 'object'), return the new plugin instance
  2. If the plugin is called with arguments of type string (typeof options === 'string'), call the respective method
$.fn[pluginName] = function (options) {
  var args = arguments;
  if (options === undefined || typeof options === 'object') {
    // New plugin instance with chainability
    return this.each(function () {
      if (!$.data(this, 'plugin_' + pluginName)) {
        $.data(this, 'plugin_' + pluginName, new Plugin(this, options));
      }
    });
  } else if (typeof options === 'string') {
    // Return method result (no chainability)
    var instance = $.data(this[0], 'plugin_' + pluginName);
    return instance[options].apply(instance, Array.prototype.slice.call(args, 1));
  }
}

We also have to declare the getSelectedNodes() pulic method using the Object.prototype property like this:

Plugin.prototype = {
  ...
  // Private method
  _getData_SelectedNodes: function() {
  		// Return selected nodes
  },
  // Public method
  getSelectedNodes: function() {
    return this._getData_SelectedNodes();
  }
}

This is the main idea of how we declare private and pulbic methods inside the plugin. For further detail, you are encouraged to check the latest version that is available in the plugin's repo:

Repo

What happens when the plugin is called

As soon as we call the plugin's constructor, the following steps take place inorder to construct the final user control:

  1. The 'searchareacontrol.beforeinit' event is triggered to notify that the plugin is about to begin it's construction
  2. The plugin sets the defined text to the target element[1]
  3. The plugin sets the provided datasource
  4. The 'searchareacontrol.beforebuildpopup' event is triggered to notify that the plugin is about to start building the popup HTML markup
  5. The popup that is going to show up on main button click and holds the tree data structure (datasource) is built
  6. The 'searchareacontrol.beforeinitsearcharea' event is triggered to notify that the popup has been successfully built and the plugin is ready to get initialized
  7. This is the step where the plugin build the final elements (popup header, body and footer with various buttons)
  8. The plugin adds a new event listener to listen at the click event on the target element in order to show the popup

The initialization process has now be completed and we are ready to call any public method on the specific plugin's instance.

Tips and advice

Generally, during the development process of any project, you realize that you could have done some things differently. Here are some tips that I think can help to improve our code quality and maintainability.

Distribute and isolate your code into small (as possible) functions that perform one task per function and always return the same result given the same parameters (Pure functions). Small pieces of code that produce predictable results are easy to maintain and be tested. Having distributed your code into small isolated functional parts helps you to keep your codebase more organized and readable as the project grows.

Testing your code including unit tests is always a very good idea. Imagine that you have created a method that performs a specific task and, after a while, you make some changes to another place that affect this method's functionality. Having set up some unit tests, checking the method's functionality whould have caught up the error and notify you to fix the code before shipping a new version with a hidden bug. (I will definitely add unit tests in the project in one of the upcoming versions! 😃 ).

Although a clean and readable code with meaningful method names provide some information about your project, I think you should always document your code, especially if you develop an open source project that to be used by other developers. Think of your self trying to use a new plugin you downloaded, but you have to figure it out without documentation, by just looking at the code.

Keep a changelog inside your project's folder, so that you, and anyone interested, could follow up with the latest changes, additions and bug fixes of your project.

Summary

In this post we have tried to illustarte the process of building a jQuery plugin, and described the way that the SearchAreaControl plugin gets initialized and operates on target elements. We have, also, pointed some tips and advice that we think they facilitate to procuce quality and maintainable code.

Thank you for reading this post! Please don't hesitate to post a comment if you have read anything innacurate or if you just want to say hello!

Refernces


  1. The element that the plugin is initialized on ↩ī¸Ž

Discover and read more posts from John Kapantzakis
get started