0

It was kinda hard to come up with the title for this one, hope it's understandable and makes sense.

Well , maybe, the difficulty to come up with the title reflects some unnecessary complexity of this situation - if there's a simple way to do this I'd love to know.

In this sample, there's a Car 'class', from which I can instance objects (Cars). It has a template (an SVG, which is not a car, actually, but who cares), and also public methods, used to modify the template, via jQuery.

In the carContainerClick() function, how do I access the Car instance whose template is contained by the currently clicked carContainer element?

Sample here

$(function(){

  var cars = [];

  for (var i = 0; i < 2; i++) {

    var carContainer = $('<div/>', { class: 'car-container'});
    var car = new Car();

    cars[i] = car;

    carContainer.on('click', carContainerClick);

    carContainer.append(car.getTemplate());
    $('.container').append(carContainer);
  }

  function carContainerClick() {

    // HERE - how do I access the Car instance whose template is contained by the currently clicked carContainer element?

    cars[0].changeColor();
  }
});

function Car () {
  this.template = $('<svg viewBox="0 0 301 259">    <g class="svg-group"><path class="stick-2" fill-rule="evenodd" clip-rule="evenodd" d="M74.192,27.447c2.589-2.042,4.576-3.188,6.991-5.093c0,0,1.753-1.11,0.416-2.945 c-1.13-1.546-3.242,0.014-3.242,0.014c-4.831,3.804-9.678,7.589-14.491,11.418c-2.335,1.861-4.335,4.009-7.954,3.233 c-2.136-0.458-3.892,1.798-3.913,4.021c-0.02,2.326,1.531,4.107,3.734,4.296c2.353,0.2,4.689-1.183,4.635-3.241    c-0.066-2.415,1.215-3.474,2.981-4.492c1.821-1.049,5.809-3.993,7.21-4.785C71.961,29.082,74.192,27.447,74.192,27.447z"/></g></svg>');
}

Car.prototype = {
  getTemplate: function() {
    return this.template;
  },
  changeColor: function() {
    console.log('changeColor');
    $('.svg-group', this.template).find("path, polygon, circle").attr("fill", "#aff");
  }
};

update

i made a little test with the solutions provided here and turns out it makes almost no difference in performance .. but i like the IIFE one for its simplicity. Sample

zok
  • 6,065
  • 10
  • 43
  • 65

4 Answers4

2

You can use an inline function as your event handler that passes car as a parameter... however, because the car declaration is hoisted out of your loop (specifically to the top of the current function), you need to ensure that the event handler captures the right Car instance, and not the last one to pass through the loop.

You can create a closure by using a self invoking function expression that creates a new scope for car that exists within the loop and allows the right car instance to be captured when you set up your handler.

  for (var i = 0; i < 2; i++) {
    (function(){
      var carContainer = $('<div/>', { class: 'car-container'});
      var car = new Car();
      cars[i] = car;
      carContainer.on('click', function(){
        carContainerClick(car);
      });
      carContainer.append(car.getTemplate());
      $('.container').append(carContainer);
    })();
  }

Now you can have a function that gets handed the Car instance you need.

  function carContainerClick(car) {
    car.changeColor();
  }
spender
  • 117,338
  • 33
  • 229
  • 351
  • it was tricky to understand why it captures the last car instance... [this post](http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) has some good explanations! not sure which one is better, to use an IFEE or to declare the function that binds `car` and `carContainer` as @Volune did – zok Aug 25 '14 at 13:50
1

Use closure to pass the car instance as an argument to the carContainerClick function.

I extracted a new function, cause doing that stuff in a for loop will probably not work (always passing the last car as an argument)

for (var i = 0; i < 2; i++) {
    var carContainer = $('<div/>', { class: 'car-container'});
    var car = new Car();

    cars[i] = car;

    bindCarContainer(carContainer, car);

    carContainer.append(car.getTemplate());
    $('.container').append(carContainer);
}

function bindCarContainer(carContainer, car) {
    carContainer.on('click', function(event) {
        carContainerClick.call(this, car, event);
    });
}

function carContainerClick(car, event) {
    //...
}
Community
  • 1
  • 1
Volune
  • 4,324
  • 22
  • 23
  • great solution! not sure which one is more readable or has better performance though.. between this one and the IIFE-wrapping one from @spender. in addition, it also works without using `.call()`...is there any particular reason to use it? – zok Aug 25 '14 at 13:43
  • Just do be compatible with the `$(this)` lots of people do in event listeners. – Volune Aug 25 '14 at 13:50
1

Try assigning storing it in a property:

$container = $('.container');
for (var i = 0; i < 2; ++i) {
    var car = new Car(),
        carContainer = $('<div/>').addClass('car-container').prop('car', car);
    carContainer.on('click', carContainerClick);
    carContainer.append(car.getTemplate());
    $container.append(carContainer);
}

function carContainerClick() {
    this.car.changeColor();
}

var $template = $('<svg viewBox="0 0 301 259">    <g class="svg-group"><path class="stick-2" fill-rule="evenodd" clip-rule="evenodd" d="M74.192,27.447c2.589-2.042,4.576-3.188,6.991-5.093c0,0,1.753-1.11,0.416-2.945 c-1.13-1.546-3.242,0.014-3.242,0.014c-4.831,3.804-9.678,7.589-14.491,11.418c-2.335,1.861-4.335,4.009-7.954,3.233 c-2.136-0.458-3.892,1.798-3.913,4.021c-0.02,2.326,1.531,4.107,3.734,4.296c2.353,0.2,4.689-1.183,4.635-3.241    c-0.066-2.415,1.215-3.474,2.981-4.492c1.821-1.049,5.809-3.993,7.21-4.785C71.961,29.082,74.192,27.447,74.192,27.447z"/></g></svg>');

function Car () {
    this.$template = $template.clone();
}

Car.prototype = {
    getTemplate: function() {
        return this.$template;
    },
    changeColor: function() {
        console.log('changeColor');
        $('.svg-group', this.$template).find("path, polygon, circle").attr("fill", "#aff");
    }
};

Some notes:

  • Don't use $ to get elements inside a loop, it's very slow! Store the result in a variable before the loop instead
  • { class: 'car-container'} may fail on old browsers because class was reserved. Try 'class' with quotes or addClass method.
  • To remember what is a DOM element and what is a jQuery wrapper, you can use $ at the beginning of the variable name.
Oriol
  • 274,082
  • 63
  • 437
  • 513
0

Use jQuery's .data() to attach a reference to the car to the template:

function Car () {
    this.template = $('<svg viewBox="0 0 301 259">    <g class="svg-group"><path class="stick-2" fill-rule="evenodd" clip-rule="evenodd" d="M74.192,27.447c2.589-2.042,4.576-3.188,6.991-5.093c0,0,1.753-1.11,0.416-2.945 c-1.13-1.546-3.242,0.014-3.242,0.014c-4.831,3.804-9.678,7.589-14.491,11.418c-2.335,1.861-4.335,4.009-7.954,3.233 c-2.136-0.458-3.892,1.798-3.913,4.021c-0.02,2.326,1.531,4.107,3.734,4.296c2.353,0.2,4.689-1.183,4.635-3.241    c-0.066-2.415,1.215-3.474,2.981-4.492c1.821-1.049,5.809-3.993,7.21-4.785C71.961,29.082,74.192,27.447,74.192,27.447z"/></g></svg>');
    this.template.data('car', this);
}

Then later you can access it from the jQuery object:

function carContainerClick() {
    var car = this.data('car');
    car.changeColor();
}
Barmar
  • 741,623
  • 53
  • 500
  • 612