C H A P T E R  16

Refactoring the Example: Part II

I have introduced some very rich features in this part of the book, and as before, I want to bring them together to give a broader view of jQuery. I am not going to try to preserve a workable non-JavaScript structure in this chapter, since all of the features I am adding to the example rely heavily on JavaScript.

Reviewing the Refactored Example

In Chapter 11, you used the core jQuery features to refactor the example to include DOM manipulation, effects, and events. Listing 16-1 shows the document you ended up with, which will be the starting point for this chapter as you integrate the features from this part of the book.

There are a number of points in the script where I insert elements dynamically. Rather than making these static, I am going to leave them as they are so that you can focus on adding the new features.

Listing 16-1. The Starting Point for This Chapter

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-1.7.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <style type="text/css">
        a.arrowButton {
            background-image: url(leftarrows.png); float: left;
            margin-top: 15px; display: block; width: 50px; height: 50px;
        }
        #right {background-image: url(rightarrows.png)}
        h1 { min-width: 0px; width: 95%; }
        #oblock { float: left; display: inline; border: thin black solid; }
        form { margin-left: auto; margin-right: auto; width: 885px; }
        #bbox {clear: left}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {

            var fNames = ["Carnation", "Lily", "Orchid"];
            var fRow = $('<div id=row3 class=drow/>').appendTo('div.dtable'),
            var fTemplate = $('<div class=dcell><img/><label/><input/></div>'),
            for (var i = 0; i < fNames.length; i++) {
                fTemplate.clone().appendTo(fRow).children()
                    .filter('img').attr('src', fNames[i] + ".png").end()
                    .filter('label').attr('for', fNames[i]).text(fNames[i]).end()
                    .filter('input').attr({name: fNames[i],
                                          value: 0, required: "required"})
            }

            $('<a id=left></a><a id=right></a>').prependTo('form')
                .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
            $('#right').appendTo('form'),

            $('#row2, #row3').hide();

            var total = $('#buttonDiv')
                .prepend("<div>Total Items: <span id=total>0</span></div>")
                .css({clear: "both", padding: "5px"});
            $('<div id=bbox />').appendTo("body").append(total);

            $('input').change(function(e) {
                var total = 0;
                $('input').each(function(index, elem) {
                    total += Number($(elem).val());
                });
                $('#total').text(total);
            });

            function handleArrowMouse(e) {
               var propValue = e.type == "mouseenter" ? "-50px 0px" : "0px 0px";
               $(this).css("background-position", propValue);
            }

            function handleArrowPress(e) {
                var elemSequence = ["row1", "row2", "row3"];

                var visibleRow = $('div.drow:visible'),
                var visibleRowIndex = jQuery.inArray(visibleRow.attr("id"),elemSequence);

                var targetRowIndex;

                if (e.target.id == "left") {
                    targetRowIndex = visibleRowIndex - 1;
                    if (targetRowIndex < 0) {targetRowIndex = elemSequence.length -1};
                } else {
                    targetRowIndex = (visibleRowIndex + 1) % elemSequence.length;
                }
                visibleRow.fadeOut("fast", function() {
                    $('#' + elemSequence[targetRowIndex]).fadeIn("fast")});
            }
        });

    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="oblock">
            <div class="dtable">
                <div id="row1" class="drow">
                    <div class="dcell">
                        <img src="astor.png"/><label for="astor">Astor:</label>
                        <input name="astor" value="0" />
                    </div>
                    <div class="dcell">
                        <img src="daffodil.png"/><label for="daffodil">Daffodil:</label>
                        <input name="daffodil" value="0"/>
                    </div>
                    <div class="dcell">
                        <img src="rose.png"/><label for="rose">Rose:</label>
                        <input name="rose" value="0" />
                    </div>
                </div>
                <div id="row2"class="drow">
                    <div class="dcell">
                        <img src="peony.png"/><label for="peony">Peony:</label>
                        <input name="peony" value="0" />
                    </div>
                    <div class="dcell">
                        <img src="primula.png"/><label for="primula">Primula:</label>
                        <input name="primula" value="0" />
                    </div>
                    <div class="dcell">
                        <img src="snowdrop.png"/><label for="snowdrop">Snowdrop:</label>
                        <input name="snowdrop" value="0" />
                    </div>
                </div>
            </div>
        </div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
</body>
</html>

This isn't quite the same as the document in Chapter 11. I have tidied up a lot of the CSS additions by adding a style element, rather than using the css method on individual selections. You can see how this document appears in Figure 16-1, and, of course, Chapter 11 breaks down the set of changes I applied to the document to get this far.

Image

Figure 16-1 The starting point for the example document in this chapter

Updating the Node.js Script

Before you get started with the jQuery features, you need to upgrade your server-side script. These additions are to enrich the data that is sent back when a form is submitted and to support a new validation feature. Listing 16-2 shows the new script.

Listing 16-2. The Revised Node.js Script

var http = require('http'),
var url = require('url'),
var querystring = require('querystring'),

http.createServer(function (req, res) {
    console.log("Request: " + req.method + " to " + req.url);

    if (req.method == 'OPTIONS') {
        res.writeHead(200, "OK", {
            "Access-Control-Allow-Headers": "Content-Type",
            "Access-Control-Allow-Methods": "*",
            "Access-Control-Allow-Origin": "*"
            });
        res.end();

    } else if (req.method == 'POST') {
        var dataObj = new Object();
        var contentType = req.headers["content-type"];
        var fullBody = '';

        if (contentType) {
            if (contentType.indexOf("application/x-www-form-urlencoded") > -1) {
                req.on('data', function(chunk) { fullBody += chunk.toString();});
                req.on('end', function() {
                    var dBody = querystring.parse(fullBody);
                    writeResponse(req, res, dBody,
                        url.parse(req.url, true).query["callback"])
                });
            } else {
                req.on('data', function(chunk) { fullBody += chunk.toString();});
                req.on('end', function() {
                    dataObj = JSON.parse(fullBody);
                    var dprops = new Object();
                    for (var i = 0; i < dataObj.length; i++) {
                        dprops[dataObj[i].name] = dataObj[i].value;
                    }
                    writeResponse(req, res, dprops);
                });
            }
        }
    } else if (req.method == "GET") {
        var data = url.parse(req.url, true).query;
        writeResponse(req, res, data, data["callback"])
    }

}).listen(80);
console.log("Ready on port 80");

