Wednesday 28 November 2012

Widgets, Web Services and Libraries - Oh My!

The time has come to fulfill my promise [0] to write a blog post on our use of widgets at Swansea University.

Background


To cut a long story short, the decision to focus on developing widgets came about through the realisation that the Web Team were being asked increasingly to provide data to a number of disparate university web based systems such as Blackboard, iFind Discover (Our library catalogue powered by VuFind and Voyager) iFind Research (Our remote discovery service powered by Xerxes and Metalib / SFX) and our intranet and web pages. We therefore wanted to create a service which followed a recognised pattern, required very little technical proficiency to implement on the client side and was easy to share and distribute. Thankfully, our lead developer Geraint always has his finger on the pulse so he was able to suggest that widgets, self-contained, platform agnostic web elements, would meet all our needs, allowing us to deliver content, style and functionality in one easy to access bundle.

The fundamental basis of approach was taken form an article by Alex Marandon [1]. In our environment, this equates to something like:

1) Data API (e.g. Rebus List - our reading list solution from PTFS Europe)
2) Data Access Object (Web Team Developed object which queries the API)
3) Widget Controller (Loads the Data Access Object, processes data, returns data in required format)
4) Widget JavaScript (Collects variables, sends them to the controller via ajax and uses a callback function to process the results)
5) Client added script tag and target html element:

<script type="text/javascript" src="http://example.com/widget/data.js"></script>
<div class="widgety-goodness" data-id="1234"></div>


This method of delivering content places virtually all of the development requirements for a service on the web team. All a client needs to be able to do is embed two lines of code in their web page with any required variables. The only further developments a client may wish to undertake is to create custom css styles or JavaScript / jQuery behaviours.

A Guided Example


For those more interested in the technical details of the service, I will now try to outline the development of our Rebus List Widget with a particular emphasis on modifications we have made to Alex Marandon's original post.

Requirements:


Get a particular course reading list from Rebus List by supplying a valid course code

Process:


1) Script tag and HTML element (Blackboard Team - Swansea University)

The Blackboard team embed the required script and html elements to each course page and generate the course id. They have also created their own style sheet which will override the default css supplied by the widget.

Example:

<script type="text/javascript" src="https://www.swan.ac.uk/widgets/js/rebus/course-reading-list.js"></script>
<div class="rebus-course-reading-list-widget" data-course="CS-061">&nbsp;</div>



2) Widget JavaScript (Web Team - Swansea University)


The Widget JavaScript grabs the course id from the html element attribute and submits it via Ajax to the Widget controller. If successful, it loads the returned content in to the html element in addition to grabbing required resources like html5shiv [3] or css files.

The major differences in our JavaScript files compared to those suggested is that we have formatted them using JSLint [4] and added methods to deal with problems caused by the way IE7 loads css and js files.

This particular example does not have any jQuery actions associated with the delivered content but it could be added as part of the $.getJSON method if required.

Example:

/*jslint browser: true, devel: true, sloppy: true */

(function () {
    var jQuery,
        script_tag,
        main = function () {

            jQuery(function ($) {

                var $targets = $('.rebus-course-reading-list-widget'),
                    devEnv = window.location.search.indexOf('env=dev') !== -1,
                    baseUrl = devEnv ? '/widgets' : '//www.swan.ac.uk/widgets',
                    styleSheet = baseUrl + '/css/rebus/course-reading-list.css';

                (document.createStyleSheet)
                    ? document.createStyleSheet(styleSheet)
                    : $('head').append($('<link>', {
                        rel:  'stylesheet',
                        type: 'text/css',
                        href: styleSheet
                    }));
                       
                // Do we need html5shiv?
                if ($.browser.msie && $.browser.version < 8) {
                    script_tag = document.createElement('script');
                    script_tag.setAttribute("type", "text/javascript");
                    script_tag.setAttribute("src",  baseUrl + "widgets/vendor/html5shiv/html5shiv.js");
                    (document.getElementsByTagName("head")[0] || document.documentElement).appendChild(script_tag);
                }

                $targets.each(function () {
                    var $this = $(this),
                        course = $this.attr('data-course'),
                        uri = baseUrl + '/controllers/rebus/course-reading-list.php?callback=?&course=' + course;

                    $.getJSON(uri, function (data) {
                        $this.html(data.html);

                    });
                });
            });
        },
        scriptLoadHandler = function () {
            jQuery = window.jQuery.noConflict(true);
            main();
        };

    if (window.jQuery === undefined || window.jQuery.fn.jquery !== '1.5.2') {
        script_tag = document.createElement('script');
        script_tag.setAttribute("type", "text/javascript");
        script_tag.setAttribute("src", "//ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js");

        if (script_tag.readyState) { // old-IE
            script_tag.onreadystatechange = function () {
                if (this.readyState === 'complete' || this.readyState === 'loaded') {
                    scriptLoadHandler();
                }
            };
        } else {
            script_tag.onload = scriptLoadHandler;
        }

        (document.getElementsByTagName("head")[0] || document.documentElement).appendChild(script_tag);
    } else {
        jQuery = window.jQuery;
        main();
    }
}());

