6
\$\begingroup\$

I've just launched my first project on GitHub. It's a jQuery plugin that helps create a responsive menu. It deals with interactivity (essentially using toggle and some classes), and leaves all the presentation stuff for CSS.

Here is the basics for using the plugin:

First, create the structure with HTML:

<div id="content" class="canvas">

<!-- (...)Here goes the content (...) -->

</div>

<!-- A button to toggle our menu -->
<a href="#" id="menu-toggle">Menu</a>

<!-- The menu wrapper -->
<div id="menu">
    <ul>
        <li><a href="#">sample link</a></li>
        <li><a href="#">sample link</a></li>
        <li><a href="#">sample link</a>
            <!-- The submenu toggle button -->
            <a href="#" class="submenu-toggle">Open Submenu</a>
            <!-- The submenu wrapper -->
            <ul class="submenu">
                <li><a href="#">sample link</a></li>
                <li><a href="#">sample link</a></li>
                <li><a href="#">sample link</a></li>
            </ul>
        </li>
    </ul>
</div>

Then, call it using jQuery:

$('#menu').responsiveMenu($('#menu-toggle'));

There are some others options to customize the plugin:

  $('#menu').responsiveMenu({
    trigger: $('#menu-toggle'),
    activeClass: 'active',
    submenuTrigger: $('.submenu-toggle'),
    submenu: $('.submenu'),
    submenuActiveClass: 'open',
    breakpoint: 720
    moveCanvas: true,
    canvas: $('.canvas'),
  });

Where $('#menu') is the main wrapper of the menu, and $('#menu-toggle') is the button to activate the behavior of the plugin.

And, finally, the plugin code:

;(function ( $, window, document, undefined ) {

  $.fn.responsiveMenu = function(settings){
     var config = {
          'trigger': null,
          'activeClass': 'active',
          'submenuTrigger': $('.sub-toggle'),
          'submenu': false,
          'submenuActiveClass': 'open',
          'breakpoint': 720,
          'timeOut': 100,
          'moveCanvas': false,
          'canvas': null,
      };
      if (settings){$.extend(config, settings);}

      // declaring plugin variables
      var mTrigger;
      var menu = $(this);
      var active = config.activeClass;
      var button = config.trigger;
      var bpoint = config.breakpoint;
      var submTrigger = config.submenuTrigger;
      var submenu = config.submenu;
      var submenuClass = '.' + submenu.prop('class');
      var submenuActive = config.submenuActiveClass;
      var canvasOn = config.moveCanvas;
      var canvas = config.canvas;
      var time = config.timeOut;

    return this.each(function () {
      if($(window).width() > bpoint){
        mTrigger = false;
      } else {
        mTrigger = true;
      }

      onChange = function(){

        clearTimeout(resizeTimer);
        var resizeTimer = setTimeout(function(){

          if($(window).width() > bpoint){
            mTrigger = false;
            menu.removeClass(active);
            button.removeClass(active);
            if(canvasOn){
              canvas.removeClass(active);
            }

          } else {
            mTrigger = true;
          }
        }, time);

      }

      $(window).bind('resize',onChange);
      $(document).ready(onChange);

      button.click(function(e){
        e.preventDefault();
        if(mTrigger) {
          menu.toggleClass(active);
          button.toggleClass(active);
          if(canvasOn){
            canvas.toggleClass(active);
          }
        }
      });

      if(submenu){
        // toggle for the submenus
        submTrigger.click(function(e){
          e.preventDefault();

          if(mTrigger) {

            if($(this).hasClass(active)){

              submTrigger.removeClass(active);
              submenu.removeClass(submenuActive);

            } else {
              submTrigger.removeClass(active);
              $(this).addClass(active);
              submenu.removeClass(submenuActive);
              $(this).next(submenuClass).addClass(submenuActive);
            }

          }

        }); 
      }

    });
  }
})( jQuery, window, document );

