2
\$\begingroup\$

Im trying to get better at writing jQuery plugins. Was hoping to get some feedback on my notes, but also just about the general architechture and implenmentation of this pattern (dont even know this one is called :P)

jsFiddle: http://jsfiddle.net/gajwp/4/ Click the input fields! (No need to comment on positioning or stylnig, it looks better on my site)

Project use jQuery 1.4.2 (so no .on() event handling :( )

Notes:

  • Should be DRY, short, fast, beautiful and all that :)
  • Havent fully wrapped my head around the constructor/prototype thing. For instance, why am I creating the $bubble in the constructor and not in the prototype init() method? I hoping its because then its the same $bubble that is used throughout.
  • I feel like there is a better way to build the bubble content. It must be possible to pass the plugin an object with the information. Tips welcome.
  • In the template I used there was stuff like 'this._name = pluginName' in the constructor. What does the underscore mean, and what are the use for it?
  • How is the event bindings? Was a bit confused about it because of the functionality. Popup should pop on fucus, and hide on blur. But not hide if focusing on a new field. Also if you click the popup or the "More..." link, it should also stay open - and close again if you click outside it.

Plugin code (same as in fiddle):

;(function ($, window, document, undefined) {
"use strict";
var pluginName = 'formHelper', $input, o, $bubble, stopBlurEvent, key,
    defaults = {
        position: "left",
        dataObject: {
            Default: {
                info: '<p class="info">Default info</p><a href="#" class="help-more">More...</a>',
                extended: '<p class="help-extended">Default extended...</p>'
            },
            Email: {
                info: '<p class="info">Email info</p><a href="#" class="help-more">More...</a>',
                extended: '<p class="help-extended">Email extended...</p>'
            },
            Name: {
                info: '<p class="info">Name info</p><a href="#" class="help-more">More...</a>',
                extended: '<p class="help-extended">Name extended...</p>'
            }
        }
    };

function FormHelper(element, options) {
    var that = this;
    $input = $(element);
    o = $.extend({}, defaults, options);
    $bubble = $('<div class="help-bubble" />').bind({
        'mouseenter': function() {
            stopBlurEvent = true; 
        },
        'mouseleave': function() {
            stopBlurEvent = false;
        },
        'mousedown': function(e) {
            e.stopImmediatePropagation();
            $('html').not(this).bind('click', function() {
                that.hideBubble();
            });
        }
    }).delegate('.help-more', 'click', function(e) {
        e.preventDefault();
        e.stopPropagation();
        that.toggleExtendedInfo();
    });

    this.init();
}

FormHelper.prototype = {
    showBubble: function(input) {
        var $currentInput = $(input);
        key = (o.dataObject.hasOwnProperty($currentInput.attr('id'))) ? $currentInput.attr('id') : 'Default';

        var bubbleContent = '<h4>' + $currentInput.attr('title') + '</h4>' + o.dataObject[key].info;
        $bubble.html(bubbleContent).insertBefore($currentInput).fadeIn();
        stopBlurEvent = false;
    },
    hideBubble: function() {
        if (!stopBlurEvent) {
            $bubble.fadeOut('fast', function() {
                $(this).detach(); $('html').unbind('click');
            });
        }
    },
    toggleExtendedInfo: function() {
        var $moreLink = $('.help-more', $bubble);

        if ($moreLink.hasClass('open')) {
            $moreLink.removeClass('open');
            $('.help-extended', $bubble).slideUp('fast', function() {
                $(this).remove();
            });
        } else {
            $moreLink.addClass('open');
            $(o.dataObject[key].extended).appendTo($bubble).slideDown();
        }
    },
    init: function() {
        var that = this;
        $input.bind({
            'focus': function() {
                that.showBubble(this);
            },
            'blur': function(e) {
                if (stopBlurEvent) {
                    e.preventDefault();
                } else {
                    that.hideBubble();
                }
            },
            'mousedown': function() {
                stopBlurEvent = true;
            }
        });
    }
};

$.fn[pluginName] = function(options) {
    return this.each(function() {
        if (!$.data(this, 'plugin_' + pluginName)) {
            $.data(this, 'plugin_' + pluginName,
                new FormHelper(this, options));
        }
    });
};
})(jQuery, window, document);
\$\endgroup\$
4
  • \$\begingroup\$ I actually had the semicolon in ealier, but removed it because jslint diddnt like it. But your suggestion made me look it up at read about it, so now I understand WHY its a good idea to put it there :) \$\endgroup\$
    – ugreen
    Mar 26, 2013 at 6:53
  • \$\begingroup\$ The click binding on everything but the bubble, doesnt seem effective to me either. But couldnt find another way to know when the user clicks outside the bubble. Any ideas? \$\endgroup\$
    – ugreen
    Mar 26, 2013 at 6:56
  • \$\begingroup\$ You're absolutely right about the init just calling another function. Refactored the code in the question. \$\endgroup\$
    – ugreen
    Mar 26, 2013 at 7:01
  • \$\begingroup\$ I've added to my answer according to your comments. \$\endgroup\$ Mar 26, 2013 at 15:02

1 Answer 1

2
\$\begingroup\$

It was fun going over your code. I haven't played with 1.4 for a while. Well here are some suggestions:

  • Place a semicolon before your IIFE as a precaution against statements without a separator. Putting a semicolon before the function prevents the function from becoming an argument. This can happen when you minify and concat JS files.

Something like this:

(function(){...foo...})()(function(){...bar...})() //No separator :/
(function(){...foo...})();(function(){...bar...})() //But you smart feller put in a semicolon
  • You bind a click to everything except the $bubble? Doesn't that seem a bit excessive?

Right here:

$('html').not(this).bind('click', function() { 
    that.hideBubble();
});
  • About prototypes, it can be difficult to grasp at first, but keep at it, you'll have an "Aha!" moment soon enough where it will all become clear. My suggestion is read and read and practice. Here are some reading materials on the subject I would recommend:

http://msdn.microsoft.com/en-us/magazine/ff852808.aspx - http://ejohn.org/apps/learn/#64

  • For your plugin's data I would suggest Handlebars.js. It's an awesome templating engine that works with Mustache, so if you've used that before this one should be easy.

  • There's a library called Underscore.js which provides functional programming support. But I think the _name you're referring to is simply a naming convention and does not carry any meaning to Javascript.

  • Your entire init: function is there to simply call another function? Are you planning on putting more in there? If not I would suggest just going straight to binding.

  • You use $(input) several times. You should cache that selector like you do elsewhere.

  • This is just my nit-pickyness but I don't like single line functions. I just think they're harder to read. But don't lose any sleep over that.

UPDATE:

If you really want to bind a click event to everything except $bubble, you can do it (jQuery 1.4 friendly) like this:

$(document).bind('click', function (e) {
 // Do whatever you want; the event that'd fire if the "special" element has been clicked on has been cancelled.
});

$('.help-bubble').bind('click', function(e) {
    e.stopPropagation();
});

Now, what I would suggest you do instead is create a div outside of your main stack to handle this. Imagine an absolutely positioned div behind most of the page, and you only bind the event once.

\$\endgroup\$
1
  • \$\begingroup\$ Nice, I'm commenting my question instead of your answer :P Sorry, and please see above. \$\endgroup\$
    – ugreen
    Mar 26, 2013 at 7:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.