A Different Approach to jQuery Plugins with Legacy Applications

jQuery has been around for a long time and is more popular than Flash. If you know jQuery and C#, today's post will show you how to create a jQuery plugin that can look like a C# class.

Written by Jonathan "JD" Danylko • Last Updated: • Develop •
Lamb eating grass from a piece of paper

There are a ton of JavaScript frameworks out there.

However, I firmly believe legacy applications outweigh them. There are tons of legacy applications out there that don't even know how to spell JavaScript. ;-)

I'm kidding, but honestly, there will always be legacy applications with various JavaScript sprinkled throughout the app.

It doesn't make sense to pick a JavaScript framework like Angular or React frameworks and insert them into your web app, and Voila...you have a modern web app.

We all know it doesn't work that way in the real world.

It just takes a little perspective to figure out a better way to organize JavaScript.

After working with jQuery/JavaScript for a while, your scripts start to pile up in your web application. This is where jQuery Plugins help you "partition" your application into modules.

Recently, I showed someone how I use jQuery on a page-by-page basis. They thought it was definitely something worth sharing with others.

In this post, we'll go through the process of building a jQuery plugin from scratch and provide a page-based template allowing you to refactor into a more maintainable web app instead of a JavaScript ball of mud.

Where do we start?

We all have our JavaScript library we love to carry with us, right? Common routines that make our JavaScript coding easier.

Luckily for us, module-loading libraries nowadays are easy to implement and I decided to go with SystemJS.

First, I have a common library that holds some general purpose routines.

/scripts/common.js

$.plugin = function (name, object) {
    $.fn[name] = function (options) {
        var args = Array.prototype.slice.call(arguments, 1);
        return this.each(function () {
            var instance = $.data(this, name);
            if (instance) {
                instance[options].apply(instance, args);
            } else {
                instance = $.data(this, name, Object.create(object).init(options, this));
            }
            return instance;
        });
    };
};

// Make sure Object.create is available in the browser if (typeof Object.create !== 'function') {     Object.create = function (o) {         function F() { }         F.prototype = o;         return new F();     }; }

The first part of this common.js is the setup of our plugin for jQuery.

This function gives us an easy way to make our plugin. It receives the argument and splits them and applies them to the instance of the jQuery function.

If the object doesn't exist on the DOM element, then we attach the object to the DOM using the $.data function.

If the object is already there, we assign the object to the "instance" parameter for retrieval.

Next, we have the simple Object.create method that creates a new object that uses an old object as its prototype. It's a simpler way to create JavaScript objects.

If you want more details on this particular method, check page 22 in the book titled JavaScript: The Good Parts by Douglas Crockford.

This is all that's required across all pages for this technique to work.

jQuery Plugin Code

Next is the actual use of the jQuery plugin. We set up our function to connect our jQuery code to each DOM element.

Anywhere you see xxxModule, I always replace it with the name or function of my page (i.e. SearchPageModule, GridPageModule). See the line near the end? I always make my string name lowercase.

Now, we are ready to make our plugin.