var flowerData = {
    astor: { price: 2.99, stock: 10, plural: "Astors"},
    daffodil: {price: 1.99, stock: 10, plural: "Daffodils"},
    rose: {price: 4.99, stock: 2, plural: "Roses"},
    peony: {price: 1.50, stock: 3, plural: "Peonies"},
    primula: {price: 3.12, stock: 20, plural: "Primulas"},
    snowdrop: {price: 0.99, stock: 5, plural: "Snowdrops"},
    carnation: {price: 0.50, stock: 1, plural: "Carnations"},
    lily: {price: 1.20, stock: 2, plural: "Lillies"},
    orchid: {price: 10.99, stock: 5, plural: "Orchids"}
}

function writeResponse(req, res, data, jsonp) {
    var jsonData;
    if (req.url == "/stockcheck") {
        for (flower in data) {
            if (flowerData[flower].stock >= data[flower]) {
                jsonData = true;
            } else {
                jsonData = "We only have " + flowerData[flower].stock + " "
                    + flowerData[flower].plural + " in stock";
            }
            break;
        }
        jsonData = JSON.stringify(jsonData);
    } else {
        var totalCount = 0;
        var totalPrice = 0;
        for (item in data) {
            if(item != "_" && data[item] > 0) {
                var itemNum = Number(data[item])
                totalCount += itemNum;
                totalPrice += (itemNum * flowerData[item].price);
            } else {
                delete data[item];
            }
        }
        data.totalItems = totalCount;
        data.totalPrice = totalPrice.toFixed(2);

        jsonData = JSON.stringify(data);
        if (jsonp) {
            jsonData = jsonp + "(" + jsonData + ")";
        }
    }
    res.writeHead(200, "OK", {
        "Content-Type": jsonp ? "text/javascript" : "application/json",
        "Access-Control-Allow-Origin": "*"});
    res.write(jsonData);
    res.end();
}

The response to the browser now includes the total prices for the items selected using the form elements and submitted to the server, returning a JSON result like this:

{"astor":"1","daffodil":"2","rose":"4","totalItems":7,"totalPrice":"26.93"}

I saved this script to a file called formserver.js. The easiest way to get this script is to download the source code that accompanies this book and that is freely available from Apress.com. I run the script by entering the following at the command prompt:


node.exe formserver.js

Preparing for Ajax

To being with, I am going to add some basic elements and styles that I will use to display any Ajax request errors and set up the basic configuration that will apply to all of my Ajax requests. Listing 16-3 shows the changes to the document.

Listing 16-3. Setting Up the Support for Ajax Requests and Error Handling

...
<style type="text/css">
    a.arrowButton {
        background-image: url(leftarrows.png); float: left;
        margin-top: 15px; display: block; width: 50px; height: 50px;}
    #right {background-image: url(rightarrows.png)}
    h1 { min-width: 0px; width: 95%; }
    #oblock { float: left; display: inline; border: thin black solid; }
    form { margin-left: auto; margin-right: auto; width: 885px; }
    #bbox {clear: left}
    #error {color: red; border: medium solid red; padding: 4px; margin: auto;
        width: 300px; text-align: center; margin-bottom: 5px}
</style>
<script type="text/javascript">
    $(document).ready(function() {

        $.ajaxSetup({
            timeout: 5000,
            converters: {
                "text html": function(data) {
                    return $(data);
                }
            }
        })

        $(document).ajaxError(function(e, jqxhr, settings, errorMsg) {
            $('#error').remove();
            var msg = "An error occurred. Please try again"
            if (errorMsg == "timeout") {
                msg = "The request timed out. Please try again"
            } else if (jqxhr.status == 404) {
                    msg = "The file could not be found";
            }
            $('<div id=error/>').text(msg).insertAfter('h1'),
        }).ajaxSuccess(function() {
            $('#error').remove();
        })

        var fNames = ["Carnation", "Lily", "Orchid"];
        var fRow = $('<div id=row3 class=drow/>').appendTo('div.dtable'),
        var fTemplate = $('<div class=dcell><img/><label/><input/></div>'),
        for (var i = 0; i < fNames.length; i++) {
            fTemplate.clone().appendTo(fRow).children()
                .filter('img').attr('src', fNames[i] + ".png").end()
                .filter('label').attr('for', fNames[i]).text(fNames[i]).end()
                .filter('input').attr({name: fNames[i],
                                      value: 0, required: "required"})
        }

        $('<a id=left></a><a id=right></a>').prependTo('form')
            .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
        $('#right').appendTo('form'),

        $('#row2, #row3').hide();

        var total = $('#buttonDiv')
            .prepend("<div>Total Items: <span id=total>0</span></div>")
            .css({clear: "both", padding: "5px"});
        $('<div id=bbox />').appendTo("body").append(total);

        $('input').change(function(e) {
            var total = 0;
            $('input').each(function(index, elem) {
                total += Number($(elem).val());
            });
            $('#total').text(total);
        });

        function handleArrowMouse(e) {
           var propValue = e.type == "mouseenter" ? "-50px 0px" : "0px 0px";
           $(this).css("background-position", propValue);
        }

        function handleArrowPress(e) {
            var elemSequence = ["row1", "row2", "row3"];

            var visibleRow = $('div.drow:visible'),
            var visibleRowIndex = jQuery.inArray(visibleRow.attr("id"),elemSequence);

            var targetRowIndex;

            if (e.target.id == "left") {
                targetRowIndex = visibleRowIndex - 1;
                if (targetRowIndex < 0) {targetRowIndex = elemSequence.length -1};
            } else {
                targetRowIndex = (visibleRowIndex + 1) % elemSequence.length;
            }
            visibleRow.fadeOut("fast", function() {
                $('#' + elemSequence[targetRowIndex]).fadeIn("fast")});
        }
    });
</script>
...

I have used the global Ajax events to set up a simple display for errors. When an error occurs, new elements are added to the screen with a description of the problem. The error messages that I show are derived from the information I get from jQuery, but I have kept things simple. In a real web application, these messages should be more descriptive and provide suggestions for basic resolution if possible. I have used the global events so that I am able to use the success and error setting on individual requests without having to worry about concatenating arrays of functions. You can see an example of a simple error in Figure 16-2.

Image

Figure 16-2 Displaying an error message for Ajax

The error is displayed until a successful request is made or another error occurs, at which point the elements are removed from the document.

In addition to the events, I used the ajaxSetup method to define values for the timeout setting and to provide a converter for HTML fragments so that they are automatically processed by jQuery.

Sourcing the Product Information

The next change I am going to make is to remove the existing product elements and the loop that adds three additional flowers to the list, replacing them with a couple of Ajax calls and a data template. First, however, I have created a new file called additionalflowers.json, which is shown in Listing 16-4.

Listing 16-4. The Contents of the Additionalflowers.json File

[{"name":"Carnation","product":"carnation"},
 {"name":"Lily","product":"lily"},
 {"name":"Orchid","product":"orchid"}]

