Putting aside the fact that this took far longer to fully code than I wanted it to, I have to say that the process of building jQuery.Bart was fun. I finally got my GitHub repository up and running, and am getting used to committing code into that. I also got to play with some CSS3 animations, and some interesting quirks to the jQuery.animate() function, so overall this was an awesome learning experience.
As I have done with earlier plugins which I have written, I will take you on a brief journey through what went into the construction of this plugin and why I have made some of the choices I have made as far as structure, syntax, and functionality.
First off, I do want to give props to several other authors of jQuery notification plugins which I have seen on the web. Zac Stewart’s Meow plugin, Red Team Design’s Notifications with CSS3 and jQuery, as well as the MsgUI plugin from Code Canyon. These are all excellent pieces of code. MsgUI was the first one I have seen which combines all of the different notification styles into one plugin. Though, in my defense, mine all comes in one file and you don’t have to pay for it.
What I was noticing in looking at these other plugins though, was that they only did one thing. Meow does Growl-like bubble notifications. The plugin from Red Team Design does excellent drop-down bar notifications. MsgUI, jQueryUI, and a bunch of others do modal dialog notifications. But MsgUI was the only one I found that did all of them, and (deservedly) they want money for it. Please don’t get me wrong. I am all for coders being paid for their work. If companies didn’t do that, I wouldn’t have a job normally. But, jQuery is open source. JavaScript is open source. Therefore, I wanted a swiss army knife notification plugin. Best thing to do? Code it myself.
From the beginning, I realized that there were several slippery slopes that I could potentially slide down in doing this project. The first was bloat. Not me, the code. It would be all too easy to have separate sets of functions and DOM manipulation for each type of plugin, and I didn’t want to do that. It would also be all too easy to get very wrapped up in the intricacies of switch-case statements, if/else conditions, etc. One of my goals was to keep this plugin as lean and mean as I could. As the minified version comes out at 2.1k of JS and 3k of CSS, I’m pretty pleased.
First off, I had to decide on HTML structure. I wanted one set of markup that I could use for all versions of the notification. I also wanted to make sure that I included icons, a title and message body, and a close button. So, this markup became very simple to do:
<div id="wrapper"> <span id="close"> <img src="/images/close.png" /> </span> <span id="icon"> <img src="/images/icon.png" /> </span> <h3 id="title">Title</h3> <span id="text">Message here</span> </div>
Now, this is pretty straightforward markup, but it becomes very quickly evident that if I’m going to have any success at this as a plugin, I’m going to have to pay attention to namespace collisions in my CSS. After all, it’s pretty likely that someone is going to use this on a page where they have another id named ‘icon’, or ‘close’. So, I decided that all my CSS ID’s and classes would be prefixed by ‘bart_’. It’s the name of the plugin, after all, and I have a much greater assurance that people aren’t going to be using ‘bart_wrapper’ as an ID on their page.
With that markup, I can represent all types of my notification without issue, just by applying classes that manipulate the CSS accordingly. I will refer you to the uncompressed CSS file for reference. This post isn’t about the CSS, it’s about the JS, so I will gloss over the gritty details of sizes, margins, floats, min-heights, and the like.
That markup left me with the knowledge that if a user chooses to go from one type of notification to another, all of the classes will need to be cleaned out. Ideally, each time Bart is run, it’s a clean slate. So, I came up with a quick function to be able to do that:
var clearClasses = function(elem) { var classList = elem.attr('class'), pattern = /^bart_/; if (classList != undefined) { var classes = classList.split(' '); for (var domclass in classes) { //We only want to remove the bart classes if (pattern.test(classes[domclass])) { elem.removeClass(classes[domclass]); } } } };
This way, this is a private function inside of Bart, and takes an element and clears all of the bart_ classes off of it. Since both the wrapper itself and the icon have classes to change their behaviour, I pass both of them through this function. However, I only need to do it after Bart has been run the first time. I like to take care of the DOM, because if I do, then it will take care of me. So, this is both how I construct the Bart markup to be added to the page, and manage it if it is pre-existing:
//This builds our notification into the DOM if it doesn't already exist if (!$("#bart_wrapper").length) { //Add it to the body now so that we can build on it. //It's hidden by default in the css $("<div>").attr('id', 'bart_wrapper') .appendTo('body'); $("<span>").attr('id', 'bart_icon') .appendTo("#bart_wrapper"); $("<img>").appendTo("#bart_icon"); $("<h3>").attr('id', 'bart_title') .insertAfter("#bart_icon"); $("<span>").attr('id', 'bart_text') .appendTo("#bart_wrapper"); $("<span>").attr('id', 'bart_close') .prependTo("#bart_wrapper"); $("<img>").attr('src', options.imgDir+'close.png') .appendTo("#bart_close"); $("#bart_close").click(function() { detention(); }); } else { //Already exists on page //Clear any old bart classes out clearClasses(wrapper); clearClasses(icon); //Empty style def's to erase any previous animation results wrapper.removeAttr('style'); }
This keeps me from typing out a lot of HTML or having to add the weight of something like jquery.tmpl. The last piece there, with the .removeAttr() call was something that got added through the course of development, because I was having an issue where the first one would render fine, but after that, the bart_wrapper div had many extra style definitions on it left over from being animated off the screen. So, I chose to do this. It isn’t the cleanest method. I suppose the argument could be made that I might step on someone else’s customizations. After all, I do give them the ability to add customizations through passing CSS definitions. My response to that is: Set the CSS customizations in a variable, and pass that each time you call Bart. Compromise is what makes the world go ’round.
After these considerations, everything that I had to do for making this plugin functional was animation. Of course, this wasn’t a direct, easy path. However, I had set myself up well, by naming my vertical and horizontal positions as top, bottom, left, right. This is because the position names correspond to CSS properties. So, all I have to do is animate options.vposition and options.hposition to the proper values, right? Wrong.
Due to the rules of interpolation, you cannot do this:
$("#bart_wrapper").animate({options.vposition: 0, options.hposition: 0}, 'slow');
You will get a variety of errors, and none of them are particularly easy to troubleshoot. In discussing this problem with one of my colleagues, Ryan Neufeld, he came up with a novel solution. What he proposed was passing in to animate an object, and using array notation on that object to set the values. Effectively, this was his solution to the above problem:
var animateSettings = {}; animateSettings[options.vposition] = 0; animateSettings[options.hposition] = 0; $("#bart_wrapper").animate(animateSettings, 'slow');
It works like a charm. Passing my parameters in as array indices causes them to be interpolated, so I end up with ‘top: 0′ and ‘right: 0′ where I want them. .animate() doesn’t complain, and the DOM elements move the way we want them to. The last consideration was accommodating for different slide directions, as well as being able to specify a starting top point. What I ended up with was the following code, for both my vertical positioning and horizontal positioning.
if (isNaN(options.vposition)) { //handles top or bottom if (options.slide == 'vertical') { wrapper.css(options.vposition, '-'+height+'px'); } else { wrapper.css(options.vposition, 0); } } else { wrapper.css('top', options.vposition+'px'); options.vposition = 'top'; }
So here’s what I’m doing with this code. First I check for NaN (not a number) on the vposition. If it is not a number, aka ‘top’ or ‘bottom’, then I want to do one action. However, if it is a number, for example if someone has specified a top position, then just set that and set the options.vposition to ‘top’ because I will be using that in the animateSettings object I discussed earlier. The code inside the first conditional is testing for sliding. If the slide has been specified as vertical, then we need to position the DOM element off screen, thereby setting a negative position on it.
The ‘height’ variable has been previously calculated, and the ‘wrapper’ variable is where I have $(“#bartwrapper”) cached (check my post on <a title=“Cash in on Caching (aka The This or That Debate)” href=“/cash-in-on-caching-aka-the-this-or-that-debate/” target=”blank”>Cashing in on Caching for an explanation).
If it’s not a vertical slide, then I just set the vposition to 0, because the hposition later is going to get set to a negative amount, and that’s what will get animated. Overall, I was pleased with this. I suppose it’s possible that I could have shortened this code by using a ternary, something like:
wrapper.css(options.vposition, (options.slide == 'vertical') ? '-'+height+'px' : 0);
But that’s not as readable, and doesn’t really gain us anything. Ultimately, it makes it harder to maintain, so I left that kind of nesting out. And, in production, you’re probably going to be using the minified version anyway, so these kinds of micro-optimizations aren’t particularly beneficial. The goal for me, as a plugin author, when I am writing out the large form of my code is to make it readable and maintainable, so that if anyone does need to figure out how something works, or why it doesn’t work the way they want it to, they will be able to trace through very easily.
As always, comments, criticisms and other errata are welcomed in the comments area below. If you like this plugin, please feel free to use and abuse it, just give credit where it’s due.
Happy Koding!