Guiding The Geek In You
Happy Post-Thanksgiving weekend everyone!
This week I begin a non-sequential series of posts describing my experiences writing plugins for the jQuery library. My idea is to cover the evolution of one – or a small set of – plugins from version 1.0 to “launch”, where launch in this case means promotioning my plugins within the massive ecosystem of the jQuery universe as loudly as possible!
The opportunity to write the first plugin for this series – Primer Carousel – came as a result of a well-designed coding challenge I recently completed. The source for this challenge is available via my public git repo here. As always your opinion is welcome; please leave your comments below or send a pull request on github.
(function ( $ ) { $.fn.primerCarousel = function( options ) { }; }( jQuery ));
The first step in writing a plugin is remembering the global namespace. Functions structured in the above format are known as Immediately-Invoked Function Expressions (IIFEs). Using the format here is essentially defensive. We want to use the $ character, but we also must be polite citizens and cooperate with other JS libraries who love the $ character. The solution: pass the function jQuery and name the parameter $, which satisfies both requirements. Perhaps the most important purpose of a IIFE, however, is that it allows private variables accessible only within the function itself.
By using .fn() object we are extending the methods made available to the jQuery object to include our plugin (which becomes just another method). Developers can therefore attach the .primerCarousel() method to any appropriate block element. Additionally, notice the use of “options”? This object contains whatever user defined settings have been made available. In version 1.0 of Primer Carousel we offer one setting, rotation speed.
var globlalOps = $.extend( {}, $.fn.natCarousel.defaults, options ); var filmStripWidth = 0; var thumbNailCount = 1; var $filmStrip = $(".filmStrip"); var $focusArrow = $("#focusArrow"); var $filmStripCollection = $(".filmStrip").find("div"); var $smallImageCollection = $(".smallPictureRow").find("img"); var widthOfLargeImage = $filmStripCollection.width(); var widthOfSmallImages = $smallImageCollection.width(); var thumbNailArraySize = $smallImageCollection.length;
I prefer defining all of my variables up top. I know other developers prefer a “define where you use” technique – so go with what you know. There are a couple of important things to call out here. First, by using an empty object as the first passed parameter to the $.extend() method we ensure the custom defaults object is not overridden by the options object. Instead a third object is created by joining the two others.
Next, Primer Carousel relies on existing markup to function properly. This design choice speaks to a larger question: is it better to ask developers to implement a specific HTML structure for your plugin, or give the plugin a root container and rely on it to generate the HTML it needs? Obviously the later option is the most common – and from a reusability standpoint the most sensible. However, dealing with assets provided by the developer (images, for example), and operating within unique markup constraints means coding a robust evaluation system capable of detecting dimensions, calculating asymmetrical widths, and so forth. This version of Primer Carousel assumes symmetrical image width – a luxury afforded only in a test bed. Look for more robust environment evaluation logic in version 2.
// Initialize timer variable var rotateTimer; // Prepare film strip element containing the large image slides $filmStripCollection.each(function() { var $this = $( this ); filmStripWidth += $(this).width(); }); $filmStrip.width(filmStripWidth); // Carousel Control - Manual $smallImageCollection.on("click", advanceFilmStrip); // Carousel Control - Automatic rotateTimerStart(); // Pause carouseling on roll-over this.on("mouseover", function() { rotateTimerClear(); }).on("mouseleave", function() { rotateTimerStart(); }); // Initialize, Set, and Clear Interval function rotateTimerInit() { advanceFilmStrip(thumbNailCount); // basic position counter ( thumbNailCount < 3 ) ? thumbNailCount++ : thumbNailCount = 0; } function rotateTimerStart() { if ( typeof globlalOps.speed !== "number" ) { globlalOps.speed = 4000; } rotateTimer = setInterval(rotateTimerInit, globlalOps.speed); } function rotateTimerClear() { clearInterval(rotateTimer); }
You may notice the heavy reliance upon the setInterval() and clearInterval() functionality in this section? These two methods are extremely helpful when creating automated behavior. Consider wrapping them in functions to improve overall code DRY-ness, and in my opinion three timer functions work the best – initialization, start, and clear.
Also, functions defined within a IIFE formatted plugin are truly private. Returning anything back to the calling script requires return – which Primer Carousel does not need. However, if your plugin needs to be chainable then something must be returned.
function advanceFilmStrip(thumbIndex) { // two use cases: the thumbnail clicked or function fired automatically if ( typeof thumbIndex === "object" ) { var $this = $( this ); var thumbPosition = $smallImageCollection.index(this); var smallImageLeftMargin = $this.css("marginLeft"); } else { var thumbPosition = thumbIndex; var smallImageLeftMargin = $smallImageCollection.eq(thumbIndex).css("marginLeft"); } // Convert px measurement to an integer smallImageLeftMargin = (smallImageLeftMargin.replace("px","")) * 1; // Calculate current position var filmStripPosition = widthOfLargeImage * thumbPosition * -1; var focusArrowPosition = (widthOfSmallImages + smallImageLeftMargin) * thumbPosition; // move the images $filmStrip.css("left",filmStripPosition+"px"); $focusArrow.css("left",focusArrowPosition+"px"); }
The secret sauce is all right here. You probably noticed the typeof check made on the incoming parameter? This is done because of the dual use cases in which advanceFilmStrip is called. In my opinion this is a mistake, I should only ever need to accommodate for a single parameter type (in this case, object or integer).
$.fn.natCarousel.defaults = { speed:4000 };
Being kind in this case means saving developers the effort of hunting through our source code to find where we configure our default plugin options. This object should contain the entire range of all available options – version 2 of Primer Carousel will have several more, including spin direction, animation, and on-screen controls.
In my opinion, just because your plugin CAN do something does not mean it must – nor does it mean those functions must be documented for the end user. Think carefully about what features users will want and refine accordingly. Also, use conventions! Primer Carousel’s speed setting accepts an integer, and passes (with error checking, naturally) that value directly to the setInterval method. Abstract settings like “fast” or “ultra slow” simply add an additional layer of complexity. The goal is to make developers want to use our plugins because they work exactly as expected, reliably, and with a minimal education required. Keep it simple!
Thanks for reading!
Sometimes the hardest part about a project is sitting down to organize your environment before you begin. Sure, you probably have several, maybe hundreds, of pieces of functional code stashed in your git repo or squirreled away in different folders on your hard drive, but starting a brand new project? Ugh! Enter the project sandbox.
Project sandboxes are templates designed for easy duplication that allow you to quickly create an ideal project environment. Pre-configured folder structures and file names optimized to your liking make launching a new project as easy as duplicating a template and making a tweak to the new project’s server config file.
Similar to regular sandboxes – also known as development servers – project sandboxes isolate projects to a well-defined structure, preventing code pollution. The key difference is Sandboxes are usually designed for developing within an existing code base protected by a code repository. Project sandboxes, however, exist only to help start new and potentially disposable development in a reliable way.
Regarding naming schemes: the word of the day is CONSISTENCY. Name your project sandbox files anything you like and commit to your scheme. When you find a better naming method (and you will), retrofit your project sandbox right away – don’t put it off! Using consistent file names will build strong mental associations that help you decide where to put certain code.
My project sandbox files:
Some file stubs (files that exist without any content) I like keeping handy to handle RWD needs, or to load AMD formatted scripts, respectively:
Flexibility in terms of a project sandbox means keeping a variety of battle tested and carefully vetted resources at your finger tips, not all of which you may need for every project. Some resources should be configured to load by default, while others (like frameworks) are kept out of the loading stack.
Some of my core resources:
General Tools
Asynchronous Resource Loaders
JS MVC Frameworks
JS Libraries
Should you use a CDN or keep your toolkit stored locally? I prefer using local files. Staying local removes a layer of potential complexity (not relying on a file that probably stays the same), improves performance, and keeps resource files readily available for dissection. That said, CDNs are extremely valuable in many other situations.
However, using local resources means you must manually keep your project sandbox current. Get into the habit of watching for updates, reading release notes, and making informed decisions about updating your resources. Broken and dull tools are even worse than no tool at all!
Folder organization depends largely on your web server and middle tier language of choice. Discussing the best ways to organize folders for different servers and languages could be a cool topic for another day.
Since I use Node.js my project sandbox folder structure is pretty simple, as demonstrated below.
Of course, this is only a start! Your project sandbox can be tailored in any way you see fit and will change over time. The important lessons are: make one, keep it consistent, keep it current.
Good luck, and please comment with any suggestions. And as always, thank you for reading!