This file contains a basic JSON description of the additional products I want to display. I am going to get the main set of products as an HTML fragment and then add to the set by processing the JSON data. Listing 16-5 shows the changes.

Listing 16-5. Setting Up the Products via HTML and JSON Obtained via Ajax

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-1.7.js" type="text/javascript"></script>
    <script src="jquery.tmpl.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <style type="text/css">
        a.arrowButton {
            background-image: url(leftarrows.png); float: left;
            margin-top: 15px; display: block; width: 50px; height: 50px;}
        #right {background-image: url(rightarrows.png)}
        h1 { min-width: 0px; width: 95%; }
        #oblock { float: left; display: inline; border: thin black solid; }
        form { margin-left: auto; margin-right: auto; width: 885px; }
        #bbox {clear: left}
        #error {color: red; border: medium solid red; padding: 4px; margin: auto;
            width: 300px; text-align: center; margin-bottom: 5px}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {

            $.ajaxSetup({
                timeout: 5000,
                converters: {
                    "text html": function(data) { return $(data); }
                }
            })

            $(document).ajaxError(function(e, jqxhr, settings, errorMsg) {
                $('#error').remove();
                var msg = "An error occurred. Please try again"
                if (errorMsg == "timeout") {
                    msg = "The request timed out. Please try again"
                } else if (jqxhr.status == 404) {
                        msg = "The file could not be found";
                }
                $('<div id=error/>').text(msg).insertAfter('h1'),
            }).ajaxSuccess(function() {
                $('#error').remove();
            })

            $('#row2, #row3').hide();

            $.get("flowers.html", function(data) {
                var elems = data.filter('div').addClass("dcell");
                elems.slice(0, 3).appendTo('#row1'),
                elems.slice(3).appendTo("#row2");
            })

            $.getJSON("additionalflowers.json", function(data) {
                $('#flowerTmpl').tmpl(data).appendTo("#row3");
            })

            $('<a id=left></a><a id=right></a>').prependTo('form')
                .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
            $('#right').appendTo('form'),

            var total = $('#buttonDiv')
                .prepend("<div>Total Items: <span id=total>0</span></div>")
                .css({clear: "both", padding: "5px"});
            $('<div id=bbox />').appendTo("body").append(total);

            $('input').change(function(e) {
                var total = 0;
                $('input').each(function(index, elem) {
                    total += Number($(elem).val());
                });
                $('#total').text(total);
            });

            function handleArrowMouse(e) {
               var propValue = e.type == "mouseenter" ? "-50px 0px" : "0px 0px";
               $(this).css("background-position", propValue);
            }

            function handleArrowPress(e) {
                var elemSequence = ["row1", "row2", "row3"];

                var visibleRow = $('div.drow:visible'),
                var visibleRowIndex = jQuery.inArray(visibleRow.attr("id"),elemSequence);

                var targetRowIndex;

                if (e.target.id == "left") {
                    targetRowIndex = visibleRowIndex - 1;
                    if (targetRowIndex < 0) {targetRowIndex = elemSequence.length -1};
                } else {
                    targetRowIndex = (visibleRowIndex + 1) % elemSequence.length;
                }
                visibleRow.fadeOut("fast", function() {
                    $('#' + elemSequence[targetRowIndex]).fadeIn("fast")});
            }
        });
    </script>
    <script id="flowerTmpl" type="text/x-jquery-tmpl">
        <div class="dcell">
            <img src="${product}.png"/>
            <label for="${product}">${name}:</label>
            <input name="${product}" value="0" />
        </div>
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="oblock">
            <div class="dtable">
                <div id="row1" class="drow"></div>
                <div id="row2" class="drow"></div>
                <div id="row3" class="drow"></div>
            </div>
        </div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
</body>
</html>

I have used the Ajax shorthand methods to get the HTML fragment and the JSON data I need to create the rows. It may not be obvious from the script, but one of the nice things about the shorthand method is that they are just wrappers around calls to the low-level API, and this means that the settings you apply via the ajaxSetup method apply just as they do when you use the ajax method directly.

In addition to the calls to the get and getJSON methods, I have added a simple data template so I can process the JSON simply and easily. There is no change to the appearance of the document, but the source of the content has changed.

Adding Form Validation

The next stage is to add some validation to your input elements. Listing 16-6 shows the additions that are required.

