User:Frank Ralf/JavaScript2

Jump to: navigation, search

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

YUI vs. jQuery - The big challenge!

"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."
--Tim Hunt 04:52, 21 July 2009 (UTC)

  • Look here for the background for this challenge...
  • Feel free to add comments on the talk page!
  • We will tackle this big task step by step ...
  • The examples were tested with FF3 and IE6 to check for cross-browser compatibility.

AJAX based selecting of users

Screenshot

For those who don't have a current installation of the brand new Moodle 2.0 at hand here's a screenshot so you get an idea what this is all about:

Moodle 2.0 Assign role with AJAX.png

While you are entering text in the search field the database is queried in real-time in the background for matching users. This is done using AJAX without any page re-loading required.

For comparison here's the same page with JavaScript turned off:

Moodle 2.0 Assign role without JavaScript.png

The underlying code

Getting ready for jQuery

If you want to play along you first have to add jQuery to your Moodle installation. Just download the library (56 KB) and copy it into the /user/selector folder. Then you add jQuery to the list of required JavaScript in /user/selector/lib.php:

// Required JavaScript code.
$PAGE->requires->yui_lib('json');
$PAGE->requires->yui_lib('connection');
$PAGE->requires->yui_lib('datasource');
// Adding jQuery before other JS files of the module
$PAGE->requires->js('user/selector/jquery-1.3.2.min.js');
$PAGE->requires->js('user/selector/script.js');

A) DOM manipulation - moving, adding and deleting HTML elements

A very common task when using JavaScript for modifying HTML is moving, adding and deleting HTML elements. So that's where we will start our venture.

The task

Following the principle of Unobtrusive JavaScript the form must also be usable with JavaScript turned off.

So without JavaScript the form contains a search field and two submit buttons: one for executing the search and one for clearing the search field. Clicking any of those two buttons triggers a request to the server which will execute the function and sends a new page back to the browser.

Without JavaScript
Buttons without JavaScript.png

For the AJAX version the search is done automatically and asynchronously (remember the "A" of "AJAX") while typing in the search field. Therefore the form should not be sent and the search button will be replaced by a simple label which will be placed to the left of the search field. This is the first task we will tackle.

AJAX version
Buttons with AJAX.png

The current solution

Here's the original code. We're creating a label, inserting it before the search field and then delete the search button.

// Hide the search button and replace it with a label.
var searchbutton = document.getElementById(this.name + '_searchbutton');
 
var label = document.createElement('label');
label.htmlFor = this.name + '_searchtext';
label.appendChild(document.createTextNode(searchbutton.value));
this.searchfield.parentNode.insertBefore(label, this.searchfield);
searchbutton.parentNode.removeChild(searchbutton);

You might be wondering why there's no sign of YUI. Well, YUI hasn't entered the ring yet. It might come as a surprise, but YUI's DOM utility doesn't provide any methods for creating and moving HTML elements. YUI's Element utility provides rudimentary support, but most of the time we have to resort to JavaScript's build in methods, which are not the most intuitive. - Some consider this a feature, not a bug, though. ;-)

The jQuery solution

jQuery on the other hand provides a whole bunch of methods for manipulating the DOM tree. So we can accomplish the same task with the following code.

Note that the order of the steps is slightly different from the above version:

Here we

  • take the search button,
  • insert it before the corresponding search field,
  • and then replace it with a label with the same text as the search button.
var searchbutton = document.getElementById(this.name + '_searchbutton');
 
$(searchbutton)
.insertBefore(this.searchfield)
.replaceWith('<label>' + searchbutton.value + '</label>');

B) Modifying the Clear button

After catering for the search button, the Clear button is next on our list. This time we can't get rid of the button altogether because we still need its function also in the AJAX version.

The current code

// Replace the Clear submit button with a clone that is not a submit button.
var oldclearbutton = document.getElementById(this.name + '_clearbutton');
this.clearbutton = document.createElement('input');
this.clearbutton.type = 'button';
this.clearbutton.value = oldclearbutton.value;
this.clearbutton.id = oldclearbutton.id;
oldclearbutton.id = '';
oldclearbutton.parentNode.insertBefore(this.clearbutton, oldclearbutton);
oldclearbutton.parentNode.removeChild(oldclearbutton);

The task

