Sms_small Smits 3 posts

Dear Christophe,
Thanks for your book. I really enjoyed reading it and, I think, it is an indispensable guide to write scripts based on Prototype and Script.aculo.us.
The only thing I experienced difficulties with (hard to grasp) is the function binding (section 4.2). Your article at A List Apart gives a clear introduction. Thanks again.

My question is how to make a routine to check a range of check boxes in a form using a click (for the first) and a shift-click (for the last). My search lead to a JQuery plugin page of John Sutherland named jquery.shiftclick
but I didn’t want to include the jquery library of John Resig, so I used the code fragment to make it the prototype way.

We have a form on our page (id=frmStudentList) with a table containing a check box in front of each name in the table rows. The check boxes have a class checkbox. There are only check boxes, no other input fields. We don’t want to attach listeners to each checkbox (because the table will be updated by an Ajax call), so there is just one statement:
$(‘frmStudentList’).observe(‘click’, handleClick);

Here is the code:


handleClick: function(e)
{
   var origin = e.element();
   if (!origin) return;
   if (!origin.nodeName) return;
   // Process click on an input of type checkbox 
   if (origin.nodeName.toLowerCase() === 'input') {
      if (e.shiftKey) {
         var checkBoxes = $A($('frmStudentList').getInputs('checkbox'));
         var last = checkBoxes.indexOf(lastSelected);
         var first = checkBoxes.indexOf(origin);
         if (-1 == last) {
            lastSelected = origin;
            return;
         }

         var start = Math.min(first, last);
         var end = Math.max(first, last);

         var chk = lastSelected.checked;
         for (var i = start; i < end; ++i) {
            checkBoxes[i].checked = chk;
         }
      } else {
         lastSelected = origin;
      }
   }
}  // end 'handleClick'

Can you comment on this code.Is there a way to do this faster or better with either Prototype or Script.aculo.us?

Thanks for your answer.

Kind regards from the Netherlands,
Johan Smits.

 
Headshot_120px_small Christophe P... 48 posts

Hey Johan,

No time to reply just now, but the problem is interesting and I think I see a simple solution. Ping me by email tomorrow and I’ll try to come up with something here.

Cheers,

 
Sms_small Smits 3 posts

Hey Christophe,

I’m sorry but I don’t have your mail address so I can’t ping. Nice to see that this problem interests you. It is, I think, worthwhile to discuss because this will give users a better UI. In the meantime I will wait for your comment.
Enjoy your weekend.

Warm regards,
Johan.

 
Headshot_120px_small Christophe P... 48 posts

Hey Johan,

Well your implementation works alright. It makes little use of “comfort methods” so it’s pretty fast as it is. I have a few remarks, however:

  1. I would explicitly declare lastSelected, at least as a global variable, ideally in a more scoped/namespaced way (perhaps just as a property of the handleClick function, which you’d then access internally using, for instance, arguments.callee.handleClick).
  2. I fail to see why you’d want the two first checks; I don’t see a use case where they’d be useful.
  3. Instead of nesting if’s, I tend to prefer inverting the conditions and short-circuting with return calls to keep it leaner and avoid remote else clauses.
  4. As of Prototype 1.6, observe calls the handler within the binding context of the element you observed on, so instead of going $('frmStudentList') (btw, are you coming from a Delphi background to use this naming scheme? :-)), you can just use this.
  5. getInputs already returns an actual array, so the wrapping $A call is superfluous.
  6. I would consider the last click always to define lastSelected, even when it’s a shift-click.

So I might rephrase your function like this:

var lastSelected = null;
handleClick: function(e)
{
    var origin = e.element();
    if (origin.nodeName.toLowerCase() != 'input')
        return;
    if (!e.shiftKey) {
        lastSelected = origin;
        return;
    }
    var checkBoxes = this.getInputs('checkbox');
    var first = checkBoxes.indexOf(origin);
    var last = checkBoxes.indexOf(lastSelected);
    if (-1 == last) {
        lastSelected = origin;
        return;
    }
    var start = Math.min(first, last), end = Math.max(first, last);
    for (var index = start; index < end; ++index)
        checkBoxes[i].checked = lastSelected.checked;
    lastSelected = origin;
} // handleClick

There are several tricks we could use to size it down a little, but they’d be somewhat detrimental to speed, so the trade-off isn’t so good.

‘HTH

 
Sms_small Smits 3 posts

Hi Christophe,

Thanks for your post and your remarks. Allow me to comment some of them.

1. I forgot to write in my origin post, but I declared lastSelected globally (in the window namespace). That’s why there was no var keyword to this variable. I already defined the function handleClick in a namespace. Can you give a reference how to use that “arguments.callee.handleClick” to have that variable in the same namespace as well.

2. Those checks are from an other try at another topic. They must be left out.

3. You are right that inverting conditions and using return calls give less lines of code. However, they taught me to write procedures (functions) with one entry point and one exit point, using nested if’s and so on. Now you’ve got a function with four exit points, but I get your point.

4. With respect to this point. Again, it is (in my opinion) the most difficult part of Javascript and your book. I thought this was referring to the original checkbox being clicked.
In my original code I declared the listener on a div within the form. So the code with the $('frmStudentList') replaced by this will not work in my very situation, because this will refer to that div. It is good to know that observe will give the element itself as being this in the function. I think it is important to realize this, but I can’t find this in the book. Is it to be found at page 100-108? I think it is worthwhile to write an article about this feature, including the bubbling and delegation aspects of events.

(My background is not Delphi, although I learned programming in Pascal, from a book of Niklaus Wirth, Algorithms+DataStructures=Programs. Later I learned SPSS, Visual Basic, dBASE, SQL, mySQL, PHP4 and 5, CSS from the Eric Meyer books and unobtrusive Javascript from Stuart Langridge and PPK. Since it is important to know what kind of object or type you have in your variable I am using the Hungarian naming convention, so all my string variables are prepended with str, objects with obj, arrays with arr and so on. Although your variable names are a little longer it gives you a very good view what to expect in a certain variable. In the html context I always have forms with (names and) ids like frm…, tables with id tbl.. and so on)

The only exception I make is to use i for an integer index, instead of the full index as you did. In your code you have to adjust that checkBox[i] also into checkBox[index]. Perhaps you can do that in your post to have a correct code snippet.

Thanks again for your comments and I hope you will enjoy Boston, at the end of September.

Warm regards from the Netherlands,
JS.

5 posts, 2 voices