Tutorial # 6: jQuery-style XML handling (without taconite)

Table of contents

Tutorial # 6: jQuery-style XML handling (without taconite)
1: Javascript to tell jQuery to expect XML from the server
2: Perl for generating XML on the server
3: Javascript for accepting XML from the server
Traps
taconite
History
Links

Tutorial # 6: jQuery-style XML handling (without taconite)

1: Javascript to tell jQuery to expect XML from the server

        function add_staff()
        {
                var occ_title   = $("#add_staff_occ_title").val();
                var person_name = $("#add_staff_person_name").val();

                if ( (occ_title == "") || (person_name == "") )
                {
                        $("#add_staff_person_name").focus();

                        return;
                }

                $.ajax
                ({
                        data:
                        {
                                occupation_title: occ_title,
                                organization_id:  $("#update_org_id").val(),
                                person_name:      person_name,
                                sid:              $("#update_org_sid").val()
                        },
                        dataType: "xml",
                        type: "POST",
                        url: "/Occupation/add_staff",
                        ...
                });
        }

Notes:

# 1: Here we use {dataType: "xml"} to tell jQuery to expect XML from the server.

# 2: This code is adding a staff member to an organization. Since the organization's data is on-screen, we know the organization's id, but the person's name and occupation (job) title are being typed in (with auto-complete activated) so we send the latter 2 fields to the server as strings rather than sending their ids.

To be specific, the person must be on file, but the occupation may not be, in which case it'll be added to the database.

2: Perl for generating XML on the server

Step 1: Output the right Content-Type:

Since I'm using a sub-class of CGI::Snapp, I can call its add_header(...) method.

        $self -> add_header('-Status' => 200, '-Content-Type' => 'text/xml; charset=utf-8');

Notes:

# 1: Since these headers are passed to CGI (actually CGI::PSGI in this case) we need '-' at the start of the keys for this hash of header parameters.

# 2: Since I'll be outputting UTF8, I have to tell the web server and web client about it.

Step 2: Generate the string of XML to send:

        sub build_ok_xml
        {
                my($self, $html) = @_;

                $self -> log(debug => 'Controller::Occupation.build_ok_xml(...)');

                # Recover the organization id and person id from the session.
                # See delete_occupation()
                # and App::Office::Contacts::View::Occupation.add()

                my($organization_id) = $self -> param('db') -> session -> param('staff_organization_id');
                my($person_id)       = $self -> param('db') -> session -> param('occupation_person_id');

                return
        qq|<response>
                <error></error>
                <org_id>$organization_id</org_id>
                <person_id>$person_id</person_id>
                <html><![CDATA[$html]]></html>
        </response>
        |;

        } # End of build_ok_xml.

Notes:

# 1: The response, if there was no error, will use $html to replace the content of a div holding the on-screen list of staff for the current organization.

# 2: Notice how I've jammed the $html into a [CDATA[...]] context, to protect the HTML, which here happens to be a table, with 1 row per person per occupation. Each row contains a clickable link to delete the person-organization connexion on that line. If that person has any other occupations with the same organization, those are not affected. The link in turn calls a JS function which does an AJAX call to delete the person from that organization's list of staff in the database on the server, and returns an updated table of person-organization connexions.

But there's more. The person who was deleted from the staff list may themselves be being displayed on another tab ('Update Person'). So the JS function also checks to see if that's true, and if so triggers another AJAX call to update that person's list of occupations.

Visa versa, if one of a person's occupations is deleted via the 'Update Person' tab, the code first updates the displayed list of occupations (i.e. on the current tab of personal details), and then does another AJAX call to update the corresponding organization's list of staff, if that organization happens to be the one displayed on the 'Update Organization' tab.

# 3: The empty container for errors is just for neatness. If something had gone wrong with the attempted delete, Try::Tiny would have trapped the error, which would be logged, and a simple <error>...</error> container would be sent back to the client.

3: Javascript for accepting XML from the server