Listing 16-6. Adding Form Validation

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-1.7.js" type="text/javascript"></script>
    <script src="jquery.tmpl.js" type="text/javascript"></script>
    <script src="jquery.validate.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <style type="text/css">
        a.arrowButton {
            background-image: url(leftarrows.png); float: left;
            margin-top: 15px; display: block; width: 50px; height: 50px;}
        #right {background-image: url(rightarrows.png)}
        h1 { min-width: 0px; width: 95%; }
        #oblock { float: left; display: inline; border: thin black solid; }
        form { margin-left: auto; margin-right: auto; width: 885px; }
        #bbox {clear: left}
        #error {color: red; border: medium solid red; padding: 4px; margin: auto;
            width: 300px; text-align: center; margin-bottom: 5px}
        .invalidElem {border: medium solid red}
        #errorSummary {border: thick solid red; color: red; width: 350px; margin: auto;
            padding: 4px; margin-bottom: 5px}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {

            $.ajaxSetup({
                timeout: 5000,
                converters: {
                    "text html": function(data) { return $(data); }
                }
            })

            $(document).ajaxError(function(e, jqxhr, settings, errorMsg) {
                $('#error').remove();
                var msg = "An error occurred. Please try again"
                if (errorMsg == "timeout") {
                    msg = "The request timed out. Please try again"
                } else if (jqxhr.status == 404) {
                        msg = "The file could not be found";
                }
                $('<div id=error/>').text(msg).insertAfter('h1'),
            }).ajaxSuccess(function() {
                $('#error').remove();
            })

            $('#row2, #row3').hide();

            var flowerReq = $.get("flowers.html", function(data) {
                var elems = data.filter('div').addClass("dcell");
                elems.slice(0, 3).appendTo('#row1'),
                elems.slice(3).appendTo("#row2");
            })

            var jsonReq = $.getJSON("additionalflowers.json", function(data) {
                $('#flowerTmpl').tmpl(data).appendTo("#row3");
            })

            $('<div id=errorSummary>Please correct the following errors:</div>')
                .append('<ul id="errorsList"></ul>').hide().insertAfter('h1'),

            $('form').validate({
                highlight: function(element, errorClass) {
                    $(element).addClass("invalidElem");
                },
                unhighlight: function(element, errorClass) {
                    $(element).removeClass("invalidElem");
                },
                errorContainer: '#errorSummary',
                errorLabelContainer: '#errorsList',
                wrapper: 'li',
                errorElement: "div"
            });

            var plurals = {
                astor: "Astors", daffodil: "Daffodils", rose: "Roses",
                peony: "Peonies", primula: "Primulas", snowdrop: "Snowdrops",
                carnation: "Carnations", lily: "Lillies", orchid: "Orchids"
            }

            $.when(flowerReq, jsonReq).then(function() {
                $('input').each(function(index, elem) {
                    $(elem).rules("add", {
                        required: true,
                        min: 0,
                        digits: true,

                        messages: {
                          required: "Please enter a number for " + plurals[elem.name],
                          digits: "Please enter a number for " + plurals[elem.name],
                          min: "Please enter a positive number for " + plurals[elem.name]
                        }
                    })
                }).change(function(e) {
                    if ($('form').validate().element($(e.target))) {
                        var total = 0;
                        $('input').each(function(index, elem) {
                            total += Number($(elem).val());
                        });
                        $('#total').text(total);
                    }
                });
            });

            $('<a id=left></a><a id=right></a>').prependTo('form')
                .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
            $('#right').appendTo('form'),

            var total = $('#buttonDiv')
                .prepend("<div>Total Items: <span id=total>0</span></div>")
                .css({clear: "both", padding: "5px"});
            $('<div id=bbox />').appendTo("body").append(total);

            function handleArrowMouse(e) {
               var propValue = e.type == "mouseenter" ? "-50px 0px" : "0px 0px";
               $(this).css("background-position", propValue);
            }

            function handleArrowPress(e) {
                var elemSequence = ["row1", "row2", "row3"];

                var visibleRow = $('div.drow:visible'),
                var visibleRowIndex = jQuery.inArray(visibleRow.attr("id"),elemSequence);

                var targetRowIndex;

                if (e.target.id == "left") {
                    targetRowIndex = visibleRowIndex - 1;
                    if (targetRowIndex < 0) {targetRowIndex = elemSequence.length -1};
                } else {
                    targetRowIndex = (visibleRowIndex + 1) % elemSequence.length;
                }
                visibleRow.fadeOut("fast", function() {
                    $('#' + elemSequence[targetRowIndex]).fadeIn("fast")});
            }
        });
    </script>
    <script id="flowerTmpl" type="text/x-jquery-tmpl">
        <div class="dcell">
            <img src="${product}.png"/>
            <label for="${product}">${name}:</label>
            <input name="${product}" value="0" />
        </div>
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <form method="post" action="http://node.jacquisflowershop.com/order">
        <div id="oblock">
            <div class="dtable">
                <div id="row1" class="drow"></div>
                <div id="row2" class="drow"></div>
                <div id="row3" class="drow"></div>
            </div>
        </div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
</body>
</html>

In this listing, I have imported the JavaScript library for the validation plugin and defined some basic styles that will be used to display validation errors. I then call the validate method on the form element to set up form validation, specifying a single validation summary. This approach is straight out of Chapter 13.

Now, the use of Ajax to generate elements for the flower products gives me a problem. These are, of course, asynchronous calls, so I can't make assumptions about the presence of the input elements in the document in the statements that follow the Ajax calls. This is the common pitfall I described in Chapter 14, and if the browser executes my selection of the input elements before both Ajax requests are complete, then I won't match any elements (because they have yet to be created and added to the document), and my validation setup will fail. To get around this, I have used the when and then methods, which are part of the jQuery deferred objects feature that I describe in Chapter 35. Here are the relevant statements:

...
$.when(flowerReq, jsonReq).then(function() {
    $('input').each(function(index, elem) {
        $(elem).rules("add", {
            required: true,
            min: 0,
            digits: true,
            messages: {
              required: "Please enter a number for " + plurals[elem.name],
              digits: "Please enter a number for " + plurals[elem.name],
              min: "Please enter a positive number for " + plurals[elem.name]
            }
        })
    }).change(function(e) {
        if ($('form').validate().element($(e.target))) {
            var total = 0;
            $('input').each(function(index, elem) {
                total += Number($(elem).val());
            });
            $('#total').text(total);
        }
    });
});
...

I don't want to get ahead of myself, but the jqXHR objects that are returned by all of the Ajax methods can be passed as arguments to the when method, and if both requests are successful, then the function passed to the then method will be executed.

I set up my form validation in the function I pass to the then method, selecting the input elements and adding the validation rules I require to each of them. I have specified that values are required, that they must be digits, and that the minimum acceptable value is zero. I have defined custom messages for each check, and these refer to an array of plural flower names to help them make sense to the user.

Since I have the input elements selected, I take the opportunity to provide a handler function for the change event, which is triggered when the value entered into the field changes. Notice that I call the element method, like this:

...
if ($('form').validate().element($(e.target))) {
...

This triggers validation on the changed element, and the result from the method is a Boolean indicating the validity of the entered value. By using this in an if block, I can avoid adding invalid values to my running total of selected items.

Adding Remote Validation

The validation I performed in the previous example and that I described in Chapter 13 are examples of local validation, which is to say that the rules and the data required to enforce them are available within the document.

The validation plugin also supports remote validation, where the value entered by the user is sent to the server and the rules are applied there. This is useful when you don't want to send the data to the browser because there is too much of it, because doing so would be insecure, or because you want to perform validation against the latest data.

Image Caution Some caution is required when using remote validation because the load it can place on a server is significant. In this example, I perform a remote validation every time the user changes the value of an input element, but in a real application this is likely to generate a lot of requests. A more sensible approach is usually to perform remote validation only as a precursor to submitting the form.

I didn't explain remote validation in Chapter 13 because it relies on JSON and Ajax, and I didn't want to get into those topics too early. Listing 16-7 shows the addition of remote validation to the example document, where I use it to ensure that the user is unable to order more items than the server records as being in stock.

Listing 16-7. Performing Remote Validation

...
$.when(flowerReq, jsonReq).then(function() {
    $('input').each(function(index, elem) {
        $(elem).rules("add", {
            required: true,
            min: 0,
            digits: true,
            remote: {
                url: "http://node.jacquisflowershop.com/stockcheck",
                type: "post",
                global: false
            },
            messages: {
              required: "Please enter a number for " + plurals[elem.name],
              digits: "Please enter a number for " + plurals[elem.name],
              min: "Please enter a positive number for " + plurals[elem.name]
            }
        })
    }).change(function(e) {
        if ($('form').validate().element($(e.target))) {
            var total = 0;
            $('input').each(function(index, elem) {
                total += Number($(elem).val());
            });
            $('#total').text(total);
        }
    });
});
...

Setting remote validation is easy now that you have seen the jQuery support for Ajax. We specify the check as remote and set it to be a standard Ajax settings object. In this example, I have used the url setting to specify the URL will be called to perform the remote validation, the type setting to specify that I want a POST request, and the global setting to disable global events.

I have disabled global events because I don't want errors making the validation request to be treated as general errors that the user can do something about. Instead, I want them to fail quietly, on the basis that the server will perform further validation when the form is submitted (the Node.js script doesn't perform any validation, but it is important that real web applications do, as I mentioned in Chapter 13).