(function ($) {
    // Start a plugin
    $.fn.mainModule = function (options) {
        if (this.length) {
            return this.each(function () {
                // Create a new module object
                var pageModule = Object.create(mainModule);

                // Run the initialization                 pageModule.init(options, this); // `this` refers to the element
                // Save the instance in the element's data store                 $.data(this, "pagemodule", pageModule);             });         }     }; })(jQuery);

This plugin code will cycle through all of the elements selected (1 or many), call the init method in your code, and save the code to each DOM element using the $.data method so we can retrieve it later.

We are also passing in options through the constructor in case you want to set defaults for your page.

THIS should look familiar

This next portion provides all of the code in a Method Invocation pattern (taken page 28 of JavaScript: the Good Parts). It's similar to a C# class, but stored in a variable.

This skeleton is used in a number of intranet/Internet applications and works quite well in production along with a "module" mentality.

Using this jQuery code in this manner should feel right at home if you've built C# classes.

var mainModule = {

    // variables in one location     // gridName: ".userGrid",
    init: function (userOptions, elem) {
        this.options = $.extend({}, this.options, userOptions);
        this.$elem = $(elem);         this.selected = "";
        if (userOptions && userOptions.onInit) {             this.options.onInit = userOptions.onInit;         }
        this.build();
        console.log("Initialization Complete.");
        return this;     },
    // These are the default options (and optional) :-)     options: {         css: { "width": "100%" },
        // in case you want to check something in your JS code.         debug: false,
        onInit: function (e) { }     },
    build: function () {
        // DOM additions/replacements         // this.createTable();
        // Attach the CSS to the table.         // this.applyCSS();
        // Hook up events.         this.options.onInit(jQuery.event);         // this.connectEvents();     },
    applyCSS: function () {
        // $(this.gridName).css(this.options.css);
    },
    connectEvents: function () {
    },
    /////////////////////////////////////////////////     // Definition of Events.     ////////////////////////////////////////////////
    onInit: function (e) { }
};

A couple notes regarding this code:

  • This is merely a template to expand upon. I've even used this template to manage SignalR connections and responses back from the server.
  • Anywhere you have a jQuery call, I never did like using those "magic strings" scattered everywhere in the code so I decided to place the "property" names at the top (i.e. gridName). Check out the commented out line in the applyCSS function.
  • You can add other functions as well into your object so long as it follows the same pattern of method invocation.
  • init - This is merely the constructor for passing in options AND the element we're working with in our DOM.
  • options - This is your list of defaults along with default events you want handled in the code.
  • build - The main area where you start building your page. You can even use other function names to partition your page further.
  • applyCSS - ..which is what I did with applyCSS.
  • connectEvents - Where the events occur on the page like $(this.gridRow).on("click"...) is defined.
  • Finally, you have your events at the bottom.

The Final Code

Now that we've gone over the JavaScript, here is our final code in two files: common.js (from above) and our mainPage.js.

/scripts/mainPage.js

var mainModule = {

    // variables in one location     // gridName: ".userGrid",
    init: function (userOptions, elem) {
        this.options = $.extend({}, this.options, userOptions);
        this.$elem = $(elem);         this.selected = "";
        if (userOptions && userOptions.onInit) {             this.options.onInit = userOptions.onInit;         }
        this.build();
        console.log("Initialization Complete.");
        return this;     },
    // These are the default options (and optional) :-)     options: {         css: { "width": "100%" },
        // in case you want to check something in your JS code.         debug: false,
        onInit: function (e) { }     },
    build: function () {
        // DOM additions/replacements         // this.createTable();
        // Attach the CSS to the table.         // this.applyCSS();
        // Hook up events.         this.options.onInit(jQuery.event);         // this.connectEvents();     },
    applyCSS: function () {
        // $(this.gridName).css(this.options.css);
    },
    connectEvents: function () {
    },
    /////////////////////////////////////////////////     // Definition of Events.     ////////////////////////////////////////////////
    onInit: function (e) { }
};
// To use multiple scripts, use //Promise.all([ //    System.import('/scripts/common.js'), //    System.import('/scripts/library.js') //]).then(function (modules) { //    var common = modules[0]; //    var library = modules[1]; //}); SystemJS.import('/scripts/common.js').then(function (e) {     // prepare the plugin code.     $.plugin("pagemodule", mainModule);     console.log("Module loaded."); });
(function ($) {     // Start a plugin     $.fn.mainModule = function (options) {         if (this.length) {             return this.each(function () {                 // Create a new multiselect object                 var pageModule = Object.create(mainModule);
                // Run the initialization                 pageModule.init(options, this); // `this` refers to the element
                // Save the instance in the element's data store                 $.data(this, "pagemodule", pageModule);             });         }     }; })(jQuery);
$("body").mainModule();

Notice the familiar jQuery line at the bottom? This is what kicks off your plugin.

Whatever you called your plugin at the bottom (i.e. $.fn.mainModule) is what you call from JavaScript and since we're already at the bottom of our plugin, we might as well call it.

In between your plugin initialization and core code, you'll notice the SystemJs command to load all of your common library routines.

You may be asking why don't we place that at the top of the routine instead of in the middle.

The reason is because we need to load our plugin code from the common.js library first before we can attach the object to our DOM element.

All I have in Index.cshtml is:

@section scripts
{
    <script src="/Scripts/mainPage.js"></script>
}

Conclusion

This post has been in my draft queue for two years and I decided to just throw it out there and see if anyone would find this technique as a benefit.

In this modern JavaScript world, we still have our veteran web applications which need some JavaScript love as well.

Some applications require an overhaul while others just need refactoring whether it's JavaScript or C#.

With this technique, now each page can have it's own JavaScript to manage the DOM with shared code across other pages.

I hope today's post presented a better way to refactor JavaScript with legacy applications instead of justifying a rewrite with a new framework.

Did you find this helpful? Do you use jQuery in a different way? Post your comments below and let's discuss.

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus