4
\$\begingroup\$

A little while ago I wrote some jQuery code to make a div flip (similar to the Apple Dashboard). I know there is already code that has this idea from Jon Raasch, but this code positioned divs absolutely, which I did not need for my cause.

I just wondered if anyone could give me some advice as to how to tidy this up and make it cleaner.

(function ($) {
  $.fn.extend({
    jFlip: function (options) {
        var defaults = {
            trigF: '.flipDiv',
            trigB: '.flipDivBack'
        };
        var o = $.extend({}, defaults, options);
        return this.each(function () {
            var f = $('.front');
                b = $('.back');

            margin = f.width() / 2;
            width = f.width();
            height = f.height();
            b.hide().css({
                width: '0px',
                height: '' + height + 'px',
                marginLeft: '' + margin + 'px',
                opacity: '0'
            });

            $(o.trigF).on('click', function () {
                var $this = $(this);

                $this.parents().closest(f).animate({
                    width: '0px',
                    height: '' + height + 'px',
                    marginLeft: '' + margin + 'px',
                    opacity: '0'
                }, {
                    duration: 500
                });

                window.setTimeout(function () {
                    $this.parents().next(b).animate({
                        width: '' + width + 'px',
                        height: '' + height + 'px',
                        marginLeft: '0px',
                        opacity: '1'
                    }, {
                        duration: 500
                    });
                }, 500);
            });

            $(o.trigB).on('click', function () {                            
                $(this).parents().closest(b).animate({
                    width: '0px',
                    height: '' + height + 'px',
                    marginLeft: '' +margin + 'px',
                    opacity: '0',
                    display: 'none'
                }, {
                    duration: 500
                });
                var $this = $(this).parents().siblings(f);
                window.setTimeout(function () {
                    $this.stop().animate({
                        width: '' + width + 'px',
                        height: '' + height + 'px',
                        marginLeft: '0px',
                        opacity: '1'
                    }, {
                        duration: 500
                    });
                }, 500);
            });
        });
    }
  });
})(jQuery);

The div structure is this:

<div class="widgetBox">
     <div class="front">
        <p>Front of the div</p>
        <button class="flipDiv">Flip</button>
     </div>
     <div class="back">
        <p>Back of the div</p>
        <button class="flipDivBack">Flip Back</button>
     </div>
  </div>

And this is the initiation:

<script>
     $(document).ready(function() {
         $('.widgetBox').jFlip();
     });
</script>

The only options as of yet however are trigF and trigB, which allows you to specify the name of your buttons which will flip the div.

You can see a jsFiddle of this in action here.

\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

Looks cool, nice job!

One this I would say is that the plugin makes too many assumptions about the client's DOM structure. Having fixed selectors like $('.front') and $('.back') within your plugin script is dangerous -- what if I'm working in a page that has a 'front' class on the body?

It's best to let the client pass in exactly what DOM elements they want to manipulate. You could, for example, do something like this:

The html:

<div id="flipper">
  <div data-face="front">I'm in front!</div>
  <div data-face="back">I'm in back!</div>
</div><!-- end #flipper -->

The initialization:

$('#flipper').jFlip();

In your plugin script, you now know exactly which elements the client wants to work with:

...
return this.each(function() {
 ...
 $flipper = $(this);
 $front = $flipper.find('[data-face=front]:first');
 $back = $flipper.find('[data-face=back]:first');
 ...
});

Now you don't have to deal with any pesky DOM tree traversal, which carries a risk of finding the wrong element.

A couple other notes:

  • When you're specifying css attrs with jquery, height: someNumberVariable works just fine. You don't need to wrap it with '' + someNumberVar + 'px'
  • Why not set the duration as an optional param?
  • It would be nice to have a $('#myEl').jsFlip('flip') method. Adding methods like this will add some complexity to your code, but the good news is there are some good plugin patterns out there that you can work off of. IMO, this is the prettiest one I've seen.
  • Once you've figured out how to add methods to your plugin, it's fairly simple (and best practice) to add a 'destroy' method, that would unbind all of your event handlers. To do this, you need to namespace your events.

For example:

to bind: $flipBtn.on('click.jFlip', function() { ... })

to destroy: $flipBtn.off('.jFlip'); // unbind all events in .jFlip namespace

One other thing: this plugin replicates the functionality of existing CSS3 animations, which will always be faster. This plugin is still useful as a fallback for browsers that are not up to speed on CSS3, but it would be best to check for that capability first, and implement with CSS3 if available.

I hope this is helpful. Keep on rocking the plugins!