The validation plugin uses your Ajax setting to make a request to the specified URL, sending the name of the input element and the value that the user has entered. If the response from the server is the word true, then the value is valid. Any other response is considered to be an error message that will be displayed to the user. You can see how these messages are used in Figure 16-3.

Image

Figure 16-3 Displaying remote validation messages

Submitting the Form Data Using Ajax

Submitting the values in the form is exceptionally simple, and Listing 16-8 shows the same technique that I used in Chapter 15.

Listing 16-8. Submitting the Form Using Ajax

...
<style type="text/css">
    a.arrowButton {
        background-image: url(leftarrows.png); float: left;
        margin-top: 15px; display: block; width: 50px; height: 50px;}
    #right {background-image: url(rightarrows.png)}
    h1 { min-width: 0px; width: 95%; }
    #oblock { float: left; display: inline; border: thin black solid; }
    form { margin-left: auto; margin-right: auto; width: 885px; }
    #bbox {clear: left}
    #error {color: red; border: medium solid red; padding: 4px; margin: auto;
        width: 300px; text-align: center; margin-bottom: 5px}
    .invalidElem {border: medium solid red}
    #errorSummary {border: thick solid red; color: red; width: 350px; margin: auto;
        padding: 4px; margin-bottom: 5px}
    #popup {
        text-align: center; position: absolute; top: 100px;
        left: 0px; width: 100%; height: 1px; overflow: visible; visibility: visible;
        display: block }
    #popupContent { color: white; background-color: black; font-size: 14px ;
        font-weight: bold; margin-left: -75px; position: absolute; top: -55px;
        left: 50%; width: 150px; height: 60px; padding-top: 10px; z-index: 2;
    }
</style>
<script type="text/javascript">
    $(document).ready(function() {

        $('<div id="popup"><div id="popupContent"><img src="progress.gif"'
            + 'alt="progress"/><div>Placing Order</div></div></div>')
            .appendTo('body'),

        $.ajaxSetup({
            timeout: 5000,
            converters: {
                "text html": function(data) { return $(data); }
            }
        })

        $(document).ajaxError(function(e, jqxhr, settings, errorMsg) {
            $('#error').remove();
            var msg = "An error occurred. Please try again"
            if (errorMsg == "timeout") {
                msg = "The request timed out. Please try again"
            } else if (jqxhr.status == 404) {
                    msg = "The file could not be found";
            }
            $('<div id=error/>').text(msg).insertAfter('h1'),
        }).ajaxSuccess(function() {
            $('#error').remove();
        })

        $('#row2, #row3, #popup').hide();

        var flowerReq = $.get("flowers.html", function(data) {
            var elems = data.filter('div').addClass("dcell");
            elems.slice(0, 3).appendTo('#row1'),
            elems.slice(3).appendTo("#row2");
        })

        var jsonReq = $.getJSON("additionalflowers.json", function(data) {
            $('#flowerTmpl').tmpl(data).appendTo("#row3");
        })

        var plurals = {
            astor: "Astors", daffodil: "Daffodils", rose: "Roses",
            peony: "Peonies", primula: "Primulas", snowdrop: "Snowdrops",
            carnation: "Carnations", lily: "Lillies", orchid: "Orchids"
        }

        $('<div id=errorSummary>Please correct the following errors:</div>')
            .append('<ul id="errorsList"></ul>').hide().insertAfter('h1'),

        $('form').validate({
            highlight: function(element, errorClass) {
                $(element).addClass("invalidElem");
            },
            unhighlight: function(element, errorClass) {
                $(element).removeClass("invalidElem");
            },
            errorContainer: '#errorSummary',
            errorLabelContainer: '#errorsList',
            wrapper: 'li',
            errorElement: "div"
        });

        $.when(flowerReq, jsonReq).then(function() {
            $('input').each(function(index, elem) {
                $(elem).rules("add", {
                    required: true,
                    min: 0,
                    digits: true,
                    remote: {
                        url: "http://node.jacquisflowershop.com/stockcheck",
                        type: "post",
                        global: false
                    },
                    messages: {
                      required: "Please enter a number for " + plurals[elem.name],
                      digits: "Please enter a number for " + plurals[elem.name],
                      min: "Please enter a positive number for " + plurals[elem.name]
                    }
                })
            }).change(function(e) {
                if ($('form').validate().element($(e.target))) {
                    var total = 0;
                    $('input').each(function(index, elem) {
                        total += Number($(elem).val());
                    });
                    $('#total').text(total);
                }
            });
        });

        $('button').click(function(e) {
            e.preventDefault();

            var formData = $('form').serialize();
            $('body *').not('#popup, #popup *').css("opacity", 0.5);
            $('input').attr("disabled", "disabled");
            $('#popup').show();
            $.ajax({
                url: "http://node.jacquisflowershop.com/order",
                type: "post",
                data: formData,
                complete: function() {
                    setTimeout(function() {
                    $('body *').not('#popup, #popup *').css("opacity", 1);
                    $('input').removeAttr("disabled");
                    $('#popup').hide();
                    }, 1500);
                }
            })
        })

        $('<a id=left></a><a id=right></a>').prependTo('form')
            .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
        $('#right').appendTo('form'),

        var total = $('#buttonDiv')
            .prepend("<div>Total Items: <span id=total>0</span></div>")
            .css({clear: "both", padding: "5px"});
        $('<div id=bbox />').appendTo("body").append(total);

        function handleArrowMouse(e) {
           var propValue = e.type == "mouseenter" ? "-50px 0px" : "0px 0px";
           $(this).css("background-position", propValue);
        }

        function handleArrowPress(e) {
            var elemSequence = ["row1", "row2", "row3"];
            var visibleRow = $('div.drow:visible'),
            var visibleRowIndex = jQuery.inArray(visibleRow.attr("id"),elemSequence);
            var targetRowIndex;
            if (e.target.id == "left") {
                targetRowIndex = visibleRowIndex - 1;
                if (targetRowIndex < 0) {targetRowIndex = elemSequence.length -1};
            } else {
                targetRowIndex = (visibleRowIndex + 1) % elemSequence.length;
            }
            visibleRow.fadeOut("fast", function() {
                $('#' + elemSequence[targetRowIndex]).fadeIn("fast")});
        }
    });
