views:

71

answers:

3

Firstly, is it possible? Been struggling with this one for hours; I think the reason my events aren't firing is because one event is unbinding/overwriting the other. I want to bind two change events to the same element. How can I do that?


As per request, here's the function I'm struggling with:

(function($) {
    $.fn.cascade = function(name, trigger, url) {
    var cache = {};
    var queue = {};

        this.each(function() {
            var $input = $(this);
            var $trigger = $input.closest('tr').prev('tr').find(trigger);

            //$input.hide();

            var addOptions = function($select, options) {
                $select.append('<option value="">- Select -</option>');
                for(var i in options) {
                    $select.append('<option value="{0}">{1}</option>'.format(options[i][0], options[i][1]));
                }
                $select.val($input.val()).trigger('change');
            }

            var $select = $('<select>')
                // copy classes
                .attr('class', $input.attr('class'))
                // update hidden input
                .bind('change', function() {
                    $input.val($(this).val());
                })
                // save data for chaining
                .data('name', name)
                .data('trigger', $trigger);

            $input.after($select);

            $trigger.bind('change', function() {
                var value = $(this).val();

                $select.empty();

                if(value == '' || value == null) {
                    $select.trigger('change');
                    return;
                }

                // TODO: cache should be a jagged multi-dimensional array for nested triggers
                if(value in cache) {
                    addOptions($select, cache[value]);
                } else if(value in queue) {
                    $select.addClass('loading');
                    queue[value].push($select);
                } else {
                    var getDict = {}
                    getDict[name] = value;
                    // TODO: use recursion to chain up more than one level of triggers
                    if($(this).data('trigger')) {
                        getDict[$(this).data('name')] = $(this).data('trigger').val();
                    }
                    $select.addClass('loading');
                    queue[value] = [$select];
                    $.getJSON(url, getDict, function(options) {
                        cache[value] = options;
                        while(queue[value].length > 0) {
                            var $select = queue[value].pop();
                            $select.removeClass('loading');
                            addOptions($select, options);
                        }
                    });
                }
            }).trigger('change');
        });

        return this;
    }
})(jQuery);

The relevant chunk of HTML is even longer... but essentially it's a select box with a bunch of years, and then an <input> that gets (visibly) replaced with a <select> showing the vehicle makes for that year, and then another <input> that gets replaced with the models for that make/year.

Actually, it seems to be running pretty well now except for on page load. The initial values are getting wiped.


Solved the issue by pulling out that $select.bind() bit and making it live:

$('select.province').live('change', function() { 
    $(this).siblings('input.province').val($(this).val()); 
});
$('select.make').live('change', function() { 
    $(this).siblings('input.make').val($(this).val()); 
});
$('select.model').live('change', function() { 
    $(this).siblings('input.model').val($(this).val()); 
});

Sucks that it's hard-coded in there for my individual cases though. Ideally, I'd like to encapsulate all the logic in that function. So that I can just have

$('input.province').cascade('country', 'select.country', '/get-provinces.json');
$('input.make').cascade('year', 'select.year', '/get-makes.json');
$('input.model').cascade('make', 'select.make', '/get-models.json');
A: 

I am actually not sure exactly if you can bind two different change events. But, why not use logic to complete both events? For example...

$('input:checkbox').change(function() {
  // Do thing #1.
  // Do thing #2.
});

That way, you get the same benefit. Now, if there are two different things you need to do, you may need to use logic so that only one or the other thing happens, but I think you would have to do that anyway, even if you can bind two change events to the same element.

JasCav
If only it were that simple ;) They're bound at different times, conditionally.
Mark
+6  A: 

Yes that is possible.

$(…).change(function () { /* fn1 */ })
    .change(function () { /* fn2 */ });

jQuery event binding is additive, calling .change a second time does not remove the original event handler.

Ryan Tenney
Guess that was an easy test I could have checked myself....damn. Guess my bug is elsewhere then. http://jsfiddle.net/Q2WpA/
Mark
Make sure that you are attaching the event handlers after the DOM is ready. The following example will fail to attach the event handler: `<html><head><script>$("#myID").click(/* fn */);</script></head><body><a id="myID">test</a></body></html>`. The contents of the script tags inside `<head>…</head>` execute before the browser has constructed the rest of the page, this can be fixed by enclosing that code like so: `$(document).ready(function() { $("#myID").click(/* fn */); });` and this way it will now run as soon as the DOM is ready.
Ryan Tenney
@Ryan: Yeah.. I put all my code in the document ready block to avoid issues like that.
Mark
Excellent! I saw the example you posted right after I finished writing that comment :)
Ryan Tenney
+1  A: 

Ryan is correct in jQuery being additive, although if you find there are problems because you are chaining the same event, beautiful jQuery allows another approach, and that is calling the second function within the first after completion of the first as shown below.

$('input:checkbox').change(function() {
  // Do thing #1.;  <-- don't forget your semi-colon here
  (function() {
     // Do thing #2.
  });

});

I use this technique frequently with form validation, one function for checking and replacing disallowed characters input, and the second for running a regex on the results of the parent function.

Update to Post:

OK... You all are quick to beat on me with your negative scores, without understanding the difference in how we each view Mark's request. I will proceed to explain by example why my approach is the better one, as it allows for the greatest flexibility and control. I have thrown up a quick example at the link below. A picture's worth a 1000 words.

