User:Frank Ralf/JavaScript1

Jump to: navigation, search

Note: You are currently viewing documentation for Moodle 2.0. Up-to-date documentation for the latest stable version is available here: Frank Ralf/JavaScript1.

Checkboxes: Show only selected

This article was inspired by the General Developer forum discussion on "Interface design input please").

The task at hand is to make a possibly long check list of teachers more handy by showing only the selected items:

JavaScript teacher list2.png JavaScript teacher list only selected.png

Nothing fancy, but a good opportunity to compare different approaches using JavaScript:

  1. Home made JavaScript
  2. Using YUI, Moodle's default JavaScript library
  3. Using jQuery, another very popular JavaScript library

Plain JavaScript

That is the original code. There's a lot of DOM traversal and node counting going on due to browser incompatibilities.

<script type="text/javascript">
    var teacherslist=document.getElementById('pe_teachers')
    if(teacherslist.childNodes[0].nodeType==3){
        //this javascript engine is counting blank text nodes
        teacherslist=teacherslist.childNodes[4];
        var start=2;
        var step=2;
    }else{
        teacherslist=teacherslist.childNodes[2];
        var start=1;
        step=1;
    }
 
    function parentseve_toggleonlyselected(state){
        var tmp=teacherslist.childNodes;
        var listlength=teacherslist.childNodes.length;
        if(state){
            for(var i=listlength-start;i>=0;i=i-step){
               if(!tmp[i].childNodes[1].firstChild.firstChild.checked){
                   tmp[i].style.display='none';
               }
            }
        } else {
            for(var i=listlength-start;i>=0;i=i-step){
                tmp[i].style.display='block';
            }
        }
    }
    parentseve_toggleonlyselected(document.getElementsByName('showonlyselected')[0].checked);
</script>

YUI

Using Moodle's default JavaScript library YUI we can overcome those browser inconsistencies. We also move the inline "onclick" event handler to the JavaScript. (Note that the required YUI libraries are already part of a standard Moodle installation.) For attaching the event handler we had to add an ID to the select checkbox.

require_js(array(
        'yui_yahoo',
        'yui_dom-event',
        )
);
 
<script type="text/javascript">
 
  // Add the event listener to the checkbox
  var addclick = function(){
    YAHOO.util.Event.addListener('id_showselected', 'click', parentseve_toggleonlyselected);
  };
 
  // Get a list of all checkboxes
  var teachers = YAHOO.util.Dom.getElementsByClassName('pe_teacher', 'input', 'pe_teachers');
 
  var parentseve_toggleonlyselected = function() {
    for (var i=0; i < teachers.length; i++){
      // Get a list of the form items for each checkbox
      var formitem = YAHOO.util.Dom.getAncestorByClassName(teachers[i], 'fitem');
      // Hide unchecked teachers if "Show only selected" is checked
      if (YAHOO.util.Dom.get('id_showselected').checked){
        if (!teachers[i].checked){
          formitem.style.display='none';
        }
      } else {
          formitem.style.display='inline';
      };
    };
  };
 
// Initialise 
YAHOO.util.Event.onDOMReady(addclick);
</script>

jQuery

The jQuery code for the same functionality is significantly shorter. And you get some additional cool effects with the show() function. (Note that the required jQuery library is not part of a standard Moodle installation so you have to provide it yourself.)

require_js(array(
        'jquery/jquery-1.3.2.min.js',
        )
);
 
<script type="text/javascript">
 
$(document).ready(function(){
  $("input[name='showonlyselected']").click(function(){
    if(this.checked){
      $('.pe_teacher:not(:checked)').parents('.fitem').hide('normal');
    } else {
      $('.fitem').show('slow');
    };
  });
});
 
</script>

Advantages:

  • Always working with sets -> inherent looping.
  • Chaining of functions.
  • Provides shorthand (and cross browser) access to native JavaScript functions.


Fair comparison

You are asking us to compare the length and understandability of two bits of code, one of which has comments and one which doesn't, and one of which uses anonymous functions an one which doesn't. Allow me to edit the two lots of code to make it a fairer comparison (and also to match the Moodle coding style.--Tim Hunt 03:28, 12 July 2009 (UTC)

YUI

require_js('yui_dom-event');
 
<script type="text/javascript">
YAHOO.util.Event.onDOMReady(function() {
    YAHOO.util.Event.addListener('id_showselected', 'click', function() {
        var showselected = document.getElementById('id_showselected').checked;
        var teachers = YAHOO.util.Dom.getElementsByClassName('pe_teacher', 'input', 'pe_teachers');
        for (var i=0; i < teachers.length; i++) {
            var formitem = YAHOO.util.Dom.getAncestorByClassName(teachers[i], 'fitem');
            if (showselected && !teachers[i].checked) {
                formitem.style.display='none';
            } else {
                formitem.style.display='inline';
            }
        }
    });
});
</script>

jQuery

require_js('jquery/jquery-1.3.2.min.js');
 
<script type="text/javascript">
$(document).ready(function() {
    $("input[name = 'showonlyselected']").click(function(){
        if (this.checked) {
            $('.pe_teacher:not(:checked)').parents('.fitem').hide('normal');
        } else {
            $('.fitem').show('slow');
        };
    });
});
</script>

Actually, isn't this buggy? $('.fitem').show('slow'); will show every single .fitem on the whole form, even if they are nothing to do with this show/hide button? What would be very bad if you had two independent show/hide widgets on the same form. --Tim Hunt 03:28, 12 July 2009 (UTC)

You're right, Tim, the show() part is not very precise and would behave like you suggest. But that could easily be amended by defining the context of the form items more precisely, thus restricting the scope to the fieldset at hand: --Frank Ralf 16:15, 20 July 2009 (UTC)
$('#pe_teachers .fitem').show('slow');

Fair comparison - Next round

I take up the gauntlet ;-) Here's the refactored code both for YUI and jQuery.

