Starter Template for Creating a jQuery Plugin

18. January 2012

Every time I needed to create a jQuery plugin, I would debate with myself on how to structure the code. It occurred to me that it would be helpful to stub out a sample plugin template that I could grab and jump right into coding.

After looking around at various jQuery plugin patterns, I came up with the template at the end of this post. I think it has a nice structure, but would enjoy hearing any suggestions. The one thing I may add to this in the future is the ability to programmatically uninstall the plugin.

Here are some samples of how you might invoke the plugin and then call it's API.

// Install the plugin on all matching elements
$("div.myPlugin").myPlugin();

// Install the plugin and override a default setting
$("div.myPlugin").myPlugin({
    playPauseSelector:'#playbutton'
});

// Control multiple instances at once 
//  and also maintain the fluent API using
//  the jQuery plugin function to call the API
$("div.myPlugin").myPlugin("play", 2); 
$("div.myPlugin").myPlugin("play", 2).myPlugin("pause"); 

// Control a single instance by accessing the attached API
// Only the officially supported API methods can be called
$("div.myPlugin").get(0).MyPlugin.play(2); 

Here is the code:

NOTE: Some parts of the code are included only to illustrate aspects of the structure. See the code comments for more explanation.

(function ($) {

    // Create the plugin jQuery function
    $.fn.myPlugin = function (optionsOrCommand, param) {

        // Setup the default options
        var defaultOptions = {
            playPauseSelector: '.MyPluginPlayPauseButton',
            playingClass: 'playing',
            pausedClass: 'paused'
        };
        
        // Define a function to setup the plugin instance
        var create = function (root, options) {
 
            // Grab a reference to the root DOM element that is the target of the plugin
            var _root = $(root);
            var _playPauseButton = null;

            // Update the settings with any overrides
            var _opts = $.extend({}, defaultOptions, (options || {}));
            
            // Create internal helper methods
            var _play = function(idx) {
                _playPauseButton.removeClass(pausedClass).addClass(playingClass);
            };
            
            var _pause = function() {
                _playPauseButton.removeClass(playingClass).addClass(pausedClass);
            };
            
            // Define an init function to kick off the plugin functionality
            var _init = function () {
                _playPauseButton = $(playPauseSelector, _root);
                _playPauseButton.bind('click', function() {
                    var self = $(this);
                    if (self.hasClass(playingClass)) {
                        _pause();
                    } else {
                        _play();
                    }
                });
            };
            
            // Return an object containing a reference to the API and the init method
            // We don't want the init in the API so it is separated here
            return {
                controller: {
                    init: function () {
                        _init();
                    }
                },
                api: {
                    pause: function () {
                        _pause();
                    },
                    play: function (idx) {
                        _play(idx);
                    }
                }
            };
        };
        
        // Cycle through each matching element and return them  
        // so as not to break the jQuery fluent API
        return this.each(function () {

            // Grab a reference to the root element
            var root = this;

            // Determine whether or not the plugin is already installed
            var exists = !(root.MyPlugin === undefined || root.MyPlugin === null);

            // Initialize the optionsOrCommand so we can handle either input
            if (optionsOrCommand === undefined || optionsOrCommand === null) {
                optionsOrCommand = {};
            }

            // If the optionsOrCommand is a string then map it to the API
            if (optionsOrCommand.constructor == String) {
                if (exists) {
                    switch (optionsOrCommand) {
                        case 'pause':
                        case 'play':
                            if (typeof root.MyPlugin[optionsOrCommand] === 'function') {
                                    root.MyPlugin[optionsOrCommand](param);
                            }
                            break;
                        default:
                            return;
                    }
                }
                return;
            }

            // If the plugin is not installed then create it and apply the API
            // to the DOM element.
            if (!exists) {
                var plugin = create(root, optionsOrCommand);
                root.MyPlugin = plugin.api;
                plugin.controller.init();
            }
        });
    };
})(jQuery);

Here are some links to other interesting plugin patterns:
JQUERY GENERATOR BY Kees C. Bakker
Creating a jQuery plugin
Keeping the plugin and jQuery separate

jQuery, Programming , ,

A jQuery action plugin to insert a custom function into the method chain

19. May 2010

I was hanging out at the #jquery IRC Channel through freenode Web IRC and a user wanted to have a callback function on the removeClass method.  This seems like a strange request since the removeClass method is a synchronous call and returns the list of jQuery items which were manipulated.

The problem could be solved as follows:

var items = $(".target").removeClass("target");
doSomething();
items.addClass("newTarget");

However the user was not satisfied with this option, so I suggested adding a callback to the removeClass method like so:

(function($) {
  var oldRemoveClass = $.fn.removeClass;
  $.fn.removeClass = function(value, callback) {
    var ret = oldRemoveClass.call(this, value);
    if ($.isFunction(callback)) {
      callback.call(ret);
    }
    return ret;
  }
})(jQuery);

NOTE: A sample is available on JS Bin.

However, after some consideration, I thought of a another option (again, of questionable value, but hey...) Why not add a jQuery function that takes a callback and returns the fluent context. The code would be quite similar to the above but would apply more broadly.

Plugin code

/*Final plugin*/
(function($) {
  $.fn.action = function(callback) {
    var ret = this;
    if ($.isFunction(callback)) {
      callback.call(ret);
    }
    return ret;
  }
})(jQuery);

Now with the new function in place the user can string a custom function into the method chain. The original solution would be refactored to look like this.

$(".target").removeClass("target").action(doSomething).addClass("newTarget");