On first glance this resembles the first task: replacing the Clear button with something else. But wait, why replacing it with another button that looks exactly like the original one? The only difference is that the clone is not a "submit" button (<input type="submit" />) but a simple "button" (<input type="button" />).

What's that good for? Well, the purpose is to prevent the button from sending a request to the server when being clicked, which is the default behavior for submit buttons. In the AJAX version, we instead apply our own click handler to the Clear button and clear the search field with client-side code alone. So we have to look at the above code snippet in conjunction with the click handler for the Clear button:

// Hook up the event handler for the clear button.
YAHOO.util.Event.addListener(this.clearbutton, "click", function(e) { oself.handle_clear() });
I am pretty sure this is the first thing I tried, and it did not work reliably cross-browser. The only option was to throw away the old button and make a new one. However, I don't remember the details now, an they don't seem to have been preserved in CVS.--Tim Hunt 09:41, 27 July 2009 (UTC)
I tried myself altering your original code using .preventDefault() but that indeed didn't work across browsers. The magic with jQuery happens under the covers, see below. --Frank Ralf 16:40, 27 July 2009 (UTC)
Hmm. Perhaps the problem is with what happens if you press enter when in the text field. If the browser can find a submit button, then perhaps it submits the form, or something like that. However, I am a bit suprised I wrote the code to replace the button without adding a comment explaining why it was necessary.--Tim Hunt 15:26, 27 July 2009 (UTC)
We'll cover that in the next section ;-) My preliminary testing shows that this will do nothing in FF and clear the search field in IE6. --Frank Ralf 16:40, 27 July 2009 (UTC)

The jQuery solution

With jQuery we don't have to clone the Clear button. We instead leave it as it is (as a "submit" button) but prevent the sending of a request to the server by using the DOM method .preventDefault() on the click event we attach to the Clear button:

this.clearbutton = document.getElementById(this.name + '_clearbutton');
 
$(this.clearbutton).click(function(event){
oself.handle_clear();
event.preventDefault();
});

Unfortunately, .preventDefault() will usually not work across browsers with normal JavaScript due to different implementations of the Event object. But jQuery does some standardizing under the hood to provide cross-browser compatibility for most Event properties and methods:

"jQuery's event system normalizes the event object according to W3C standards. The event object is guaranteed to be passed to the event handler (no checks for window.event required). Most properties from the original event are copied over to our wrapper object."

event.preventDefault()

C) Replacing other event handlers

We can replace other event handlers in a similar way.

Search field changes

The YUI version:

// Hook up the event handler for when the search text changes.
YAHOO.util.Event.addListener(this.searchfield, "keyup", function(e) { oself.handle_keyup(e) });

And this is the jQuery equivalent:

$(this.searchfield).keyup(function(event) {
oself.handle_keyup(event);
event.preventDefault;
});

We add preventDefault() as a replacement for the following code in the handle_keyup() function:

// If enter was pressed, prevent a form submission from happening.
var keyCode = e.keyCode ? e.keyCode : e.which;
if (keyCode == 13) {
YAHOO.util.Event.stopEvent(e);
}

However, I haven't noticed any change in behavior either way which surely is due to my cursorily browser testing.

Selection changes

This is the YUI code:

// Hook up the event handler for when the selection changes.
YAHOO.util.Event.addListener(this.listbox, "keyup", function(e) { oself.handle_selection_change() });
YAHOO.util.Event.addListener(this.listbox, "click", function(e) { oself.handle_selection_change() });
YAHOO.util.Event.addListener(this.listbox, "change", function(e) { oself.handle_selection_change() });

And this the jQuery equivalent, which is a bit less verbose due to jQuery's chaining capability:

$(this.listbox)
.keyup (function(event){oself.handle_selection_change()})
.click (function(event){oself.handle_selection_change()})
.change(function(event){oself.handle_selection_change()});

Search anywhere option

The YUI code:

// And when the search any substring preference changes. Do an immediate re-search.
YAHOO.util.Event.addListener('userselector_searchanywhereid', 'click', function(e) { oself.handle_searchanywhere_change() });

And this is the jQuery version:

$('#userselector_searchanywhereid')
.click(function(event){oself.handle_searchanywhere_change()});

Note that with YUI you just use the element's ID as a selector whereas with jQuery you have to add the pound sign "#" to the ID like with CSS.


[TODO]

See also