</script>
...

I've gone beyond just making an Ajax POST request because I want to provide some additional context for how these requests can be handled in real projects. To start with, I have added an element that is positioned above all of the other elements in the document and tells the user that their order is being placed. Here are the CSS and jQuery statements that create this effect:

...
#popup {
    text-align: center; position: absolute; top: 100px;
    left: 0px; width: 100%; height: 1px; overflow: visible; visibility: visible;
    display: block }
#popupContent { color: white; background-color: black; font-size: 14px ;
    font-weight: bold; margin-left: -75px; position: absolute; top: -55px;
    left: 50%; width: 150px; height: 60px; padding-top: 10px; z-index: 2;
}
...
$('<div id="popup"><div id="popupContent"><img src="progress.gif"'
    + 'alt="progress"/><div>Placing Order</div></div></div>')
    .appendTo('body'),
...

It is surprisingly hard to create an element that looks like a pop-up and that is properly positioned on the screen, and you can see that the amount of CSS required to make it work is significant. The HTML elements are surprisingly simple by comparison, and the HTML that is generated looks like this when properly formatted:

<div id="popup">
    <div id="popupContent">
        <img src="progress.gif" alt="progress">
        <div>Placing Order</div>
    </div>
</div>

The img element I have specified (progress.gif) is an animated GIF image. There are a number of web sites that will generate progress images to your specification, and I used one of them. If you don't want to create your own, then use the one from this example that is included in the source code download for this book (available without charge at Apress.com). You can see how these elements appear in Figure 16-4, where I have removed the other elements for clarity.

Image

Figure 16-4 Showing progress to the user

I hide these elements initially because it makes no sense to show the user a progress display until they actually place the order:

$('#row2, #row3, #popup').hide();

With these elements in place and hidden, you can turn to the form submission. I register a handler function for the click event for the button element, as follows:

...
$('button').click(function(e) {
    e.preventDefault();

    var formData = $('form').serialize();
    $('body *').not('#popup, #popup *').css("opacity", 0.5);
    $('input').attr("disabled", "disabled");
    $('#popup').show();
    $.ajax({
        url: "http://node.jacquisflowershop.com/order",
        type: "post",
        data: formData,
        complete: function() {
            setTimeout(function() {
                $('body *').not('#popup, #popup *').css("opacity", 1);
                $('input').removeAttr("disabled");
                $('#popup').hide();
            }, 1500);
        }
    })
})
...

Before starting the Ajax request, I show the pop-up elements and make all of the other elements partially transparent. I also disable the input elements by adding the disabled attribute. I do this because I don't want the user to be able to change the value of any of the input elements while I am sending the data to the user:

...
$('body *').not('#popup, #popup *').css("opacity", 0.5);
$('input').attr("disabled", "disabled");
$('#popup').show();
...

The problem with disabling the input elements is that their values won't be included in the data sent to the server. The serialize method will include values only from input elements that are successful controls, as defined by the HTML specification; this excludes those elements that are disabled or don't have a name attribute. I could iterate through the input elements myself and get the values anyway, but it is simpler to gather the data to send before disabling the elements, like this:

    var formData = $('form').serialize();

I have used the complete setting to restore the interface to its normal state by making all of the elements opaque, removing the disabled attribute from the input elements and hiding the pop-up elements. I have introduced an artificial 1.5-second delay after the request has completed before restoring the interface, like this:

...
complete: function() {
    setTimeout(function() {
        $('body *').not('#popup, #popup *').css("opacity", 1);
        $('input').removeAttr("disabled");
        $('#popup').hide();
    }, 1500);
}
...

I would not do this in a real web application, but for demonstration purposes when the development machine and the server are on the same LAN, this is useful to emphasize the transition. You can see how the browser appears during the Ajax request in Figure 16-5.

Image

Figure 16-5 The browser during the form submission request

Processing the Server Response

All that remains is to do something useful with the data that you get back from the server. For this chapter, I am going to use a simple table. You will learn about creating rich user interfaces with jQuery UI in the next part of this book, and I don't want to have to do by hand what I can do much more elegantly with the UI widgets. You can see the finished result in Figure 16-6.

Image

Figure 16-6 Displaying the order summary

Listing 16-9 shows the complete document that supports this enhancement.

