Idiomatic jQuery

bocoup

bocoup

“Cowboy” Ben Alman

benalman.com
github.com/cowboy
@cowboy

jQuery Core
Style Guidelines

Making Sure
Your Code Runs

JSLint

Test in Different
Browsers

Chrome, Firefox, Safari

Internet Explorer

Maintainability

Consistent Style

Spacing Comments Equality Blocks
Function Calls Arrays & Objects
Assignment Type Checks Strings

Inconsistent Style

(Just plain bad code)

    function validateContactForm() {
      var result = "";
        if($('#contact_form #name').val()=="") {
          result = false;
          return false;
        }    
        if($("#contact_form").find('#address')[0].value!="")result=true;   
        return result;
    }
    
    function doesJQElementExistInDOM(jQueryElement)
    {
    	if(jQueryElement.length)
    		return true;
    	else
    		return false;
    }
    
    function show()
    {
        for (var i = 0; i < arguments.length; i++) {    	
        	    jq(arguments[i]).show();  			
        }
    }

    function hide()
    {
        for (var i = 0; i < arguments.length; i++) {    	
        	jq(arguments[i]).hide();  			
        }
    }
  

Adjust as Necessary

Collection Methods

Feeling jQuery-like

(Collection methods should be called on a jQuery object)

    function enumerate( elems, start ) {
      for ( var i = 0; i < elems.length; i++ ) {
        elems.eq( i ).prepend( "" + ( i + start ) + " " );
      }
    };
    
    // This is no fun!
    enumerate( $("li"), 1 );
    
    // Wouldn't you much rather do this?
    $("li").enumerate( 1 );
  

Methods should Chain

    // Create a new jQuery collection method.
    
    $.fn.enumerate = function() {
      // Code goes here.
    };
    
    // This doesn't error, but it does nothing and returns undefined.
    $("li").enumerate();
    
    // Error: cannot call method 'css' of undefined!
    $("li").enumerate().css( "color", "red" );
    
    
    // A simple test.
    
    var elems = $("li");
    
    $.fn.enumerate = function() {
      // What, exactly, is `this`? I have a nagging suspicion...
      console.log( this === elems );
    };
    
    // Logs true!
    elems.enumerate();
    
    
    // Create a chainable jQuery collection method.
    
    $.fn.enumerate = function() {
      return this;
    };
    
    // Does nothing, but returns the same jQuery object it was called on.
    $("li").enumerate();
    
    // Because of this, it's chainable!
    $("li").enumerate().css( "color", "red" );
  

Implicit Iteration

(Plugins implicitly iterate… by explicitly iterating)

    // No explicit iteration in the plugin...
    
    $.fn.enumerate = function( start ){
      this.prepend( "" + start + " " );
      return this;
    };
    
    // ...means no implicit iteration when it's used.
    
    $("li").enumerate( 1 );
    
    
    // Explicit iteration in the plugin...
    
    $.fn.enumerate = function( start ){
      for ( var i = 0; i < this.length; i++ ) {
        this.eq( i ).prepend( "" + ( i + start ) + " " );
      }
      
      return this;
    };
    
    // ...means implicit iteration when it's used!
    
    $("li").enumerate( 1 );
    
    
    // But you should explicitly iterate using jQuery's .each().
    
    $.fn.enumerate = function( start ){
      this.each(function(i){
        $(this).prepend( "" + ( i + start ) + " " );
      });
      
      return this;
    };
    
    
    // Roll the return up to remove the extra, unnecessary line, and you
    // have the basic "Chainable jQuery Collection Method" pattern:
    
    $.fn.enumerate = function( start ){
      return this.each(function(i){
        $(this).prepend( "" + ( i + start ) + " " );
      });
    };
  

Stack Manipulation

How do you make a method “end”-able?

The .end Method

    // In brief:
    
    $("ul").find( "li" ).addClass( "fancy" ).end().addClass( "selected" );
    
    
    // Select all UL elements.
    $("ul")
      
      // From there, find all LI descendants.
      .find( "li" )
        // Add a class to each selected LI element.
        .addClass( "fancy" )
        // Revert back to the previous collection.
        .end()
        
      // Add a class to each selected UL element.
      .addClass( "selected" );
  

The .pushStack Method

    // The most basic "end"-able collection method.
    
    $.fn.listitems = function() {
      var elems = this.find( "li" );
      return this.pushStack( elems );
    };
    
    // Just like before!
    
    $("ul")
      .listitems()
        .addClass( "fancy" )
        .end()
      .addClass( "selected" );
    
    
    // An "end"-able collection method with an optional selector.
    
    $.fn.listitems = function( selector ){
      var elems = this.find( "li" );
      
      if ( selector ) {
        elems = elems.filter( selector );
      }
      
      return this.pushStack( elems, "listitems", selector || "" );
    };
    
    // Fancy!
    
    $("ul")
      .listitems( ":first-child" )
        .addClass( "fancy" )
        .end()
      .addClass( "selected" );
  

