Using function scope to build a jQuery event handler
So here’s another cool thing from the development of poll4.me that might interest some people.
When I’m creating or editing polls, I have the option of adding an arbitrary number of answers. I don’t want to just leave an infinite number of form fields for the user to potentially fill in if he wants to — that’s as stupid as it is impossible. So they have to be able to take an action that adds a field dynamically.
The first time I did it, on the poll creation screen, it was just a feature; I didn’t need to abstract it, so I didn’t. But once I needed it on the editing screen too, it was time to abstract. So here’s what it looks like.
First the creation screen, where there are three default answer fields and the ability to add more:
%ol#answers_list
%li
%input.answer{ :type => "text", :name => "answers[]", :tabindex => 2 }
%li
%input.answer{ :type => "text", :name => "answers[]", :tabindex => 3 }
%li
%input.answer{ :type => "text", :name => "answers[]", :tabindex => 4 }
%p
%a#add_answer_link{ :href => "#" }="add another answer"
And now the link needs to be connected to its functionality:
:javascript
$(document).ready(function() {
$("#add_answer_link").click(add_answer_callback(3, 5, "#answers_list"));
});
I’m just adding a click event handler to the link, and it’s the result of the add_answer_callback() function.
Now, the editing screen, where form fields only exist if they’re filled in. I’m keeping track of the proper tabindex with a variable here … it seems dirty to me so if anyone knows enough about Haml to do this more elegantly I’d like to hear about it.
%ol#answers_list
- tabindex = 2
- @answers.each do |answer|
%li
%input.answer{ :type => "text", :name => "answers[]", :tabindex => "#{tabindex}", :value => "#{answer}" }
- tabindex += 1
%p
%a#add_answer_link{ :href => "#" }="add another answer"
And then connecting the event handler is pretty much exactly the same:
:javascript
$(document).ready(function() {
$("#add_answer_link").click(add_answer_callback(#{@answers.count}, #{@answers.count + 2}, "#answers_list"));
});
Note that instead of using constants for the parameters, I’m using calculated values. You can probably tell from here, but the parameters I’m passing in are: “the 0-based index of the next answer,” “the 1-based tabindex of the next form field,” and “the jQuery selector to which to append the field.” Maybe that wasn’t as obvious as I thought. It’s a good thing the code has comments (in the source, that is, I’m not bothering with them here).
So I’m calling a function to get the event handler, but the event handler itself is supposed to be a function. This is Javascript now, so that’s not hard in the slightest.
function add_answer_callback(answer_index, tabindex, append_to) {
return function() {
$('<li><input id="answer_' + answer_index + '" type="text" class="answer" name="answers[]" tabindex="' + tabindex + '" /></li>').appendTo(append_to);
$('#answer_' + answer_index).focus();
answer_index++;
tabindex++;
}
}
This function takes its three parameters and returns a function that takes no parameters and can be used as an event handler. Nice. But the really cool thing about it is that the answer_index and tabindex variables are incremented inside the event handler (ie, that happens each time you click the link), but since they were parameters on the outer function, they’re scoped within the add_answer_callback() function, which is only called once (when the page is loaded).
That way, the id and tabindex of each field are set properly as you add more fields, and because you’re passing them in it doesn’t even matter how many you started with (crucial for the editing screen, of course).
Cool stuff Sean. I like the scoping trick with the anonymous function.
It’s a bit inelegant to use:
What about doing something like
That’ll even clone event handlers if you end up binding any to the individual li/input elements.
Awesome, I shattered the comment system.
Good call. I tried to go in that direction:
I had to split it up because I’m setting the attributes on the input element, but appending the li element to the list.
Maybe there’s a way to fit it onto one line, but once that “find(‘input’)” is in there I can’t figure out how.
Yeah I was concerned since the .find changes the scope all the remaining methods act on, but then I decided I’d just ignore that potential issue. Your 3 liner is much easier to read too.
Actually, it’s important to note that the second line is wrong.
Because we’re cloning the first field, we have to set the value attribute to empty; if we don’t, all the new fields you add will start with the value of the first field (which, in this case, is not what we want).