Listing 16-9. Processing the Response from the Server

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-1.7.js" type="text/javascript"></script>
    <script src="jquery.tmpl.js" type="text/javascript"></script>
    <script src="jquery.validate.js" type="text/javascript"></script>
    <link rel="stylesheet" type="text/css" href="styles.css"/>
    <style type="text/css">
        a.arrowButton {
            background-image: url(leftarrows.png); float: left;
            margin-top: 15px; display: block; width: 50px; height: 50px;}
        #right {background-image: url(rightarrows.png)}
        h1 { min-width: 0px; width: 95%; }
        #oblock { float: left; display: inline; border: thin black solid; }
        #orderForm { margin-left: auto; margin-right: auto; width: 885px; }
        #bbox {clear: left}
        #error {color: red; border: medium solid red; padding: 4px; margin: auto;
            width: 300px; text-align: center; margin-bottom: 5px}
        .invalidElem {border: medium solid red}
        #errorSummary {border: thick solid red; color: red; width: 350px; margin: auto;
            padding: 4px; margin-bottom: 5px}
        #popup {
            text-align: center; position: absolute; top: 100px;
            left: 0px; width: 100%; height: 1px; overflow: visible; visibility: visible;
            display: block }
        #popupContent { color: white; background-color: black; font-size: 14px ;
            font-weight: bold; margin-left: -75px; position: absolute; top: -55px;
            left: 50%; width: 150px; height: 60px; padding-top: 10px; z-index: 2;
        }
        #summary {text-align: center}
        table {border-collapse: collapse; border: medium solid black; font-size: 18px;
            margin: auto; margin-bottom: 5px;}
        th {text-align: left}
        th, td {padding: 2px}
        tr > td:nth-child(1) {text-align: left}
        tr > td:nth-child(2) {text-align: right}
    </style>
    <script type="text/javascript">
        $(document).ready(function() {

            $('<div id="popup"><div id="popupContent"><img src="progress.gif"'
                + 'alt="progress"/><div>Placing Order</div></div></div>')
                .appendTo('body'),

            $.ajaxSetup({
                timeout: 5000,
                converters: {
                    "text html": function(data) { return $(data); }
                }
            })

            $(document).ajaxError(function(e, jqxhr, settings, errorMsg) {
                $('#error').remove();
                var msg = "An error occurred. Please try again"
                if (errorMsg == "timeout") {
                    msg = "The request timed out. Please try again"
                } else if (jqxhr.status == 404) {
                        msg = "The file could not be found";
                }
                $('<div id=error/>').text(msg).insertAfter('h1'),
            }).ajaxSuccess(function() {
                $('#error').remove();
            })

            $('#row2, #row3, #popup, #summaryForm').hide();

            var flowerReq = $.get("flowers.html", function(data) {
                var elems = data.filter('div').addClass("dcell");
                elems.slice(0, 3).appendTo('#row1'),
                elems.slice(3).appendTo("#row2");
            })

            var jsonReq = $.getJSON("additionalflowers.json", function(data) {
                $('#flowerTmpl').tmpl(data).appendTo("#row3");
            })

            var plurals = {
                astor: "Astors", daffodil: "Daffodils", rose: "Roses",
                peony: "Peonies", primula: "Primulas", snowdrop: "Snowdrops",
                carnation: "Carnations", lily: "Lillies", orchid: "Orchids"
            }

            $('<div id=errorSummary>Please correct the following errors:</div>')
                .append('<ul id="errorsList"></ul>').hide().insertAfter('h1'),

            $('#orderForm').validate({
                highlight: function(element, errorClass) {
                    $(element).addClass("invalidElem");
                },
                unhighlight: function(element, errorClass) {
                    $(element).removeClass("invalidElem");
                },
                errorContainer: '#errorSummary',
                errorLabelContainer: '#errorsList',
                wrapper: 'li',
                errorElement: "div"
            });

            $.when(flowerReq, jsonReq).then(function() {
                $('input').each(function(index, elem) {
                    $(elem).rules("add", {
                        required: true,
                        min: 0,
                        digits: true,
                        remote: {
                            url: "http://node.jacquisflowershop.com/stockcheck",
                            type: "post",
                            global: false
                        },
                        messages: {
                          required: "Please enter a number for " + plurals[elem.name],
                          digits: "Please enter a number for " + plurals[elem.name],
                          min: "Please enter a positive number for " + plurals[elem.name]
                        }
                    })
                }).change(function(e) {
                    if ($('#orderForm').validate().element($(e.target))) {
                        var total = 0;
                        $('input').each(function(index, elem) {
                            total += Number($(elem).val());
                        });
                        $('#total').text(total);
                    }
                });
            });

            $('#orderForm button').click(function(e) {
                e.preventDefault();

                var formData = $('#orderForm').serialize();
                $('body *').not('#popup, #popup *').css("opacity", 0.5);
                $('input').attr("disabled", "disabled");
                $('#popup').show();
                $.ajax({
                    url: "http://node.jacquisflowershop.com/order",
                    type: "post",
                    data: formData,
                    dataType: "json",
                    dataFilter: function(data, dataType) {
                        data = $.parseJSON(data);

                        var cleanData = {
                            totalItems: data.totalItems,
                            totalPrice: data.totalPrice
                        };
                        delete data.totalPrice; delete data.totalItems;
                        cleanData.products = [];
                        for (prop in data) {
                            cleanData.products.push({
                                name: plurals[prop],
                                quantity: data[prop]
                            })
                        }
                        return cleanData;
                    },
                    converters: {"text json": function(data) { return data;}},
                    success: function(data) {
                        processServerResponse(data);
                    },
                    complete: function() {
                        $('body *').not('#popup, #popup *').css("opacity", 1);
                        $('input').removeAttr("disabled");
                        $('#popup').hide();
                    }
                })
            })

            function processServerResponse(data) {
                if (data.products.length > 0) {
                    $('body > *:not(h1)').hide();
                    $('#summaryForm').show();
                    $('#productRowTmpl').tmpl(data.products).appendTo('tbody'),
                    $('#totalitems').text(data.totalItems);
                    $('#totalprice').text(data.totalPrice);
                } else {
                    var elem = $('input').get(0);
                        var err = new Object();
                        err[elem.name] = "No products selected";
                    $('#orderForm').validate().showErrors(err);
                    $(elem).removeClass("invalidElem");
                }
            }

            $('<a id=left></a><a id=right></a>').prependTo('#orderForm')
                .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
            $('#right').appendTo('#orderForm'),

            var total = $('#buttonDiv')
                .prepend("<div>Total Items: <span id=total>0</span></div>")
                .css({clear: "both", padding: "5px"});
            $('<div id=bbox />').appendTo("body").append(total);

            function handleArrowMouse(e) {
               var propValue = e.type == "mouseenter" ? "-50px 0px" : "0px 0px";
               $(this).css("background-position", propValue);
            }

            function handleArrowPress(e) {
                var elemSequence = ["row1", "row2", "row3"];
                var visibleRow = $('div.drow:visible'),
                var visibleRowIndex = jQuery.inArray(visibleRow.attr("id"),elemSequence);
                var targetRowIndex;
                if (e.target.id == "left") {
                    targetRowIndex = visibleRowIndex - 1;
                    if (targetRowIndex < 0) {targetRowIndex = elemSequence.length -1};
                } else {
                    targetRowIndex = (visibleRowIndex + 1) % elemSequence.length;
                }
                visibleRow.fadeOut("fast", function() {
                    $('#' + elemSequence[targetRowIndex]).fadeIn("fast")});
            }
        });
    </script>
    <script id="flowerTmpl" type="text/x-jquery-tmpl">
        <div class="dcell">
            <img src="${product}.png"/>
            <label for="${product}">${name}:</label>
            <input name="${product}" value="0" />
        </div>
    </script>
      <script id="productRowTmpl" type="text/x-jquery-tmpl">
        <tr><td>${name}</td><td>${quantity}</td></tr>
    </script>
</head>
<body>
    <h1>Jacqui's Flower Shop</h1>
    <form id="orderForm" method="post" action="http://node.jacquisflowershop.com/order">
        <div id="oblock">
            <div class="dtable">
                <div id="row1" class="drow"></div>
                <div id="row2" class="drow"></div>
                <div id="row3" class="drow"></div>
            </div>
        </div>
        <div id="buttonDiv"><button type="submit">Place Order</button></div>
    </form>
    <form id="summaryForm" method="post" action="">
        <div id="summary">
            <h3>Order Summary</h3>
            <table border="1">
                <thead>
                    <tr><th>Product</th><th>Quantity</th>
                </thead>
                <tbody>
                </tbody>
                <tfoot>
                    <tr><th>Number of Items:</th><td id="totalitems"></td></tr>
                    <tr><th>Total Price:</th><td id="totalprice"></td></tr>
                </tfoot>
            </table>
            <div id="buttonDiv2"><button type="submit">Complete Order</button></div>
        </div>
    </form>