If you want to check the repository of the plugin, here's the link.

In the repository, there is more information about it, such as which option does what in the plugin, etc. There is a simple demo as well.

The plugin is in development, so I'm searching for feedback on it. I'm okay with best practices, performance, and possibly any suggestions and contributions as well.

\$\endgroup\$
2
  • \$\begingroup\$ Nice little plugin you got there! Representando o Brasil na net - show de bola! What happens if I were to have sub-sub-menus? IE: <ul> <li> <ul> <li> <ul> <li></li> </ul> </li> </ul> </li> </ul> \$\endgroup\$ Sep 6, 2013 at 18:52
  • 1
    \$\begingroup\$ Valeu, @JonnySooter! Que bom que curtiu! Thanks, I'm glad that you liked! Well, for now I only supported two levels of menus, because this is the use case for most of the times when I need to use it. But I plan to push a update soon that support multiple levels of submenus. I plan too in the next update, to create some methods (like responsiveMenu.init() or responsiveMenu.reset()) and improve the instantiation (like var menu = new responsiveMenu({options});. Keep your eyes on it! \$\endgroup\$ Sep 6, 2013 at 20:16

1 Answer 1

2
\$\begingroup\$

Don't be intimidated be the size of the book, I just like to write out and explain in as much detail as possible. Well anyways, I've pretty much re-wrote the whole plugin. I've applied a more object-oriented approach and re-factored the plugin into the Module Design pattern. It's now much easier to add functionality and debug code. I've also added in some safe guards to properly initiate the plugin as well. At the end of the code, and inside it, I've included a few links for reading and watching materials. I highly recommend you go through them as they can explain the concepts demonstrated here a lot better than I can.