This code is within the JS function add_staff(...):

        success: function(response, status, jqXHR)
        {
                // Step 1: Check for errors.

                var $xml  = $(response);
                var error = $xml.find("error").text();
                var html  = $xml.find("html").text();

                if (error != "")
                {
                        status_error(error, html);

                        return;
                }

                // Step 2: Display updated staff list.

                $("#staff_list_div")
                .empty()
                .append(html);

                $("#status_div").val("Status: Added staff member");
                $("#add_staff_person_name").val("").focus();
                $("#add_staff_occ_title").val("");

                // Step 3: Check to see if the person who was added
                // to the staff is currently displayed.
                // If they are visible, their list of occupations must be updated.

                var visible_person_id = $("#update_person_id").val();

                if (visible_person_id == null)
                {
                        return;
                }

                var ajax_person_id = $xml.find("person_id").text();

                if (visible_person_id == ajax_person_id)
                {
                        update_occupations(person_name, visible_person_id, $("#staff_sid").val() );
                }
        },
        error: function(jqXHR, status, error)
        {
                status_error("Cannot add " + person_name, "HTTP error: " + error);
        }
        ...

Notes:

# 1: Notice how the return string from the server, 'response', is wrapped in $(response) to prepare it for processing by jQuery.

This point is of huge significance, since over the years various jQuery plugins have does things slightly differently, and books have been written advocating and hence snap-freezing now-obsolete code.

You have been warned!

Here, I'm using jQuery V 1.8 which is shipped as part of DataTables V 1.9.4. And yes, jQuery 1.9 has been released.

# 2: You can see that $xml.find(...) is used to extract the various components of the XML sent by the server.

        var error = $xml.find("error").text();
        ...
        $("#staff_list_div")
        .empty()
        .append($xml.find("staff").text() );
        ...
        var visible_person_id = $("#update_person_id").val();

Do not use jQuery's $.parseXML(string). It is only designed for the case where 'string' is purely XML and nothing else! Here, the 'response' is an object.

You have been warned!

Traps

As you know, jQuery is full of astonishing and infuriating traps.

Here are some connected with XML.

taconite

See the taconite website for examples, docs and download info.

taconite is a jQuery plugin which allows you to send an XML message from the server. But - this XML is specially constructed, and contains 1 or more commands to be executed by taconite.

The examples on the taconite website are impressive, but I should explain why I rejected taconite in my current application (App::Office::Contacts):

# 1: All taconite commands are verbs, but I needed the complexity available with an 'if'.

Some of taconite's commands are: append, prepend, remove, replace (a div say) and replaceContent (a div's content), etc.

The clever thing about taconite is that a series of commands can appear in a single XML message, and hence a set of divs etc can be updated with just that one message. This is impressive.

# 2: In my case, I wanted to stop taconite automatically executing commands, and to first be able to perform various tests (see code above) before deciding what exactly to do.

But I still want to transfer a set of fields from the server to the client, so I copied the idea implemented by taconite, with the modification that I extract the fields manually and then process them individually.

# 3: As for taconite, I need to warn you about some of it's characteristics:

A) taconite hooks into jQuery so as to grab the server's response before the success function executes.

This means the XML absolutely must look like <taconite>...</taconite>.

B) Inside this XML there is no provision for arbitrary user data. Each container at the first level inside the <taconite>...</taconite> must use one of taconite's pre-defined verbs as the tag.

C) I did try sending a message a bit like this: <taconite><staff>...</staff></taconite>, and used jQuery to extract the 'staff' component, along with the fields mentioned above, org_id and person_id.

I figured I might be able to trick taconite in this way, so that I could use code in the success function to process the org_id and person_id fields, and then when the time came I could re-wrap the 'staff' content in another <taconite>...</taconite> tag-set, and pass that to taconite manually. And taconite does actually have such a facility to process a string of XML.

But it all failed because my non-taconite tags did not work.

Perhaps I was just doing it wrongly, but since taconite lacked that fundamental requirement (for me) of conditional execution, I did not return to using taconite after I got my taconite-free code working.

History

V 1.01 2013-07-01. Update after releasing App::Office::Contacts V 2.01.

V 1.00 2013-05-07. Original version.

Links

My home page

All tutorials (Includes references)

POD source for this tutorial