4

I've made a simple carousel for my site and I want to take it to the next step. I would like to have several carousels on a page and I'd like to just call $('.carousel').swapify() and be done with it.

Here's what I have thus far:

$.fn.swapify = function() {
  var $this = $(this);
  console.log('this is currently: ' + this.selector);

  function advanceSlide() {
    $('.swapify li:first-child').appendTo('.swapify');
  }

  $(this).find('li').click(function() { advanceSlide(); });
}

$(document).ready(function () {
  $('.carousel').swapify();
});

When I interact with a carousel they all advance. This is pretty obvious, but I'm not sure how to make the interaction specific. I've tried something like:

$('li:first-child', this).appendTo(this);

I'm still developing my javascript vocabulary and it's proving hard to google.

edit: I setup a fiddle to illustrate this problem http://jsfiddle.net/69FM7/1/

riot
  • 43
  • 3
  • I'm not sure of the actual problem, but a couple of things to tidy up: Firstly, be careful with similarly named variables: you define `$this`, but then use `this.selector` and `$(this).find`. Secondly, you don't need to wrap `advanceSlide` in an extra function; just `.click(advanceSlide)` will do (I personally prefer the more explicit `.on('click', advanceSlide)`). – IMSoP Feb 02 '14 at 13:53

3 Answers3

2

IMSoP's answer is probably what you intended, but another options is to use delegated events:

$.fn.swapify = (function() {
    function advanceSlide(e) {
        var $carousel = $(e.delegateTarget);
        $carousel.find('li:first-child').appendTo($carousel);
    }

    return function swapify() {    
        this.on("click", "li", advanceSlide);
    };
}());

Fiddle


Here are some quick explanations of some key concepts:

  • Immediately Invoked Function Expressions

    IIFE's give you a way to make a stable new scope for new variables and functions to live in. We create a function and immediately invoke it.

    (function() {
        // new scope here   
    }());
    

    We are using one here to make a new scope for a single instance of the advanceSlide and swapify functions to live in.

  • Closures and Free Variables

    The return value from the IIFE is the swapify function. This function is a closure – it closes over the free variable advanceSlide. "Free" in the sense that variable advanceSlide "escapes" along with swapify when it is returned out of the function – it is free to live beyond it's containing scope.

  • $.fn is a alias of the jQuery's prototype object. In short, this means that all instances of a jQuery object will have access to functions defined by the prototype.

    For clarity, $("someselector") will return a jQuery object. We assign the return value of the IIFE to $.fn.swapify which will imbue jQuery objects our new functionality.

  • Delegated Events

    When we invoke our swapify helper, instead of binding events on each li – which is what .find("li").on(...) would do – it would be ideal to bind events on the parent element. Then let jQuery dynamically check that the target of the event matches the set we care about.

    For example, you may have only a handful of ul elements, however each one of those may contain hundreds of li elements. Rather than needing to create a handler and attach an event on each one of those, we can simply attach a single handler on the parent. This minimizes the number of event handlers that need to be bound.

  • Using the delegatedTarget property from the event object

    Finally, when our event handler gets invoked it will be passed an event object. That will have several useful properties, like target, currentTarget, and delegatedTarget. We use delegatedTarget to look up the root element that we originally bound to, and assign that to $this. Then, the rest of your logic works the way you expect it to.

Community
  • 1
  • 1
J. Holmes
  • 18,466
  • 5
  • 47
  • 52
1

You could achieve this via below:

$.fn.swapify = function() {
  $.each($(this), function(index, element) {
    var $this = $(this);

    function advanceSlide() {
       $this.find('li:first-child').appendTo($this);
    }

    $this.find('li').click(function() { advanceSlide(); });
  });
}

As $this denotes to the specific selector for which swapify get bind.

Demo Fiddle

This will fix your issue.

Kundan Singh Chouhan
  • 13,952
  • 4
  • 27
  • 32
  • This didn't work, check this fiddle out: http://jsfiddle.net/69FM7/1/ Every time you click an li, that li gets appended to all `.swapify` – riot Feb 02 '14 at 13:27
  • @riot, i have fixed the issue and attached the demo fiddle. Have a look now. – Kundan Singh Chouhan Feb 02 '14 at 15:16
  • One optimisation noted in my answer: `this` will already be a jQuery object when `.swapify()` is run, so you can just do `this.each` rather than `$(this).each` or `$.each($(this),` – IMSoP Feb 02 '14 at 18:04
1

When extending the jQuery prototype (jQuery.fn), the function you write is given as this the complete jQuery object passed in, which in this case is the result of $('.carousel'), i.e. a collection of all elements with the class carousel. Therefore everything you do involving this applies to that entire collection.

What you want to do, and indeed what is very common in such cases, is act individually on each element in the collection. To do that, you can use the .each() function to run the same function on every element. Each time the function is run, this is set to a single DOM element - not a jQuery object, which is why you need to re-wrap it with $(this).

Since the function has its own scope, your use of var $this = $(this) allows you to create "closures", which carry the particular element around with them. Because it was declared inside the same scope, the function advanceSlide is such a closure - each time your function runs, new variables are created for both $this and advanceSlide.

Make sure you always use $this, though, as the actual this variable will be different when the function runs than when it was defined. (The click-handler will set it to the element that was clicked on.) For this reason, it might be better to use a different variable name, say, $carousel to remember which object it refers to.

Since advanceSlide is already a local function, you don't need another function() { } wrapper, just pass it in to the .click() (or .on()) call.

So what you end up with is the below, as demoed in this JSFiddle:

$.fn.swapify = function () {
    // Here, `this` is a jQuery collection with multiple elements in
    this.each(function () {
        // Here, `this` is a single DOM node

        // using `var`, we make a variable bound to this scope
        // while we're at it, we wrap the DOM node with jQuery
        // Rather than $this, I've named the variable something meaningful
        var $carousel = $(this);

        // Declaring a function in the same scope as our var
        // creates a "closure",  which can always see the scope it was created in
        function advanceSlide() {
            // When this runs, it will be in response to a click,
            // and jQuery will set `this` to the element clicked on

            // Our $carousel variable, however, is carried in the closure,
            // so we know it will be the container we want
            $carousel.find('li:first-child').appendTo($carousel);
        }

        // A function in JS is just another kind of object, 
        // so we can pass in advanceSlide like any variable
        $carousel.find('li').on('click', advanceSlide);
    });
}

$(document).ready(function () {
    $('.swapify').swapify();
});
IMSoP
  • 89,526
  • 13
  • 117
  • 169