Getters & Setters

Setting a Value

    // Our basic, chainable, "enumerate" method.
    
    $.fn.enumerate = function( start ) {
      return this.each(function(i){
        $(this).prepend( "" + ( i + start ) + " " );
      });
    };
    
    // Enumerate the listitems, effectively "setting" values.
    
    $("li").enumerate( 1 );
  

Getting a Value

    // Only return the appropriate value from the first selected element.
    
    $.fn.getEnumerateStartingValueOMGLongName = function() {
      var val = this.eq( 0 ).children( "b" ).eq( 0 ).text();
      return Number( val );
    };
    
    // Because this doesn't return a jQuery object, it's not chainable!
    
    $("li").getEnumerateStartingValueOMGLongName();
  

Getting & Setting

    // A jQuery collection method that both "sets" and "gets."
    
    $.fn.enumerate = function( start ) {
      if ( typeof start !== "undefined" ) {
        // Since `start` value was provided, enumerate and return
        // the initial jQuery object to allow chaining.
        
        return this.each(function(i){
          $(this).prepend( "" + ( i + start ) + " " );
        });
        
      } else {
        // Since no `start` value was provided, function as a
        // getter, returing the appropriate value from the first
        // selected element.
        
        var val = this.eq( 0 ).children( "b" ).eq( 0 ).text();
        return Number( val );
      }
    };
  

Use a Closure

    // This is ok if it's just for you.
    
    $.fn.enumerate = function( start ){
      return this.each(function(i){
        $(this).prepend( "" + ( i + start ) + " " );
      });
    };
    
    
    // If you're sharing with others, you need to wrap your code
    // in an IIFE (Immediately-Invoked Function Expression).
    
    (function($){
      
      $.fn.enumerate = function( start ){
        return this.each(function(i){
          $(this).prepend( "" + ( i + start ) + " " );
        });
      };
      
    })(jQuery);
    
    
    // Closures are great, because they allow you a private scope in
    // which you can store "private" variables and functions.
    
    /*!
     * jQuery Tiny Pub/Sub - v0.3pre - 11/4/2010
     * http://benalman.com/
     * 
     * Copyright (c) 2010 "Cowboy" Ben Alman
     * Dual licensed under the MIT and GPL licenses.
     * http://benalman.com/about/license/
     */
     
    (function($){
      
      var o = $({});
      
      $.subscribe = function(){
        o.bind.apply( o, arguments );
      };
      
      $.unsubscribe = function(){
        o.unbind.apply( o, arguments );
      };
      
      $.publish = function(){
        o.trigger.apply( o, arguments );
      };
      
    })(jQuery);
  

Utility Methods

Utility, Yes. jQuery, No.

    // What's wrong with this "jQuery plugin"? Hint: it's not
    // the code itself.
    
    (function($){
      
      $.log = function( msg ) {
        if ( $.log.enabled && window.console ) {
          console.log( msg );
        }
      };
      
      $.log.enabled = true;
      
    })(jQuery);
    
    
    // Don't attach arbitrary methods to $ unless they're REALLY
    // jQuery plugins.
    
    var log = (function(){
      
      function fn( msg ) {
        if ( log.enabled && window.console ) {
          console.log( msg );
        }
      };
      
      fn.enabled = true;
      
      return fn;
      
    })();
  

This One's Legit

    // This method will never be called on a collection of elements,
    // it's just a utility method.
    
    $.deparam = function( str ) {
      var obj = {};
      
      $.each( str.split( "&" ), function(i,pair){
        var nv = pair.split( "=" );
        obj[ nv[0] ] = nv[ 1 ];
      });
      
      return obj;
    };
    
    $.deparam( "a=1&b=2&c=3" ); // { a: "1", b: "2", c: "3" }
  

Namespacing

    // For jQuery utility methods, this works.
    
    $.myPlugin = function() {
      // Code goes here.
    };
    
    $.myPlugin.submethod = function() {
      // Code goes here.
    };
    
    $.myPlugin();
    $.myPlugin.submethod();
    
    
    // For jQuery collection methods, this doesn't work.
    
    $.fn.myPlugin = function() {
      // Code goes here.
    };
    
    $.fn.myPlugin.submethod = function() {
      // Code goes here.
    };
    
    $("li").myPlugin();           // While this works great...
    $("li").myPlugin.submethod(); // This ain't gonna happen.
    
    
    // This kind of thing is also no good. It's technically possible
    // (with a lot of work) but it's confusing. For example, how do
    // you keep track of chaining? What does .end() do? No good.
    
    $("li").myPlugin().submethod();
    
    
    // Of course you can do this (and a whole lot more), using the
    // jQuery UI Widget Factory, and it works great.
    
    $("li").myPlugin( "submethod", options );
    
    
    // But for jQuery collection methods, this is usually sufficient.
    
    $.fn.myPluginSubmethod = function() {
      // Code goes here.
    };
    
    $("li").myPluginSubmethod();
  

Recap