The action plugin could allow the callback to filter or replace the item context, but I think that would hurt readability and lead to hidden bugs.

jQuery, Programming , , ,

Moved blog from Blogger to BlogEngine.NET

19. May 2010

I managed my old blog with Blogger but the FTP deployment process was kind-of-a pain. Google agreed and stopped supporting it.  At the time, I was going to switch to a .NET blog solution, but I didn't love any of the options out there and was too lazy to copy over the old posts manually.  Additionally, Twitter was gaining momentum and seemed to fit with the limited amount I had to say.

After a while I started having ideas for longer posts and wanted to put them on the blog, but I felt I had to upgrade first.  Needless to say that didn't happen. 

When BlogEngine.Net came along I wanted to import my posts with their BlogML import helper but for some reason I couldn't get it working. Recently, I tried it again and the current version (1.6.1) worked like a charm.  I have all my old posts and I am ready to put up some content.

I'd like to thank Aaron Lerch for his PowerShell script which I used to extract my BlogML from Blogger.

Script: BloggerToBlogML.ps1
Website: http://www.aaronlerch.com/blog/
License: Public Domain

The PowerShell script relies on the blogML .NET library which can be downloaded from Codeplex.

Programming , , ,

Full Text Index Required for Some Functions

1. March 2006

If you use SQL scripts to create and modify your SQL Server 2000 databases, you may at some point run into a problem. Full-text search functions are only available when a full text catalog exists for the table you are querying.

Since my install scripts are automated I wanted a way to conditionally install one of two versions of the same function. The solution was to use a special metadata function along with the EXEC command to create the function.

IF (SELECT fulltextcatalogproperty('MyFullTextCatalog', 'ItemCount')) IS NULL
BEGIN

EXEC('
    CREATE FUNCTION [dbo].[udf_MySearchFunction]
    (
        @criteria AS VARCHAR(200)
    )
    RETURNS @retTable
    TABLE
    (
        [seqid] [int] IDENTITY (1, 1) NOT NULL
        ,[resultid] [int] NOT NULL
    )
    AS
    BEGIN
        INSERT INTO @retTable
        SELECT [resultid]
        FROM [MyTable]
        WHERE
        [Description] LIKE '%' + @criteria + '%'
        OR [Keywords] LIKE '%' + @criteria + '%'
    END
    RETURN')

END
ELSE
BEGIN

EXEC('
    CREATE FUNCTION [dbo].[udf_MySearchFunction]
    (
        @criteria AS VARCHAR(200)
    )
    RETURNS @retTable
    TABLE
    (
        [seqid] [int] IDENTITY (1, 1) NOT NULL ,
        [resultid] [int] NOT NULL
    )
    AS
    BEGIN
        INSERT INTO @retTable
        SELECT [resultid]
        FROM [MyTable]
        WHERE FREETEXT([MyTable].*, @criteria)
    END
    RETURN')

END
GO

Programming , , ,

Enable ASP.NETWebAdminFiles Outside Visual Studio

7. December 2005

Using the default ASP.NET 2.0 Membership (AspNetSqlMembershipProvider) you can modify config settings and add/remove users from the Visual Studio IDE if you are running it locally or FrontPage Server Extensions are installed and you are connected remotely to the site.

If you want to use the ASP.NetWebAdminFiles web interface without Visual Studio, as I recently did, then here is what you do.

FYI: Only do this if you are careful and for the right purposes, since you are exposing some administration capability.

1. Create a virtual directory that points to the web admin files.

This is what I did:
VirtualDirectory: ASP.NetWebAdminFiles
MappedTo: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ASP.NETWebAdminFiles

2. Modify the properties of the virtual directory so that it is running under ASP.NET 2.0. (Properties > ASP.NET)

NOTE: if you are running 1.1 and 2.0 applications on the same server or site, you may have to set up a separate application pool for the 2.0 sites. If you get the notice, "Application Unavailable" then that is why.

3. While you are there, remove anonymous access to that virtual directory.

4. After that, you will be able to connect to the web admin tools using the following url syntax

http://localhost/ASP.NETWebAdminFiles/default.aspx?applicationPhysicalPath=XXX&applicationUrl=/YYY

in my case, it is:
http://localhost/ASP.NETWebAdminFiles/default.aspx?applicationPhysicalPath=C:\inetpub\wwwroot\myapp\&applicationUrl=/myapp

NOTE: Although it isn't recommended, if you want to access web admin tool from a different/remote computer, then open the WebAdminPage.cs file from the C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ASP.NETWebAdminFiles\App_Code directory and comment the following code block:

if (!application.Context.Request.IsLocal)
{
    SecurityException securityException =
        new SecurityException((string)HttpContext.GetGlobalResourceObject(
                "GlobalResources",
                "WebAdmin_ConfigurationIsLocalOnly"));
    WebAdminPage.SetCurrentException(application.Context, securityException);
    application.Server.Transfer("~/error.aspx");
}


The web admin tool will still be protected to some degree by the Integrated Windows Authentication.

NOTE: If while trying to update user information you get the following error:

Failed to update database "C:\inetpub\wwwroot\myapp\App_Data\ASPNETDB.MDF" because the database is read-only.

Then the NETWORK_SERVICE account does not have read/write access to the MDF file that is being used to store the user information.

http://lab.msdn.microsoft.com/productfeedback/viewfeedback.aspx?feedbackid=dd6d161b-df08-40bc-b9ed-fbca71949ddc

Programming , , ,