CHAPTER 16

image

Refactoring the Example: Part II

I introduced some rich features in this part of the book, and as I did in before in Chapter 11, I want to bring them together to give a broader view of jQuery.

image Tip  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, I used the core jQuery features to refactor the example to include DOM (Domain Object Model) manipulation, effects, and events. Listing 16-1 shows the document I ended up with, which will be the starting point for this chapter as I integrate the features from this part of the book.

Listing 16-1.  The Starting Point for This Chapter

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.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="aster.png"/><label for="aster">Aster:</label>
                        <input name="aster" 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>

image Tip  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 I can focus on adding the new features.

This isn’t exactly the same document: I have tidied the Cascading Style Sheet (CSS) additions by adding a style element rather than using the css method on individual selections. You can see how this HTML document appears in the browser in Figure 16-1, and, of course, Chapter 11 breaks down the set of changes I applied to the document to get this far.

9781430263883_Fig16-01.jpg

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

Updating the Node.js Script

I need to upgrade the formserver.js server-side script for this chapter. The changes, which are shown in Listing 16-2, are to enrich the data that is sent back when a form is submitted and to support a new validation feature. As with all of the examples in this book, you can download the revised formserver.js file from the Source Code/Download area of the Apress web site (www.apress.com).

Listing 16-2.  The Revised Node.js Script

var http = require("http");
var querystring = require("querystring");
var url = require("url");
  
var port = 80;
  
http.createServer(function (req, res) {
    console.log("[200 OK] " + 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": "http://www.jacquisflowershop.com"
        });
        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"])
    }
  
    var flowerData = {
        aster: { price: 2.99, stock: 10, plural: "Asters" },
        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();
    }
  
  
}).listen(port);
console.log("Ready on port " + port);

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 the following:

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

I run the script by entering the following at the command prompt:

node.exe formserver.js

Preparing for Ajax

To begin, I am going to add some basic elements and styles that I will use to display 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).css("clear: left");
  
        $("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 created to provide a description of the problem. The simple error messages that I show are derived from the information I get from jQuery, but in a real web application, these messages should be more descriptive and provide suggestions for resolution. You can see an example of an error being shown to the user in Figure 16-2.

9781430263883_Fig16-02.jpg

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 global 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 is to remove the existing product elements and the loop that adds three additional flowers to the list, replacing them with a pair of Ajax calls and a data template. First, however, I created a file called additionalflowers.json and placed it in the same directory as the other example files. Listing 16-4 shows the contents of the additionalflowers.json file.

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 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-2.0.2.js" type="text/javascript"></script>
    <script src="handlebars.js" type="text/javascript"></script>
    <script src="handlebars-jquery.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 id="flowerTmpl" type="text/x-handlebars-template">
        {{#flowers}}
        <div class="dcell">
            <img src="{{product}}.png"/>
            <label for="{{product}}">{{name}}:</label>
            <input name="{{product}}" value="0" />
        </div>
        {{/flowers}}
    </script>
    <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();
            });
  
            $("<a id=left></a><a id=right></a>").prependTo("form")
                .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
            $("#right").appendTo("form");
  
            $("#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").template({ flowers: data }).appendTo("#row3");
            })
  
            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).css("clear: left");
  
            $("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>
                <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 methods 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 work 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 data template so I can process the JSON 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 validation to the 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-2.0.2.js" type="text/javascript"></script>
    <script src="handlebars.js" type="text/javascript"></script>
    <script src="handlebars-jquery.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 id="flowerTmpl" type="text/x-handlebars-template">
        {{#flowers}}
        <div class="dcell">
            <img src="{{product}}.png"/>
            <label for="{{product}}">{{name}}:</label>
            <input name="{{product}}" value="0" />
        </div>
        {{/flowers}}
    </script>
    <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();
            });
  
            $("<a id=left></a><a id=right></a>").prependTo("form")
                .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
            $("#right").appendTo("form");
  
            $("#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").template({ flowers: data }).appendTo("#row3");
            });
  
            $("<div id=errorSummary>").text("Please correct the following errors:")
                .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 = {
                aster: "Asters", 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 of " + plurals[elem.name],
                            digits: "Please enter a number of" + plurals[elem.name],
                            min: "Please enter a positivenumber of "
                                + 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);
                    }
                });
            });
     
            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).css("clear: left");
  
            $("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>
                <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 imported the validation plug-in and defined some CSS that will be used to style validation errors. I call the validate method on the form element to set up form validation, specifying a single validation summary, just as I did in Chapter 13.

The fact that I am using Ajax to generate elements for the flower products gives me a problem to solve. 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, 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 solve this problem, I have used the when and then methods, which are part of the jQuery deferred objects feature that I describe in Chapter 35. Following 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 of " + plurals[elem.name],
                digits: "Please enter a number of" + plurals[elem.name],
                min: "Please enter a positive number of "
                    + 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, 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 validation 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, as follows:

...
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 an if block, I avoid adding invalid values to my running total of selected items.

Adding Remote Validation

The validation I performed in the Listing 16-6 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 part of the HTML document.

The validation plug-in 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 validation rules to the browser because it would require too much data (e.g., you might validate a user name for a new account by checking that it has not already been used—something that would require all account names to be sent to the client for local validation).

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 and this is likely to generate a lot of requests in a real application. 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 how I set up remote validation, which I use to ensure that the user cannot order more items than are 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 of " + plurals[elem.name],
                digits: "Please enter a number of" + plurals[elem.name],
                min: "Please enter a positive number of "
                    + 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: I specify the validation check by setting the remote property to a map object that configures the Ajax request the validation plug-in will make to the user. In this example, I have used the url setting to specify the URL that 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 that arise when making the remote validation Ajax 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 formserver.js script doesn’t perform any validation, but it is important that real web applications do, as I explained in Chapter 13).