</body>
</html>

I'll break down the changes I made step by step.

Adding the New Form

The first thing that I did was add a new form to the static HTML part of the document, like this:

...
<form id="summaryForm" method="post" action="">
    <div id="summary">
        <h3>Order Summary</h3>
        <table border="1">
            <thead>
                <tr><th>Product</th><th>Quantity</th>
            </thead>
            <tbody>
            </tbody>
            <tfoot>
                <tr><th>Number of Items:</th><td id="totalitems"></td></tr>
                <tr><th>Total Price:</th><td id="totalprice"></td></tr>
            </tfoot>
        </table>
        <div id="buttonDiv2"><button type="submit">Complete Order</button></div>
    </div>
</form>
...

This is the heart of the new functionality. When the user submits their product selection to the server, the table in this form will be used to display the data you get back from the Ajax request.

ImageTip I had been using the $('form') selector in previous examples, but since they are two forms in the document now, I have gone through and switched these references to the use the form element's id attribute values.

I don't want to display the new form immediately, so I added it to the list of elements that I hide in the script, like this:

$('#row2, #row3, #popup, #summaryForm').hide();

And, as you might expect by now, when there are new elements, there is new CSS to style them, as follows:

...
#summary {text-align: center}
table {border-collapse: collapse; border: medium solid black; font-size: 18px;
    margin: auto; margin-bottom: 5px;}
th {text-align: left}
th, td {padding: 2px}
tr > td:nth-child(1) {text-align: left}
tr > td:nth-child(2) {text-align: right}
...

These styles ensure that the table is displayed in the middle of the browser window and the text in various columns is aligned to the correct edge.

Completing the Ajax Request

The next step was to complete the call to the ajax request, like this:

...
$('#orderForm button').click(function(e) {
    e.preventDefault();

    var formData = $('#orderForm').serialize();
    $('body *').not('#popup, #popup *').css("opacity", 0.5);
    $('input').attr("disabled", "disabled");
    $('#popup').show();
    $.ajax({
        url: "http://node.jacquisflowershop.com/order",
        type: "post",
        data: formData,
        dataType: "json",
        dataFilter: function(data, dataType) {
            data = $.parseJSON(data);

            var cleanData = {
                totalItems: data.totalItems,
                totalPrice: data.totalPrice
            };
            delete data.totalPrice; delete data.totalItems;
            cleanData.products = [];
            for (prop in data) {
                cleanData.products.push({
                    name: plurals[prop],
                    quantity: data[prop]
                })
            }
            return cleanData;
        },
        converters: {"text json": function(data) { return data;}},
        success: function(data) {
            processServerResponse(data);
        },
        complete: function() {
            $('body *').not('#popup, #popup *').css("opacity", 1);
            $('input').removeAttr("disabled");
            $('#popup').hide();
        }
    })
})
...

I removed the explicit delay in the complete function and added the dataFilter, converters, and success settings to the request.

I use the dataFilters setting to provide a function that transforms the JSON data I get from the server into something more useful. The server sends me a JSON string like this:

{"astor":"4","daffodil":"1","snowdrop":"2","totalItems":7,"totalPrice":"15.93"}

I parse the JSON data and restructure it so that I get this:

{"totalItems":7,
 "totalPrice":"15.93",
 "products":[{"name":"Astors","quantity":"4"},
             {"name":"Daffodils","quantity":"1"},
             {"name":"Snowdrops","quantity":"2"}]
}

This format has two advantages. The first is that it is better suited for use with data templates because I can pass the products property to the tmpl method. The second is that I can check whether the user has selected any elements with products.length. These are two quite minor advantages, but I wanted to integrate as many of the features from the earlier chapters as possible. Notice that I have also replaced the name of the product (orchid, for example) with the plural name (Orchids).

Having already parsed the JSON data into a JavaScript object (using the parseJSON method, which I describe in Chapter 33), I want to disable the built-in converter, which will try to do the same thing. To that end, I have defined a custom converter for JSON, which just passes the data through without modification:

...
converters: {"text json": function(data) { return data;}}
...

Processing the Data

For the success setting in the ajax method call, I specified the processServerResponse function, which I defined as follows:

...
function processServerResponse(data) {
    if (data.products.length > 0) {
        $('body > *:not(h1)').hide();
        $('#summaryForm').show();
        $('#productRowTmpl').tmpl(data.products).appendTo('tbody'),
        $('#totalitems').text(data.totalItems);
        $('#totalprice').text(data.totalPrice);
    } else {
        var elem = $('input').get(0);
            var err = new Object();
            err[elem.name] = "No products selected";
        $('#orderForm').validate().showErrors(err);
        $(elem).removeClass("invalidElem");
    }
}
...

If the data from the server contains product information, then I hide all of the elements in the document that I don't want (including the original form element and the additions I made in the script) and show the new form. I populate the table using the following data template:

<script id="productRowTmpl" type="text/x-jquery-tmpl">
  <tr><td>${name}</td><td>${quantity}</td></tr>
</script>

This is a very simple template and produces a table row for each selected product. Finally, I set contents of the cells that display the total price and item count using the text method:

$('#totalitems').text(data.totalItems);
$('#totalprice').text(data.totalPrice);

However, if the data from the server doesn't contain any product information (which indicates that the user has left all of the input element values as zero), then I do something very different. First I select the first of the input elements, like this:

var elem = $('input').get(0);

I then create an object that contains a property whose name is the name value of the input element and whose value is a message to the user. I then call the validate method on the form element and the showErrors method on the result, like this:

var err = new Object();
err[elem.name] = "No products selected";
$('#orderForm').validate().showErrors(err);

This allows me to manually inject an error into the validation system and take advantage of all of the structure and formatting that I put in place earlier. I have to provide the name of an element so that the validation plugin can highlight where the error occurs, which is not ideal, as you can see in Figure 16-7.

Image

Figure 16-7 Display the selection error

I am displaying a general message, but the highlighting is applied to just one input element. To deal with this, I remove the class that the validation plugin uses for highlighting, like this:

$(elem).removeClass("invalidElem");

This produces the effect shown in Figure 16-8.

Image

Figure 16-8 Removing the highlighting from the element associated with the error

Summary

In this chapter, I refactored the example to bring together the themes and features covered in this part of the book. I used Ajax widely (using both the shorthand and low-level methods), applied a pair of data templates, and used the validation plugin to check values locally and remotely (and to display an error manually).

In the next part of the book, you will turn your attention to jQuery UI, and the next time that you refactor the example document, it will have a very different appearance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.42.128