3) Widget Controller - (Web Team - Swansea University)


The widget controller receives the course id, calls the getListsByCourseId method in the data access object, assigns the result to a variable used by Mustache to create a html template and finally returns the template using the json content type.

NB: You don't have to use Mustache - you could use any html templating system or just build the html yourself in the controller.

Example:

require_once realpath(__DIR__ . '/../../..') . '/config/config.php';

require PATH_TO_HELPERS . '/handle-error.php';

// Validate request

$clean = new stdClass();

if (! array_key_exists('course', $_GET)) {
handleError('HTTP/1.1 400 Bad Request', 'Required parameter "course" not specified');
exit();
} else {
$clean->course = (strpos($_GET['course'], "_") !== false)
? preg_replace('/^.*_/', "", $_GET['course']): $_GET['course'];
}

if (array_key_exists('callback', $_GET)) {
$clean->callback = $_GET['callback'];
}

// Fetch Data

try {
require_once 'SU/DataAccess/Rebus.php';
$dao = new SU_DataAccess_Rebus();
$view = (object) array(
'lists' => $dao->getListsByCourseId($clean->course, true, true)
);
} catch (SU_Exception $e) {
handleError('HTTP/1.0 404 Not Found', $e->getMessage());
exit();
}

// Render
$template = file_get_contents(PATH_TO_TEMPLATES . '/rebus/course-reading-list.mustache');
$m = new Mustache();

$data = (object) array(
'html' => $m->render($template, $view)
);

if (isset($clean->callback)) {
header('Content-Type: text/javascript');
header('X-XSS-Protection: 0');
printf('%s(%s)', $clean->callback, json_encode($data));
} else {


// Used for debugging only
print $data->html;
}


Example Output:

jQuery15205464451520296695_1354114033050({"html":"<div id=\"rebus-reading-list\"> ..."})



4) Rebus Data Access Object (Web Team - Swansea University)

The Rebus data access object receives a course identifier, builds and calls a url using the Zend HTTP Client, parses the received XML data and returns an array of item data, optionally sorted by the reading list category heading.

Example Output:

Array ( [0] => Array ( [list_id] => 15 [org_unit_id] => 16 [org_unit_name] => Science [year] => 2012 [list_name] => CS-061 Introduction to computing I [published] => y [no_students] => 0 [creation_date] => 1345540250 [last_updated] => 1349271819 [course_identifier] => CS-061 [associated_staff] => [categories] => Array ( [0] => Array ( [title] => Essential reading [items] => Array ( [0] => Array ( [category_heading] => Essential reading [material_type] => Book [title] => Java for everyone / Cay Horstmann. [secondary_title] => [authors] => Horstmann, Cay S., [secondary_authors] => [edition] => [volume] => [issue] => [start_page] => [end_page] => [year] => c2010. [publisher] => John Wiley & Sons, [publication_place] => Hoboken, New Jersey: [publication_date] => c2010. [print_control_no] => 9780471791911 [elec_control_no] => [note] => [creation_date] => 1345540306 [modified_date] => 0 [lms_print_id] => 598756 [lms_elec_id] => [url] => [tags] => ) [1] => Array ( [category_heading] => Essential reading [material_type] => Book [title] => Computer science : an overview / J. Glenn Brookshear. [secondary_title] => [authors] => Brookshear, J. Glenn. [secondary_authors] => [edition] => 10th ed. [volume] => [issue] => [start_page] => [end_page] => [year] => c2009. [publisher] => Pearson Addison-Wesley, [publication_place] => Boston, Mass. ; [publication_date] => c2009. [print_control_no] => 9780321544285 [elec_control_no] => [note] => [creation_date] => 1345540363 [modified_date] => 0 [lms_print_id] => 561530 [lms_elec_id] => [url] => [tags] => ) ) ) ) ) )


5) Data API (PFTS Europe - Rebus List)

Rebus List comes with a very useful API which will return a list of items in a reading list in an xml format when supplied with a valid course code. It can be access using the syntax:

http://xxx.xxxxxxx.com/api?service=lists_by_course_identifier&course_identifier=ASQ201

The Widget Output

Default Styling:



Blackboard Styling:





[0] http://shambrarianknights.blogspot.co.uk/2012/10/library-camp-2012-part-2.html
[1] http://alexmarandon.com/articles/web_widget_jquery/
[2] http://mustache.github.com/
[3] http://code.google.com/p/html5shiv/
[4] http://www.jslint.com/