;(function ( $, window, document, undefined ) {

"use strict";

var $window = $(window),
    ResponsiveMenu = { 
    //This is your main object, it holds everything about your plugin
    //This way you can easily add methods and functionality to your plugin
    //It also provides an easy way to chain methods
    //It makes it easy to spot bugs because the code is broken down into sections/methods

    init: function(options, elem) {
        //In your functions "this" refers to the ResponsiveMenu object
        //To call your methods you can do "this.init()" or "this.options"

        this.options = $.extend( {}, this.options, options ); //Basic options extend
        this.elem = $(elem); //Here I cache the element that the plugin was called on $("menu")

        if($window.width() > this.options.breakpoint) {
            this.options.mTrigger = false;
        }

        this.bindEvents(); //Call my first method

        return this; //Maintain chainable
    },

    options: { //Options
        trigger: null,
        activeClass: 'active',
        submenuTrigger: $('.sub-toggle'),
        submenu: false,
        submenuActiveClass: 'open',
        breakpoint: 720,
        timeOut: 100,
        moveCanvas: false,
        canvas: null,
        mTrigger: true,
        callback: null
    },

    bindEvents: function() {
        //Here I cache the reference to ResponsiveMenu
        //I do this because inside event callback methods, "this" refers to the element the event was triggered from
        //Now I can still refer to my main object and keep "this" inside the callbacks the same
        var self = this;

        this.options.trigger.on('click', function(evt) {
            evt.preventDefault();
            //As you see here I use "self" to refer to the main ResponsiveMenu object
            //If I would have used "this" I would be referring to the "trigger" element
            self.triggerMain(self); //From here we go to the "triggerMain" method
        });

        if(this.options.submenu){
            this.options.submenuTrigger.on("click", function(evt) { //Same idea from the one above
                evt.preventDefault();
                self.triggerSubMenu(this, self);
            });
        }

        //Here I put in a safeguard on your resize event
        //As you probably know, the resize event fires every time there is a resize, even if you're not finished resizing
        //To prevent that I added in a custom event
        $window.on('resize', function() {
            if(this.resizeTO) clearTimeout(this.resizeTO); //Here we see if there's a timeout open, if yes, cancel it

            this.resizeTO = setTimeout(function() { //Set a time out so that the event is only triggered once
                $(this).trigger('resizeEnd'); //Trigger the event
            }, self.options.timeOut); //After "time"
        });

        //I set up the custom event here
        //This will only be called once
        $window.on('resizeEnd', this.onFinalResize(self)); //When this happens, we move down to "onFinalResize" method
    },

    triggerMain: function(self) {
        //I pass "self" here because in here "this" still refers to the "trigger" element because this has the context of a callback method
        var activeClass = self.options.activeClass;

        if(self.options.mTrigger) {
            self.elem.toggleClass(activeClass);
            self.options.trigger.toggleClass(activeClass);

            if(self.options.moveCanvas){
                self.options.canvas.toggleClass(activeClass);
            }
        }
    },

    triggerSubMenu: function(elem, self) {
        //Here I did things a bit differently to show you different ways to do the same thing
        //As a rule of thumb, if you use a selection more than once, you should cache it
           //So here we cache the clicked element
        //You could use "this" instead, but I used "elem" so it would be easier to understand
        var $elem = $(elem),
            activeClass = self.options.activeClass,
            subActiveClass = self.options.submenuActiveClass,
            submenu = self.options.submenu;

        if(self.options.mTrigger) {
            if($elem.hasClass(activeClass)) {
                $elem.removeClass(activeClass);
                submenu.removeClass(subActiveClass);
            } else {
                $elem.removeClass(activeClass);
                $elem.addClass(activeClass);
                submenu.removeClass(subActiveClass);
                $elem.next('.' + submenu.prop('class')).addClass(subActiveClass);
            }
        }
    },

    onFinalResize: function(self) {
        //This is all your code, just that referencing the main object
        if($window.width() > self.options.breakpoint){

            var activeClass = self.options.activeClass;

            self.options.mTrigger = false;
            self.elem.removeClass(activeClass);
            self.options.trigger.removeClass(activeClass);

            if(self.options.moveCanvas) {
                self.options.canvas.removeClass(activeClass);
            }

            if (typeof self.options.callback == 'function') {
                self.options.callback.call(this);
            }

        } else {
            self.options.mTrigger = true;
        }
    }
} //ResponsiveMenu

//This section here is Crockford's shim to true prototypal inheritance
//Read up here: http://javascript.crockford.com/prototypal.html
if ( typeof Object.create !== 'function' ) {
    Object.create = function (o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}

//Here is where your plugin actually gets set up
$.fn.responsiveMenu = function( options ) { //Add to the namespace
    if (this.length) {
        return this.each(function() {
            var myMenu = Object.create(ResponsiveMenu); //Create the ResponsiveMenu object we made above and save it to "myMenu"
            myMenu.init(options, this); //Call the init method, passing in the user's options and the element the plugin wall called on
            $.data(this, 'responsiveMenu', myMenu); //Store the information from our plugin in a safe way - no memory leaks
        });
    }
};
})( jQuery, window, document );

A really good "course" for someone getting started with jQuery is the 30 Days to Learn jQuery by Jeffery Way. He does a great job with explaining tough concepts and provides an overall lesson on jQuery and plugin development. You have some experience so I'd skip the first few videos and jump right into where you'll start learning.

Anyways, there is no one size fits all when it comes to making plugins. I've simply demonstrated the way I like best. Use this as a learning experience and not as a "this is how you should do it". If you have any questions after this or if there's some part that isn't very clear, let me know, and I can elaborate some more.

\$\endgroup\$
1
  • \$\begingroup\$ Awesome! I liked very much this approach of creating an object that holds all the functions of the plugin! I'm currently learning a little of Backbone.js and I see some similarities with it. Also, very interesting reading on Crockford's article about true prototypal inheritance! And I'll be watching for sure this jQuery series! Now, I see that the plugin is really more extensible and maintainable. I'll be merging your commits later today! \$\endgroup\$ Sep 10, 2013 at 13:34

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.