User:Mark Johnson/Javascript

Jump to: navigation, search

This page follows on from Frank Ralf's work at examining jQuery vs YUI2, this time looking at YUI3. The idea is to establish what the relative advantages/disadvantages/differences are between the two, and whether there's any compelling reason to switch Moodle's Javascript framework from YUI3 to jQuery.

Experiment 1

The experiment shown below is a conversion of my Quickcourselist block from YUI to jQuery. The block allows a user to type all or part of a course's shortname or fullname, and presents a list of links to matching courses. The block can work without Javascript by displaying a submit button, but is progressively enhanced to allow instant searching via AJAX.

To achieve this, the javascript must attach event listeners, perform ajax requests, and perform some DOM manipulation. I've recently become aware that the effect could possibly be achieved more easily using YUI's AutoComplete widget. It would be interesting to see this solution and how it would compare to the jQuery/jQuery UI equivalent.

All code for the block is available on Github. The original code can be found in the "dev" branch - the converted code can be found in the "jq" branch. I am an expert in neither, so any improvements to either version are welcome (please use this page's discussion).

Enabling jQuery

This was simple - the jQuery library was downloaded and saved in the block's directory. The following line then included the library:

$this->page->requires->js('/blocks/quickcourselist/jquery.js');

jQuery/$ then became available as a global Javascript function.

init function

The init function finds the progress indicator so it can be shown/hidden later, and attaches the event handlers to call the search function. We attach a handler for when a character is typed, and one for when the form is submitted (the submit button is hidden when Javascript is on, but a user may still press Enter).

YUI:

init: function(Y, instanceid) {
        this.Y = Y;
        this.instanceid = instanceid;
 
        this.progress = Y.one('#quickcourseprogress');
        this.xhr = null;
 
        Y.one('#quickcourselistsearch').on('keyup', function(e) {
            var searchstring = e.target.get('value');
            this.search(searchstring);
        }, this);
        Y.one('#quickcourseform').on('submit', function(e) {
            e.preventDefault();
            var searchstring = e.target.getById('quickcourselistsearch').get('value');
            this.search(searchstring);
        }, this);
    }

jQuery:

init: function(instanceid) {
        this.instanceid = instanceid;
 
        this.progress = $('#quickcourseprogress');
        this.xhr = null;
 
        $('#quickcourselistsearch').keyup(function(e) {
            var searchstring = e.target.value;
            M.block_quickcourselist.search(searchstring);
        });
        $('#quickcourseform').submit(function(e) {
            e.preventDefault();
            var searchstring = $('#quickcourselistsearch').get('value');
            M.block_quickcourselist.search(searchstring);
        });
    }

The jQuery code is marginally simpler for this function. YUI passes an "event façade" object to the event handler rather than the raw DOM event, meaning that an additional method call is requires to get the search box's value. However, it also allows the search box to be pinpointed within the form when handling the submit event, rather than having to search the entire document, which is probably a bit quicker. In this example, jQuery uses a specific method for attaching the keyup and submit event handlers. This is slightly tidier if wishing to attach a single handler, or wishing to chain method calls to attach different handlers for different events. The .bind() method is also available to attach a handler to multiple events, as you can with Y.on(). Finally, as Tim Hunt pointed it, it's also possible to pass an object to Y.on() to use as the context for the callback, as shown here by the use of this.

AJAX

YUI:

search: function(string) {
 
        var Y = this.Y;
        uri = M.cfg.wwwroot+'/blocks/quickcourselist/quickcourse.php';
        if (this.xhr != null) {
            this.xhr.abort();
        }
        this.progress.setStyle('visibility', 'visible');
        this.xhr = Y.io(uri, {
            data: 'course='+string+'&instanceid='+this.instanceid,
            context: this,
            on: {
                success: function(id, o) {
                    ...
                },
                failure: function(id, o) {
                    ...
                }
            }
        });
    }

jQuery:

search: function(string) {
 
        uri = M.cfg.wwwroot+'/blocks/quickcourselist/quickcourse.php';
        if (this.xhr != null) {
            this.xhr.abort();
        }
        this.progress.css('visibility', 'visible');
        this.xhr = $.ajax(uri, {
            data: 'course='+string+'&instanceid='+this.instanceid,
            context: M.block_quickcourselist,
            success: function(courses, status) {
                ...
            },
            error: function(o, status) {
                ...
            }
        });
    }

These two examples are very similar indeed. The only notable thing that's nicer with jQuery is that it parses the JSON in the responseText automatically on successs, returning an object rather than a string as the first argument.

YUI does let you set the context for event handlers: http://yuilibrary.com/yui/docs/api/classes/Node.html#method_on--Tim Hunt 17:34, 2 November 2011 (WST)
Thanks Tim, I've amended the Y.on example above. Further investigation shows the context specification in Y.io to be possible too: http://yuilibrary.com/yui/docs/io/#the-configuration-object --Mark Johnson 16:47, 23 November 2011 (WST)

DOM manipulation

YUI:

success: function(id, o) {
                    var courses = Y.JSON.parse(o.responseText);
                    var block = M.block_quickcourselist;
                    list = Y.Node.create('<ul />');
                    if (courses.length > 0) {
                        Y.Array.each(courses, function(course) {
                            Y.Node.create('<li><a href="'+M.cfg.wwwroot+'/course/view.php?id='+course.id+'">'+course.shortname+' '+course.fullname+'</a></li>').appendTo(list);
                        });
                    }
                    Y.one('#quickcourselist').replace(list);
                    list.setAttribute('id', 'quickcourselist');
                    block.progress.setStyle('visibility', 'hidden');
                },

jQuery:

success: function(courses, status) {
                list = $('<ul />');
                if (courses.length > 0) {
                    $.each(courses, function(key, course) {
                        $('<li><a href="'+M.cfg.wwwroot+'/course/view.php?id='+course.id+'">'+course.shortname+' '+course.fullname+'</a></li>').appendTo(list);
                    });
                }
                $('#quickcourselist').replaceWith(list);
                list.attr('id', 'quickcourselist');
                this.progress.css('visibility', 'hidden');
            }

There's really nothing to call between these two examples, except perhaps the use of $() being slightly more concise than Y.Node.create().

Conclusions

This simple example shows that jQuery can be slightly cleaner or more convenient to write than YUI3. However, in terms of functionality, at this level there is nothing to call between them.

As a relative jQuery newbie, while performing this experiment I drew upon the jQuery - YUI3 rosetta stone and jQuery API reference. The two frameworks are structured very differently, YUI being fiercely modular and keeping each module's methods in seperate namespaces, while jQuery's core functionality is all made available as methods of the jQuery/$ global. jQuery has a lot of plugins available to extend and enhance its functionality. Further experiments should look at more complex examples to see if the functionality required by Moodle would require a reliance on 3rd party jQuery plugins, and if this option is sustainable.

Comments

As an additional comment, I have had some experience with the Prototype framework and find that Prototype would also be tidier in places than either YUI or jQuery. For example, Prototype implements each() as a method of the Array object, so the example above could use

courses.each(fn)

in place of

Y.Array.each(courses, fn)
or
$.each(courses, fn)

which is, IMHO, a lot nicer. This isn't a suggestion that we should switch to Prototype, but merely a demonstration that each framework has it's relative merits, and which is "best" is probably largely subjective.

Mark Johnson 16:56, 2 November 2011 (WST)