\$\endgroup\$
5
  • \$\begingroup\$ Thanks! Really useful advice, will definitely work on this advice! In terms of the CSS3 animations, I completely agree, though most browsers only support 2D animations? This is faux-3D animations but works right down to IE6. This plugin was built off the back of a project which needed the code to be as compatible as possible. But for sure, the part about DOM tree traversal will be used as it is a much cleaner way of doing it, though on your third point about using .jsFlip('flip'), Im not sure I understand what you mean? \$\endgroup\$
    – Joshua M
    May 8, 2012 at 8:54
  • \$\begingroup\$ Take a look at this on plugin methods: goo.gl/Z2MUu. Current versions of Chrome, Safari, and FF support 3d transforms: caniuse.com/#search=3dtran. It may be worthwhile to detect the user's browser, then either apply the animations, or add css3 3d transform properties to the .front/.back elements \$\endgroup\$
    – edan
    May 8, 2012 at 11:00
  • \$\begingroup\$ I've just put in the data-face, however now it's lost the ability to have multiple instances of the flipping div. Also, I will look at using 3D transforms and using this as a backup, then. \$\endgroup\$
    – Joshua M
    May 8, 2012 at 11:27
  • \$\begingroup\$ Can you post the updated code to jsfiddle.com? I'll take a look. \$\endgroup\$
    – edan
    May 9, 2012 at 21:07
  • \$\begingroup\$ Hi edan, thanks mate. If you look here you can see my problem. You click on the top div's flip button and it flips the bottom, then it just.. goes a bit crazy! \$\endgroup\$
    – Joshua M
    May 10, 2012 at 11:02
1
+100
\$\begingroup\$

Note: I wrote my answer before reading edan's answer, which heavily influenced the code in the updated question. As a result my answer is more of an addition to edan's answer rather than being independent and focusing on the author's original code.

Creating the Plugin

jQuery plugins are usually created with the following syntax, $.fn.pluginName. It is the preferred way because it directly extends jQuery.prototype and is common javascript syntax. By using $.extend you may run into speed issues and other problems.

Moving the Default Options

I moved your default settings outside of the plugin's function. In addition, I changed the names from ['trigF', 'trigB'] to ['trigger_front', 'trigger_back'] so they are a bit more clear.

// allow global options
$.fn.divFlip.defaults = {
  trigger_front: '.flipDiv',
  trigger_back: '.flipDivBack',
  duration: 500
};

// inside of the plugin's function
options = $.extend($.fn.divFlip.defaults, options);

Doing this will not break your code and its primary advantage is that whoever uses your plugin can define their own defaults.

For instance, if I wanted to make the default duration of the plugin 200ms, I could simply run. $.fn.divFlip.defaults.duration = 200;

Defining the Variables

Most of what I changed in your variables were a personal preference.

  1. Your $front and $back variables don't need the :first selector. The markup for the div.widgetBox only has one front and back face. Which means selecting the first one is unnecessary.

  2. I added variables for your front and back triggers to help consolidate all of the selectors. It isn't a necessary change, simply how I roll :)

// find the triggers
$front_trigger = $front.find(options.trigger_front),
$back_trigger = $back.find(options.trigger_back),

The Big Change

The biggest change that I made to your plugin is the way the animations are handled. $.fn.animate()'s third parameter allows a callback function that runs after the animation has completed. Which means you didn't need to create a setTimeout to manually show the next face.

In addition, because you are showing and and hiding alternative faces, I rewrote the function to automatically switch between the front and back.

My Final Code (View jsFiddle)

Here is the code that I came up with in the end. As you can see it is a lot more DRY and should make it easier to add new features and options later on.

(function($) {

  $.fn.divFlip = function(options) {

    // set the default options with the ones specified
    options = $.extend($.fn.divFlip.defaults, options);

    return this.each(function() {
  
      // get the main elements
      var $flipDiv = $(this),
          $front = $flipDiv.find('[data-face=front]'),
          $back = $flipDiv.find('[data-face=back]'),
      
          // find the triggers
          $front_trigger = $front.find(options.trigger_front),
          $back_trigger = $back.find(options.trigger_back),
      
          // get the dimensions
          front_width = $front.width(),
          front_height = $front.height(),
          margin = front_width / 2;
  
      // hide the back slide
      $back.css({
        width: 0,
        height: front_height,
        marginLeft: margin,
        opacity: 0
      });
  
      // catch all clicks for the triggers
      $([$front_trigger.get(0), $back_trigger.get(0)]).on('click', function() {
    
        // by default show the back and hide the front slide
        var toShow = $back,
            toHide = $front;
    
        // clicked on the back trigger
        if( this == $back_trigger.get(0) ) {
          toShow = $front;
          toHide = $back;
        }
    
        // hide the visible slide
        toHide.animate({
          width: 0,
          height: front_height,
          marginLeft: margin,
          opacity: 0
        }, options.duration, function() {
      
          // show the hidden slide
          toShow.animate({
            width: front_width,
            height: front_height,
            marginLeft: 0,
            opacity: 1
          }, options.duration);
        });
      });
    });
  };

  // allow global options
  $.fn.divFlip.defaults = {
    trigger_front: '.flipDiv',
    trigger_back: '.flipDivBack',
    duration: 500
  };

})(jQuery);
\$\endgroup\$

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.