The validation plug-in uses the standard jQuery Ajax settings to make a request to the specified remote validation URL, sending the name of the input element and the value that the user has entered. If I enter 22 into the Aster input element and then navigate away to trigger the change event, the validation plug-in will make an HTTP POST request to the server that contains the following information:

aster=22

The response that the server sends is simple. If the response 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. My formserver.js script will send back an error message such as the following:

We only have 10 Asters in stock

This message is treated just like a local validation message, as illustrated in Figure 16-3.

9781430263883_Fig16-03.jpg

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 technique I used in Chapter 15.

Listing 16-8.  Submitting the Form Using Ajax

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="handlebars.js" type="text/javascript"></script>
    <script src="handlebars-jquery.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}
        #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 id="flowerTmpl" type="text/x-handlebars-template">
        {{#flowers}}
        <div class="dcell">
            <img src="{{product}}.png"/>
            <label for="{{product}}">{{name}}:</label>
            <input name="{{product}}" value="0" />
        </div>
        {{/flowers}}
    </script>
    <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();
            });
  
            $("<a id=left></a><a id=right></a>").prependTo("form")
                .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
            $("#right").appendTo("form");
  
            $("#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").template({ flowers: data }).appendTo("#row3");
            });
  
            $("<div id=errorSummary>").text("Please correct the following errors:")
                .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 = {
                aster: "Asters", 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,
                        remote: {
                            url: "http://node.jacquisflowershop.com/stockcheck",
                            type: "post",
                            global: false
                        },
                        messages: {
                            required: "Please enter a number of " + plurals[elem.name],
                            digits: "Please enter a number of" + plurals[elem.name],
                            min: "Please enter a positive number of "
                                + 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);
                    }
                })
            });
  
            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).css("clear: left");
  
            $("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>
                <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 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 her order is being placed. Following is the statement that creates these elements:

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

I also added some CSS to the style element for these new elements.

...
#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; }
...

It is 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 substantial. The HTML elements themselves are simple by comparison and the HTML that is generated from the jQuery statement looks like the following:

...
<div id="popup" style="display: none;">
    <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, which is included in the Source Code/Download area for this book (freely available from the Apress web site [www.apress.com]). You can see how the progress elements appear in Figure 16-4 (I have removed the other elements for clarity).

9781430263883_Fig16-04.jpg

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 he actually places the order.

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

With these elements in place and hidden, I 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 disable the input elements by adding the disabled attribute 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 considered 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, as follows:

...
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, as follows:

...
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 local network, this is useful to emphasize the transition. You can see how the browser appears during the Ajax request in Figure 16-5.

9781430263883_Fig16-05.jpg

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 I 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. Figure 16-6 shows the finished result.

9781430263883_Fig16-06.jpg

Figure 16-6. Displaying the order summary

Listing 16-9 shows the changes to the HTML document that support this enhancement. I’ll break down the changes I made step by step in the sections that follow.

Listing 16-9.  Processing the Response from the Server

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <script src="jquery-2.0.2.js" type="text/javascript"></script>
    <script src="handlebars.js" type="text/javascript"></script>
    <script src="handlebars-jquery.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 id="flowerTmpl" type="text/x-handlebars-template">
        {{#flowers}}
        <div class="dcell">
            <img src="{{product}}.png"/>
            <label for="{{product}}">{{name}}:</label>
            <input name="{{product}}" value="0" />
        </div>
        {{/flowers}}
    </script>
    <script id="productRowTmpl" type="text/x-handlebars-template">
        {{#rows}}
            <tr><td>{{name}}</td><td>{{quantity}}</td></tr>
        {{/rows}}
    </script>
    <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();
            });
  
            $("<a id=left></a><a id=right></a>").prependTo("#orderForm")
                .addClass("arrowButton").click(handleArrowPress).hover(handleArrowMouse);
            $("#right").appendTo("#orderForm");
  
            $("#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").template({ flowers: data }).appendTo("#row3");
            });
  
            $("<div id=errorSummary>").text("Please correct the following errors:")
                .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"
            });
              
            var plurals = {
                aster: "Asters", 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,
                        remote: {
                            url: "http://node.jacquisflowershop.com/stockcheck",
                            type: "post",
                            global: false
                        },
                        messages: {
                            required: "Please enter a number of " + plurals[elem.name],
                            digits: "Please enter a number of" + plurals[elem.name],
                            min: "Please enter a positive number of "
                                + 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")
                        .template({ rows: 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");
                }
            }
  
            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).css("clear: left");
  
            $("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 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>

Adding the New Form

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

...
<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 his product selection to the server, the table in this form will be used to display the data I get back from the Ajax request.

image Tip  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 elements’ 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, as follows:

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

And, as you might expect by now, where 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 the table columns is aligned to the correct edge.

Completing the Ajax Request

The next step is to complete the call to the ajax request.

...
$("#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 from the server into something more useful. The server sends me a JSON string like the following:

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

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

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

This format gives me two advantages. The first is that it is better suited for use with data templates because I can pass the products property to the template 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 (ochids).

Having already parsed the JSON data into a JavaScript object (using the parseJSON method, which I describe in Chapter 34), 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 ajax method success callback, 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").template({ rows: 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 contain 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-handlebars-template">
    {{#rows}}
        <tr><td>{{name}}</td><td>{{quantity}}</td></tr>
    {{/rows}}
</script>
...

This template 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 don’t contain any product information (which indicates that the user has left all of the input element values as zero), then I do something different. First I select the first of the input elements, as follows:

...
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, as follows:

...
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 the structure and formatting that I put in place earlier. I have to provide the name of an element so that the validation plug-in can highlight where the error occurs, which is not ideal, as you can see in Figure 16-7.

9781430263883_Fig16-07.jpg

Figure 16-7. Display the selection error

I am displaying a general message, but the highlighting is applied to just one input element, which will be confusing to the user. To deal with this, I remove the class that the validation plug-in uses for highlighting, as follows:

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

This produces the effect shown in Figure 16-8.

9781430263883_Fig16-08.jpg

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 plug-in to check values locally and remotely (and to display an error manually). In the next part of the book, I turn to jQuery UI and the next time that I 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
3.137.212.212