I created a slightly simpler form for demonstration purposes. Here's the code:

The form

JavaScript1 Teacher list.png

$mform =& $this->_form; 
 
$mform->addElement('checkbox','headmaster', 'Mr. Hunt', 'Headmaster');
$mform->addElement('header', 'teachers', 'Teachers');
$teachers= array('Mr. Smith', 'Ms. Miller', 'Mr. Morgan');
    foreach($teachers as $teacher){
        $mform->addElement(
            'checkbox',
            '',
            $teacher,
            '',                       
            array('class'=>'teacher') 
        );
    };
$mform->closeHeaderBefore('showonlyselected');
$mform->addElement('checkbox','showonlyselected','','Show only selected teachers');

You see we added another checkbox outside of the fieldset to see whether our code affects any other checkboxes which it shouldn't.

YUI

Actually, I cheated a bit with the first version by giving the checkbox for the "Show only selected" option an ID. This is a bit problematic because in the HTML the ID is rendered as "id_showselected", which you might not suspect.

$mform->addElement(
    'checkbox',
    'showonlyselected',
    '',
    'Show only selected teachers', 
    array('id'=> 'showselected')
);

Because the jQuery code selects this form element by its name ($("input[name = 'showonlyselected']")), I changed the YUI code accordingly to use getElementsByName() instead of getElementById().

Note that these are no YUI methods but the usual JavaScript DOM ones (and therefor applied to document.). YUI only enhances the JavaScript DOM methods. (Whether this should be considered a bug or a feature is open to discussion.)

So here's is the refactored code which matches the jQuery version more closely:

<script type="text/javascript">
YAHOO.util.Event.onDOMReady(function() {
    var selectbox = document.getElementsByName('showonlyselected')[0];
    YAHOO.util.Event.addListener(selectbox, 'click', function() {
        var teachers = YAHOO.util.Dom.getElementsByClassName('teacher', 'input', 'teachers');
        for (var i=0; i < teachers.length; i++) {
            var formitem = YAHOO.util.Dom.getAncestorByClassName(teachers[i], 'fitem');
            if (selectbox.checked && !teachers[i].checked) {
                formitem.style.display='none';
            } else {
                formitem.style.display='inline';
            }
        }
    });
});
</script>

jQuery

And here's the optimized jQuery code. Please note:

  • Instead of parents() we're using the new method closest() in jQuery 1.3 which prevents us from targeting too many form items up the DOM tree.
  • We use the convenient toggle() method instead of separate hide() and show().
  • The toggle() method provides animation as feedback for the user to see what's going on with the form. This kind of feedback is lacking from the YUI version (We leave adding this as an exercise for the reader).
<script type="text/javascript">
$(document).ready(function() {
    $("input[name='showonlyselected']").click(function(){
        $('.teacher:not(:checked)').closest('.fitem').toggle('normal');
    });
});
</script>

Another version

Here's another version which is even more concise and nearly reads like plain English.

  • We get rid of the DOM traversing which was mimicking the native JavaScript solution above.
  • We use the shortcut $(function() {...} instead of $(document).ready(function() {...}.
  • We don't need the class="teacher" anymore on the checkbox items.
  • EDIT: We make the solution more robust by ensuring that the selectbox is unchecked when (re-)loading the page. Actually we are not checking the "checked" state of the selectbox anymore but only toggle the selectbox and the form items in parallel. That might get out of synch if we reload the page while the selectbox is checked.
<script type="text/javascript">
$(function() {
    $("input[name='showonlyselected']").removeAttr("checked").click(function(){
        $('#teachers .fitem:has(input:not(:checked))').toggle('normal');
    });
});
</script>
If you think that reads like plain English then I must be speaking some other language :-P--Tim Hunt 09:57, 22 July 2009 (UTC)
Well, we're talking in comparison to the YUI or native JavaScript solution... --Frank Ralf 10:32, 22 July 2009 (UTC)

What's left to do?

As this feature will be only available to the user when she has JavaScript activated in her browser we should display the "Show only selected" checkbox also only when JavaScript is available.

Next challenge

OK, that latest comparison looks quite favourable to jQuery. However, this is a very simple example, and that is always dangerous. For example compare "Hello world" in PHP and Java:

<?php echo "Hello world!" ?>
 
// Compared to
 
class myfirstjavaprog {  
    public static void main(String args[]) {
        System.out.println("Hello World!");
    }
}

It would be a mistake to conclude from that that PHP is a better language for building a large system.

If you really want to convince me, I would like to see a conversion of http://cvs.moodle.org/moodle/user/selector/script.js?view=markup (it is used on the assign roles and add group members pages). That is a serious application of JavaScript. However, that is also a lot of code, so it is not really fair to ask for a conversion just as a proof of concept.

Re: "Note that these are no YUI methods but the usual JavaScript DOM ones (and therefor applied to document.). YUI only enhances the JavaScript DOM methods."
That is definitely a strength of YUI. HTML DOM is a widely supported standard. Any in-browser JS should build on that, rather than ignore it.--Tim Hunt 04:52, 21 July 2009 (UTC)

Thanks for the feedback, Tim - and the next challenge... --Frank Ralf 07:48, 21 July 2009 (UTC)

The next challenge will take place on User:Frank Ralf/JavaScript2.

See also