Nested Functions on One Event Trigger

This example shows how you can tie in three functions to just one change event, and also how the second and third functions can be controlled independently, even though they are still triggered by the parent change event. This also shows how programmatically the second and third functions can BOTH be tied into the same parent function trigger, yet respond either with or independently (see this by UNCHECKING the checkbox) of the parent function it is nested within.

$('#thecheckbox').change(function() { 

      $("#doOne").fadeIn();

      if ($('#thecheckbox').attr('checked')) { doFunc2() }
      else { doFunc3() };

      function doFunc2() { $("#doTwo").fadeIn(); return true; }
      function doFunc3() { $("#doTwo").fadeOut(); return true; }

      $("#doThree").fadeIn();

  });

I've included the third 'Do thing #3 in the example, to show how yet another event can follow the two nested functions as described earlier.

Forgive the earlier bad pseudocode originally posted first, as I always use ID's with my jQuery because of their ability to give everything an individual status to address with jQuery. I never use the 'input:checkbox' method in my own coding, as this relies on the 'type' attribute of an input statement, and therefore would require extra processing to isolate any desired checkbox if there is more than one checkbox in the document. Hopefully, the example will succeed at articulating what my comments here have not.

Epiphany
That looks funny... how does inner function get executed? Did you forget the `()` after it?
Mark
Yea, I know it looks funny, and it took me awhile myself to get used to when learning jQuery. The inner function gets executed because it is actually the last 'do thing' of the first function. That's why I said 'don't forget your semi-colon here' as this tells the first function it isn't done yet, and then executes the next statement, which is the second function.
Epiphany
Mark is correct that "thing #2" will never be executed. http://jsfiddle.net/t5UFB/
Ryan Tenney
That's not true, Mark. It will be executed, provided the semi-colon follows the ending of the last 'Do thing #1, as now the second function is the last "Do thing #1", because it is nested within the first function. You can even nest a third function within the second function the same way... and so on and so on.
Epiphany
I fail to see the point of this even if it worked. All you'd end up doing is executing some additional code that you might just as well put in the original handler. -1.
Tin
That's exactly the point of the post... that two functions are being tied into just one change event instead of calling two. This means jQuery is only having to poll the event once, instead of it polling the same event twice... which could be conflicting should they overrun one another
Epiphany
This is no different than executing both chunks of code inline.
Tin
There is a very big difference, Tin. Mine is using half the system resources to do the same thing. In otherwords, mine is just checking the state of the change event with one call, rather than the same event state being monitored by two calls.
Epiphany
Further, and I'll have to test this, the two change method may not work, because the first change may respond to the change event and reset the status after responding, so that the second change event will see there is no change and do nothing at all.
Epiphany
Your insistence here just highlights your mistake.On the contrary, you're using more resources by setting up a new closure to execute code that doesn't need one. Again, this is functionally equivalent to executing $('input:checkbox').change(function() { // Do thing #1. // Do thing #2.});
Tin
@Epiphany: In the future, please just edit your original posts instead of adding new ones to make the same point. Just to clarify, two *different* answers on the same question would be fine, but if you just want to clarify or defend your existing answer, please do it by editing or commenting. Thanks.
Bill the Lizard
@Epiphany - I'm sorry that we came down on your answer so negatively. The second example that you posted is, of course, correct, and goes a long way to help clear up the misunderstandings that led to our criticism. Posting complete, executable code is very important when trying to illustrate your point, and would have permitted us to begin a discussion on whether the technique does provide a performance benefit, instead of both sides simply repeating that the other is wrong.
Ryan Tenney
@Epiphany - Regarding performance, I do agree that your technique may improve performance, in certain cases. Declaring those functions inside the callback will allow them access to the variables of the callback, while only creating one instance of the function. The question is whether that will outweigh the cost of calling the function.
Ryan Tenney
Thank you, Ryan, for your response. As I understand it, standard javascript IS faster than jQuery, as jQuery IS javascript, and not a new language, but rather a library. The additional functions are actually declared with normal javascript within the jQuery construct, making it faster.It also has the benefit of being able to place these inner functions outside of the both the .change trigger and the jquery document.ready usually without ill effect, although I would agree making a standard practice of keeping your jQuery within the document.ready construct is best to avoid headaches.
Epiphany
I might add, it is disturbing to me to see so many these days trying to do everything with jQuery. Even the authors of jQuery encourage the use of hybrid javascript/jQuery. Folks seem to quickly forget, that jQuery is a library of javascript, and not the other way around. Best to keep sharp with your standard javascript skills, and not get into the mindset that jQuery is a replacement for standard javascript.
Epiphany
Your second piece of code is very different from the first. This makes a lot more sense now, although it's still not particularly helpful to my specific problem. Oh well. +1 for effort and persistence.
Mark
Thanks, Mark. Actually, if you look at the original again, you'll see it is almost exactly the same as the working example I put up. The only difference was making a sleepy mistake where I added the parenthesis before the function and another to close the function, as well as used id's instead of elements. Otherwise it is almost verbatim of the working second example I posted. Sorry it didn't help solve your problem, but at least it may come in handy down the road for you on another project.
Epiphany