CHAPTER 33

image

Refactoring the Example: Part IV

In prior chapters in this part of the book, I introduced you to jQuery Mobile. In this chapter, I’ll build a more complete example that uses the jQuery Mobile functionality. By its nature, jQuery Mobile is a lot simpler than jQuery UI, and there are a lot fewer design choices available. Your development efforts with jQuery Mobile are further constrained by the unique issues that face mobile device development.

Starting with the Basics

In Chapter 32, I showed you an example that used split lists. This example is the starting point for this chapter and I'll use it to build out some additional functionality. Listing 33-1 shows the initial example document for this chapter.

Listing 33-1.  The Starting Point for This Chapter

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <style type="text/css">
        .lcontainer {float: left; text-align: center; padding-top: 10px}
        .productData {float: right; padding: 10px; width: 60%}
        .cWrapper {text-align: center; margin: 20px}
    </style>
</head>
<body>
    <div id="page1" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div id="container" style="padding: 20px">
            <ul data-role="listview" data-inset=true>
                <li><a href="#basket" class="buy" id="rose">Roses</a>
                    <a href="#roses">Roses</a></li>
                <li><a href="#basket" class="buy" id="orchid">Orchids</a>
                    <a href="#orchids">Orchids</a>  </li>
                <li><a href="#basket" class="buy" id="aster">Asters</a>
                    <a href="#asters">Asters</a>  </li>
            </ul>
        </div>
    </div>
    <div id="basket" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div class="cWrapper">
            Basket will go here
        </div>
        <div class="cWrapper">
            <a href="#" data-rel="back" data-role="button" data-inline=true
               data-direction="reverse">Back</a>
        </div>
    </div>
    <div id="roses" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Roses</h1>
        </div>
        <div>
            <div class="lcontainer">
                <img src="rose.png">
                <div><a href="#" data-rel="back" data-role="button"
                       data-inline=true data-direction="reverse">Back</a>
                </div>
            </div>
            <div class="productData">
                A rose is a woody perennial within the family Rosaceae.
                They form a group of erect shrubs, and climbing or trailing plants.
                <div><b>Price: $4.99</b></div>
            </div>
        </div>
    </div>
  
   <div id="orchids" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Orchids</h1>
        </div>
        <div>
            <div class="lcontainer">
                <img src="orchid.png">
                <div><a href="#" data-rel="back" data-role="button"
                       data-inline=true data-direction="reverse">Back</a>
                </div>
            </div>
            <div class="productData">
                The orchid family is a diverse and widespread family in the order
                Asparagales. It is one of the largest families of flowering plants.
                <div><b>Price: $10.99</b></div>
            </div>
        </div>
    </div>
   <div id="asters" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Asters</h1>
        </div>
        <div>
            <div class="lcontainer">
                <img src="aster.png">
                <div><a href="#" data-rel="back" data-role="button"
                       data-inline=true data-direction="reverse">Back</a>
                </div>
            </div>
            <div class="productData">
                The name Aster comes from the Ancient Greek word meaning "star",
                referring to the shape of the flower head.
                <div><b>Price: $2.99</b></div>
            </div>
        </div>
    </div>
</body>
</html>

Inserting Products Programmatically

The first thing I’ll do is replace the static pages that describe each flower with some that are created dynamically. This change allows me to have a more compact document and to easily add more flowers for the user to pick from without duplicating HTML elements. I will generate the pages using a data template, which I described in Chapter 12. Data templates work with the core jQuery library and so fit nicely into a jQuery Mobile application as well. I created a file called data.json that contains the data I need for the flowers. Listing 33-2 shows the contents of data.json.

Listing 33-2.  The Content of the data.json file

[{  "name": "aster",
    "label": "Asters",
    "price": "$2.99",
    "text": "The name Aster comes from the Ancient Greek word meaning star..."
},{ "name": "carnation",
    "label": "Carnations",
    "price": "$1.99",
    "text": "Carnations require well-drained, neutral to slightly alkaline soil..."
},{ "name": "daffodil",
    "label": "Daffodils",
    "price": "$1.99",
    "text": "Daffodil is a common English name, sometimes used for all varieties..."
},{ "name": "rose",
    "label": "Roses",
    "price": "$4.99",
    "text":  "A rose is a woody perennial within the family Rosaceae. They form a..."
},{ "name": "orchid",
    "label": "Orchids",
    "price": "$10.99",
    "text": "The orchid family is a diverse and widespread family in the order..."
}]

The data describes five flowers. For each of them, I defined the product name, the label to display to the user, the price per unit, and a text description.

image Note  I did not show the full text description in the listing, but it is included in the data.json file that is part of the source code download for this book (which you can get from Apress.com).

Now that I have the data, I can integrate it into the document. Listing 33-3 shows the changes from static to programmatically generated pages using a data template.

Listing 33-3.  Adding Pages Dynamically

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <style type="text/css">
        .lcontainer {float: left; text-align: center; padding-top: 10px}
        .productData {float: right; padding: 10px; width: 60%}
        .cWrapper {text-align: center}
    </style>
    <script id="flowerTmpl" type="text/x-handlebars-template">
        {{#products}}
       <div id="{{name}}" data-role="page" data-theme="b">
            <div data-role="header">
               <h1>{{label}}</h1>
            </div>
            <div>
                <div class="lcontainer">
                    <img src="{{name}}.png">
                    <div><a href="#" data-rel="back" data-role="button"
                           data-inline=true data-direction="reverse">Back</a>
                    </div>
                </div>
                <div class="productData">
                    {{text}}
                    <div><b>Price: {{price}}</b></div>
                </div>
            </div>
        </div>
        {{/products}}
    </script>
  
    <script id="liTmpl" type="text/x-handlebars-template">
        {{#products}}
        <li>
            <a href="#basket" class="buy" id="A1">{{label}}</a>
            <a href="#{{name}}">{{label}}</a>
        </li>
        {{/products}}
    </script>
  
    <script type="text/javascript">
        var initComplete = false;
        $(document).bind("pageinit", function () {
            if (!initComplete) {
                $.getJSON("data.json", function (data) {
                    $("#flowerTmpl").template({ products: data })
                        .filter("*").appendTo("body");
                    $("ul").append($("#liTmpl").template({ products: data })
                        .filter("*")).listview("refresh")
                });
                initComplete = true;
            }
        })
    </script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
</head>
<body>
    <div id="page1" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div id="container" style="padding: 20px">
            <ul data-role="listview" data-inset=true></ul>
        </div>
    </div>
    <div id="basket" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div class="cWrapper">
            Basket will go here
        </div>
        <div class="cWrapper">
            <a href="#" data-rel="back" data-role="button" data-inline=true
               data-direction="reverse">Back</a>
        </div>
    </div>
</body>
</html>

I removed the per-flower pages and used data templates to generate what I need from the data, which I obtain using the getJSON method (described in Chapter 14). The key to this change is the simple custom JavaScript code, as follows:

..
<script type="text/javascript">
    var initComplete = false;
    $(document).bind("pageinit", function () {
        if (!initComplete) {
            $.getJSON("data.json", function (data) {
                $("#flowerTmpl").template({ products: data })
                    .filter("*").appendTo("body");
                $("ul").append($("#liTmpl").template({ products: data })
                    .filter("*")).listview("refresh")
            });
            initComplete = true;
        }
    })
</script>
...

When I obtain the data, I use a template to generate elements form the data and add the dynamically generated pages to the body element in the document. I also use a template to generate the items for the main list of flowers. I tell jQuery Mobile that I have modified the contents of the list, which I do by calling the refresh method on the listview widget, like this:

...
$("ul").append($("#liTmpl").template({ products: data })
    .filter("*")).listview("refresh");
...

The data templates are simple and use the standard techniques I described in Chapter 12. You can see the result in Figure 33-1—a list whose items are generated programmatically and link to pages that have been added to the document programmatically.

9781430263883_Fig33-01.jpg

Figure 33-1. Programmatically generated list items and pages

Reusing Pages

I like the data template approach because it shows how jQuery underpins such a wide range of functionality, allowing you to bring together features like templates with an interface toolkit like jQuery Mobile.

That said, you can adopt a more elegant approach for dealing with the per-flower pages. Rather than generate a set of elements for each flower you want to show, you can generate one set of elements and modify them to show the flower that the user has selected. Listing 33-4 shows the changes to the document that make this possible.

Listing 33-4.  Reusing a Single Page for Multiple Products

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <script id="liTmpl" type="text/x-handlebars-template">
        {{#products}}
        <li>
            <a href="#basket" class="buy" id="{{name}}">{{label}}</a>
            <a class="productLink" data-flower="{{name}}" href="#">{{label}}</a>
        </li>
        {{/products}}
    </script>
    <script type="text/javascript">
        var initComplete = false;
  
        $(document).bind("pageinit", function () {
  
            if (!initComplete) {
  
                $.getJSON("data.json", function (data) {
                    $("ul").append($("#liTmpl").template({ products: data })
                        .filter("*")).listview("refresh");
  
                    $("a.productLink").bind("tap", function () {
                        var targetFlower = $(this).attr("data-flower");
                        for (var i = 0; i < data.length; i++) {
                            if (data[i].name == targetFlower) {
                                var page = $("#productPage");
                                page.find("#header").text(data[i].label);
                                page.find("#image").attr("src", data[i].name + ".png");
                                page.find("#description").text(data[i].text);
                                page.find("#price").text(data[i].price);
  
                                $.mobile.changePage("#productPage");
                                break;
                            }
                        }
                    })
                });
                initComplete = true;
            }
        })
    </script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <style type="text/css">
        .lcontainer {float: left; text-align: center; padding-top: 10px}
        .productData {float: right; padding: 10px; width: 60%}
        .cWrapper {text-align: center}
    </style>
</head>
<body>
    <div id="page1" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div id="container" style="padding: 20px">
            <ul data-role="listview" data-inset=true>
  
            </ul>
        </div>
    </div>
    <div id="productPage" data-role="page" data-theme="b">
         <div data-role="header">
            <h1 id="header"></h1>
         </div>
         <div>
             <div class="lcontainer">
                 <img id="image" src="">
                 <div><a href="#" data-rel="back" data-role="button"
                        data-inline=true data-direction="reverse">Back</a>
                 </div>
             </div>
             <div class="productData">
                 <span id="description"></span>
                 <div><b>Price: <span id="price"></span></b></div>
             </div>
         </div>
     </div>
    <div id="basket" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div class="cWrapper">
            Basket will go here
        </div>
        <div class="cWrapper">
            <a href="#" data-rel="back" data-role="button" data-inline=true
               data-direction="reverse">Back</a>
        </div>
    </div>
</body>
</html>

image Tip  This is an approach that is particularly suited to jQuery Mobile because multiple pages are contained within a single HTML document. As a rule, you want to keep your HTML documents as simple as possible because of the limitations inherent in mobile devices.

I removed one of the data templates from the document and added a new page (whose id is productPage) that I use for each flower. I modified the template used to generate the list items so that there is no target page in the href attribute and to add my own data attribute so that I know which flower any given link relates to. When the data has been retrieved from JSON, the revised script selects all of the per-product links from the list elements I just created using the template and binds to the tap event. When a list item is tapped, I find the appropriate data item and use its properties to configure the productPage page, setting the text and image to display to the user, as follows:

...
<script type="text/javascript">
  
    var initComplete = false;
  
    $(document).bind("pageinit", function () {
        if (!initComplete) {
            $.getJSON("data.json", function (data) {
                $("ul").append($("#liTmpl").template({ products: data })
                    .filter("*")).listview("refresh");
  
                $("a.productLink").bind("tap", function () {
                    var targetFlower = $(this).attr("data-flower");
                    for (var i = 0; i < data.length; i++) {
                        if (data[i].name == targetFlower) {
                            var page = $("#productPage");
                            page.find("#header").text(data[i].label);
                            page.find("#image").attr("src", data[i].name + ".png");
                            page.find("#description").text(data[i].text);
                            page.find("#price").text(data[i].price);
  
                            $.mobile.changePage("#productPage");
                            break;
                        }
                    }
                })
            });
            initComplete = true;
        }
    })
</script>
...

After I configure the page, I use the changePage method to trigger the navigation. There is no change in the appearance of the example, but you have a smaller set of elements for the mobile browser to manage, and it’s a nice example of how you can manipulate the page structure of a jQuery Mobile document.

Creating the Shopping Basket

I am using a split list in this example, and the left side of the list item leads to the basket page. In this section, I’ll define the elements for the page and add some JavaScript so that there is a simple basket in place. Listing 33-5 shows the changes to the document.

Listing 33-5.  Implementing the Shopping Basket

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <script id="liTmpl" type="text/x-handlebars-template">
        {{#products}}
        <li>
            <a href="#" class="buy" id="{{name}}">{{label}}</a>
            <a class="productLink" data-flower="{{name}}" href="#">{{label}}</a>
        </li>
        {{/products}}
    </script>
    <script id="trTmpl" type="text/x-handlebars-template">
        <tr data-price="{{price}}" id="{{name}}"><td>{{label}}</td><td id="count">1</td>
            <td id="subtotal">0</td></tr>
    </script>
    <script type="text/javascript">
        $(document).ready(function () {
            $.getJSON("data.json", function (data) {
                $("ul").append($("#liTmpl").template({ products: data }))
                    .filter("*").listview("refresh");
  
                $("a.productLink").bind("tap", function () {
                    var targetFlower = $(this).attr("data-flower");
                    for (var i = 0; i < data.length; i++) {
                        if (data[i].name == targetFlower) {
                            var page = $("#productPage");
                            page.find("#header").text(data[i].label);
                            page.find("#image").attr("src", data[i].name + ".png");
                            page.find("#description").text(data[i].text);
                            page.find("#price").text(data[i].price);
                            $.mobile.changePage("#productPage");
                            break;
                        }
                    }
                });
  
                $("a.buy").bind("tap", function () {
                    var targetFlower = this.id;
                    var row = $("#basketTable tbody #" + targetFlower);
                    if (row.length > 0) {
                        var countCell = row.find("#count");
                        countCell.text(Number(countCell.text()) + 1);
                    } else {
                        for (var i = 0; i < data.length; i++) {
                            if (data[i].name == targetFlower) {
                                $("#trTmpl").template(data[i])
                                    .appendTo("#basketTable tbody")
                                break;
                            }
                        }
                    }
                    calculateTotals();
                    $.mobile.changePage("#basket")
                });
            })
        })
  
        function calculateTotals() {
            var total = 0;
            $("#basketTable tbody").children().each(function (index, elem) {
                var count = Number($(elem).find("#count").text())
                var price = Number($(elem).attr("data-price").slice(1))
                var subtotal = count * price;
                $(elem).find("#subtotal").text("$" + subtotal.toFixed(2));
                total += subtotal;
            })
            $("#total").text("$" + total.toFixed(2))
        }
    </script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <style type="text/css">
        .lcontainer {float: left; text-align: center; padding-top: 10px}
        .productData {float: right; padding: 10px; width: 60%}
        .cWrapper {text-align: center}
        table {display: inline-block; margin: auto; margin-top: 20px; text-align: left;
            border-collapse: collapse}
        td {min-width: 100px}
        th, td {text-align: right}
        th:nth-child(1), td:nth-child(1) {text-align: left}
    </style>
</head>
<body>
    <div id="page1" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div id="container" style="padding: 20px">
            <ul data-role="listview" data-inset=true></ul>
        </div>
    </div>
    <div id="productPage" data-role="page" data-theme="b">
         <div data-role="header">
            <h1 id="header"></h1>
         </div>
         <div>
             <div class="lcontainer">
                 <img id="image" src="">
                 <div><a href="#" data-rel="back" data-role="button"
                        data-inline=true data-direction="reverse">Back</a>
                 </div>
             </div>
             <div class="productData">
                 <span id="description"></span>
                 <div><b>Price: <span id="price"></span></b></div>
             </div>
         </div>
     </div>
    <div id="basket" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div class="cWrapper">
            <table id="basketTable" border=0>
                <thead>
                    <tr><th>Flower</th><th>Quantity</th><th>Subtotal</th></tr>
                </thead>
                <tbody></tbody>
                <tfoot>
                    <tr><th colspan=2>Total:</th><td id="total"></td></tr>
                </tfoot>
            </table>
        </div>
        <div class="cWrapper">
            <a href="#" data-rel="back" data-role="button" data-inline=true
               data-direction="reverse">Back</a>
            <button data-inline="true">Checkout</button>
        </div>
    </div>
</body>
</html>

I added a table to the basket page, which shows one row for each selected product. Each row shows the name of the product, the quantity, and the subtotal. There is a footer in the table that shows the overall total. I bound to the tap event so that when the user clicks on the left side of the split button, either a new row is added to the table or the quantity is incremented if there is already a row for this product in the table. New rows are generated using another data template, and everything else is handled by reading the contents of elements in the document.

I determine and maintain the entire state of the customer’s basket using the DOM itself. I could have created a JavaScript object to model the order and driven the contents of the table from the object, but in a book about jQuery, I like to take every opportunity to work with the document itself. The result is a simple basket, which is shown in Figure 33-2.

9781430263883_Fig33-02.jpg

Figure 33-2. The basket page

Adding for Quantity Changes

The basket is functional, but if the user wants two roses, for example, she has to tap the Rose list item, tap the Back button, and then tap the Rose item again. This process is pretty ridiculous, so to make it easier to change the quantity of a product, I added some input elements to the table. You can see the changes in Listing 33-6.

Listing 33-6.  Adding Range Sliders to the Basket Table

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <script id="liTmpl" type="text/x-handlebars-template">
        {{#products}}
        <li>
            <a href="#" class="buy" id="{{name}}">{{label}}</a>
            <a class="productLink" data-flower="{{name}}" href="#">{{label}}</a>
        </li>
        {{/products}}
    </script>
    <script id="trTmpl" type="text/x-handlebars-template">
        <tr data-theme="b" data-price="{{price}}" id="{{name}}"><td>{{label}}</td>
            <td id="count"><input type=number value=1 min=0 max=10></td>
            <td id="subtotal">0</td>
        </tr>
    </script>
    <script type="text/javascript">
  
        var initComplete = false;
  
        $(document).bind("pageinit", function () {
            if (!initComplete) {
                $.getJSON("data.json", function (data) {
  
                    $("ul").append($("#liTmpl")
                        .template({ products: data })).listview("refresh");
  
                    $("a.productLink").bind("tap", function () {
                        var targetFlower = $(this).attr("data-flower");
                        for (var i = 0; i < data.length; i++) {
                            if (data[i].name == targetFlower) {
                                var page = $("#productPage");
                                page.find("#header").text(data[i].label);
                                page.find("#image").attr("src", data[i].name + ".png");
                                page.find("#description").text(data[i].text);
                                page.find("#price").text(data[i].price);
  
                                $.mobile.changePage("#productPage");
                                break;
                            }
                        }
                    })
  
                    $("a.buy").bind("tap", function () {
                        var targetFlower = this.id;
                        var row = $("#basketTable tbody #" + targetFlower);
                        if (row.length > 0) {
                            var countCell = row.find("#count input");
                            countCell.val(Number(countCell.val()) + 1);
                        } else {
                            for (var i = 0; i < data.length; i++) {
                                if (data[i].name == targetFlower) {
                                    $("#trTmpl").template(data[i])
                                        .appendTo("#basketTable tbody")
                                        .find("input").textinput()
  
                                    break;
                                }
                            }
                        }
                        calculateTotals();
                        $.mobile.changePage("#basket")
                    })
  
                    $(document).on("change click", "input", function (event) {
                        calculateTotals();
                    })
                });
                initComplete = true;
            }
        })
  
        function calculateTotals() {
            var total = 0;
            $("#basketTable tbody").children().each(function (index, elem) {
                var count = Number($(elem).find("#count input").val())
                var price = Number($(elem).attr("data-price").slice(1))
                var subtotal = count * price;
                $(elem).find("#subtotal").text("$" + subtotal.toFixed(2));
                total += subtotal;
            })
            $("#total").text("$" + total.toFixed(2))
        }
    </script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <style type="text/css">
        .lcontainer {float: left; text-align: center; padding-top: 10px}
        .productData {float: right; padding: 10px; width: 60%}
        .cWrapper {text-align: center}
        table {display: inline-block; margin: auto; margin-top: 20px; text-align: left;
            border-collapse: collapse}
        td {min-width: 100px; padding-bottom: 10px}
        td:nth-child(2) {min-width: 75px; width: 75px}
        th, td {text-align: right}
        th:nth-child(1), td:nth-child(1) {text-align: left}
        input[type=number] {background-color: white}
        tfoot tr {border-top: medium solid black}
        tfoot tr td {padding-top: 10px}
    </style>
</head>
<body>
    <div id="page1" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div id="container" style="padding: 20px">
            <ul data-role="listview" data-inset=true></ul>
        </div>
    </div>
    <div id="productPage" data-role="page" data-theme="b">
         <div data-role="header">
            <h1 id="header"></h1>
         </div>
         <div>
             <div class="lcontainer">
                 <img id="image" src="">
                 <div><a href="#" data-rel="back" data-role="button"
                        data-inline=true data-direction="reverse">Back</a>
                 </div>
             </div>
             <div class="productData">
                 <span id="description"></span>
                 <div><b>Price: <span id="price"></span></b></div>
             </div>
         </div>
     </div>
    <div id="basket" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div class="cWrapper">
            <table id="basketTable" border=0>
                <thead>
                    <tr><th>Flower</th><th>Quantity</th><th>Subtotal</th></tr>
                </thead>
                <tbody></tbody>
                <tfoot>
                    <tr><th colspan=2>Total:</th><td id="total"></td></tr>
                </tfoot>
            </table>
        </div>
        <div class="cWrapper">
            <a href="#" data-rel="back" data-role="button" data-inline=true
               data-direction="reverse">Back</a>
            <button data-inline="true">Checkout</button>
        </div>
    </div>
</body>
</html>

I inserted an input element into the quantity cell in the template, which is used to generate rows for the table. The type of this input element is number, which causes some browsers to insert small up and down buttons alongside the text entry area. These buttons are too small to be useful for touch, but the browser will also filter the characters to discard anything that isn’t appropriate for a number. Although it’s acceptable for this chapter, this isn’t a perfect approach for real projects because floating point numbers are supported, which means that the user can input fractions of products.

I call the textinput method when I add input elements to the document after the pages have been enhanced by jQuery Mobile:

...
$("#trTmpl").template(data[i]).appendTo("#basketTable tbody").find("input").textinput()
...

If I do not add this method call, the browser displays the native input element. Calling the textinput method causes jQuery Mobile to enhance the element, although it doesn’t properly assign the swatch. So I defined a style for the input element to set the background color consistently:

...
input[type=number] {background-color: white}
...

I need to calculate the subtotals and totals more frequently now that the user can change the quantity of a product in the basket page. Because I add input elements to the document throughout the life of the application, I use the jQuery on method to handle events. The on method is described in Chapter 9. Here is the event handler code:

...
$(document).on("change click", "input", function (event) {
    calculateTotals();
})
...

I use the on method to associate my handler function with both the change and click events. The browsers that add up and down buttons to numeric input elements trigger the click event when those buttons are pressed, so I need to handle this event in addition to the more expected change event. My handler function calls the calculateTotals function when either event is triggered. You can see how the basket looks in Figure 33-3.

9781430263883_Fig33-03.jpg

Figure 33-3. Adding input elements to the basket page

Adding a Button to the Information Page

The product information describes the flower the user has selected, but it doesn’t provide any way for the user to add it to the basket. To round out the basic basket functionality, I added a button to the product page that adds the item to the basket. Listing 33-7 shows the changes to the product page.

Listing 33-7.  Adding a Button to the Product Page

...
<div id="productPage" data-role="page" data-theme="b">
     <div data-role="header">
        <h1 id="header"></h1>
     </div>
     <div>
         <div class="lcontainer">
             <img id="image" src="">
             <div><a href="#" data-rel="back" data-role="button"
                    data-inline=true data-direction="reverse">Back</a>
             </div>
         </div>
         <div class="productData">
             <span id="description"></span>
             <div>
                <b>Price: <span id="price"></span></b>
                <a href="#" id="buybutton" data-flower="" data-role="button"
                   data-inline=true>Buy</a>
             </div>
         </div>
     </div>
 </div>
...

I defined an a element that jQuery Mobile will transform into a button widget. I added a data attribute (data-flower) so that I can keep track of which flower is being displayed when the user taps the button. To support this button, I made some changes to the script. These changes are shown in Listing 33-8.

Listing 33-8.  Adding Support for the Buy Button in the Script

...
<script type="text/javascript">
  
    var initComplete = false;
  
    $(document).bind("pageinit", function () {
        if (!initComplete) {
            $.getJSON("data.json", function (data) {
  
                $("ul").append($("#liTmpl")
                    .template({ products: data })).listview("refresh");
  
                $("a.productLink").bind("tap", function () {
                    var targetFlower = $(this).attr("data-flower");
                    for (var i = 0; i < data.length; i++) {
                        if (data[i].name == targetFlower) {
                            var page = $("#productPage");
                            page.find("#header").text(data[i].label);
                            page.find("#image").attr("src", data[i].name + ".png");
                            page.find("#description").text(data[i].text);
                            page.find("#price").text(data[i].price);
                            page.find("#buybutton").attr("data-flower", data[i].name);
                            $.mobile.changePage("#productPage");
                            break;
                        }
                    }
                })
  
                $("#buybutton").bind("tap", function () {
                    addProduct($(this).attr("data-flower"));
                })
  
                $("a.buy").bind("tap", function () {
                    addProduct(this.id);
                })
  
                function addProduct(targetFlower) {
                    var row = $("#basketTable tbody #" + targetFlower);
                    if (row.length > 0) {
                        var countCell = row.find("#count input");
                        countCell.val(Number(countCell.val()) + 1);
                    } else {
                        for (var i = 0; i < data.length; i++) {
                            if (data[i].name == targetFlower) {
                                $("#trTmpl").template(data[i])
                                    .appendTo("#basketTable tbody")
                                    .find("input").textinput()
  
                                break;
                            }
                        }
                    }
                    calculateTotals();
                    $.mobile.changePage("#basket")
                }
  
                $(document).on("change click", "input", function (event) {
                    calculateTotals();
                })
            });
            initComplete = true;
        }
    })
  
    function calculateTotals() {
        var total = 0;
        $("#basketTable tbody").children().each(function (index, elem) {
            var count = Number($(elem).find("#count input").val())
            var price = Number($(elem).attr("data-price").slice(1))
            var subtotal = count * price;
            $(elem).find("#subtotal").text("$" + subtotal.toFixed(2));
            total += subtotal;
        })
        $("#total").text("$" + total.toFixed(2))
    }
</script>
...

These changes are pretty straightforward. When the user selects a product from the main list, I set the value of the data-flower attribute on the a element. I register a function to handle the tap event for the button and use the value data-flower to call the addProduct function, which contains code I extracted from another handler function. With these changes, the user can add products to the basket from the main list (by tapping on the left side of the split list item) or from the information page (by tapping the Buy button). Figure 33-4 shows the addition of the Buy button to the page.

9781430263883_Fig33-04.jpg

Figure 33-4. Adding a button to the product information page

Implementing the Checkout Process

To round out this example, I’ll demonstrate gathering the data from the various jQuery Mobile pages in a form that can be used to make an Ajax request. I will not make the request itself or implement the server. jQuery Mobile uses the core jQuery support for Ajax, which I described in Chapters 14 and 15. Listing 33-9 shows the addition of a page that is shown to the user when the Checkout button is tapped and the handler function that gathers the data.

Listing 33-9.  Implementing the Checkout Process

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="jquery.mobile-1.3.1.css" type="text/css" />
    <script type="text/javascript" src="jquery-1.10.1.js"></script>
    <script src="handlebars.js"></script>
    <script src="handlebars-jquery.js"></script>
    <script id="liTmpl" type="text/x-handlebars-template">
        {{#products}}
        <li>
            <a href="#" class="buy" id="{{name}}">{{label}}</a>
            <a class="productLink" data-flower="{{name}}" href="#">{{label}}</a>
        </li>
        {{/products}}
    </script>
    <script id="trTmpl" type="text/x-handlebars-template">
        <tr data-theme="b" data-price="{{price}}" id="{{name}}"><td>{{label}}</td>
            <td id="count"><input type=number value=1 min=0 max=10></td>
            <td id="subtotal">0</td>
        </tr>
    </script>
    <script type="text/javascript">
        var initComplete = false;
        $(document).bind("pageinit", function () {
            if (!initComplete) {
                $.getJSON("data.json", function (data) {
                    $("ul").append($("#liTmpl")
                        .template({ products: data })).listview("refresh");
  
                    $("a.productLink").bind("tap", function () {
                        var targetFlower = $(this).attr("data-flower");
                        for (var i = 0; i < data.length; i++) {
                            if (data[i].name == targetFlower) {
                                var page = $("#productPage");
                                page.find("#header").text(data[i].label);
                                page.find("#image").attr("src", data[i].name + ".png");
                                page.find("#description").text(data[i].text);
                                page.find("#price").text(data[i].price);
                                page.find("#buybutton")
                                    .attr("data-flower", data[i].name);
                                $.mobile.changePage("#productPage");
                                break;
                            }
                        }
                    })
  
                    $("#buybutton").bind("tap", function () {
                        addProduct($(this).attr("data-flower"));
                    })
  
                    $("a.buy").bind("tap", function () {
                        addProduct(this.id);
                    })
  
                    function addProduct(targetFlower) {
                        var row = $("#basketTable tbody #" + targetFlower);
                        if (row.length > 0) {
                            var countCell = row.find("#count input");
                            countCell.val(Number(countCell.val()) + 1);
                        } else {
                            for (var i = 0; i < data.length; i++) {
                                if (data[i].name == targetFlower) {
                                    $("#trTmpl").template(data[i])
                                        .appendTo("#basketTable tbody")
                                        .find("input").textinput();
                                    break;
                                }
                            }
                        }
                        calculateTotals();
                        $.mobile.changePage("#basket")
                    }
  
                    $(document).on("change click", "input", function (event) {
                        calculateTotals();
                    })
  
                    $("#submit").bind("tap", function () {
                        var dataObject = new Object();
                        $("#basketTable tbody").children().each(function (index, elem) {
                            dataObject[elem.id] = $(elem).find("#count input").val();
                        })
                        dataObject["name"] = $("#name").val();
                        dataObject["wrap"] = $("option:selected").val();
                        dataObject["shipping"] = $("input:checked").attr("id")
  
                        console.log("DATA: " + JSON.stringify(dataObject))
                    })
                });
                initComplete = true;
            }
        })
  
        function calculateTotals() {
            var total = 0;
            $("#basketTable tbody").children().each(function (index, elem) {
                var count = Number($(elem).find("#count input").val())
                var price = Number($(elem).attr("data-price").slice(1))
                var subtotal = count * price;
                $(elem).find("#subtotal").text("$" + subtotal.toFixed(2));
                total += subtotal;
            })
            $("#total").text("$" + total.toFixed(2))
        }
  
    </script>
    <script type="text/javascript" src="jquery.mobile-1.3.1.js"></script>
    <style type="text/css">
        .lcontainer {float: left; text-align: center; padding: 10px}
        .productData {float: right; padding: 10px; width: 60%}
        .cWrapper {text-align: center}
        table {display: inline-block; margin: auto; margin-top: 20px; text-align: left;
            border-collapse: collapse}
        td {min-width: 100px; padding-bottom: 10px}
        td:nth-child(2) {min-width: 75px; width: 75px}
        th, td {text-align: right}
        th:nth-child(1), td:nth-child(1) {text-align: left}
        input[type=number] {background-color: white}
        tfoot tr {border-top: medium solid black}
        tfoot tr td {padding-top: 10px}
    </style>
</head>
<body>
    <div id="page1" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div id="container" style="padding: 20px">
            <ul data-role="listview" data-inset=true></ul>
        </div>
    </div>
    <div id="productPage" data-role="page" data-theme="b">
         <div data-role="header">
            <h1 id="header"></h1>
         </div>
         <div>
             <div class="lcontainer">
                 <img id="image" src="">
                 <div><a href="#" data-rel="back" data-role="button"
                        data-inline=true data-direction="reverse">Back</a>
                 </div>
             </div>
             <div class="productData">
                 <span id="description"></span>
                 <div>
                    <b>Price: <span id="price"></span></b>
                    <a href="#" id="buybutton" data-flower="" data-role="button"
                       data-inline=true>Buy</a>
                 </div>
             </div>
         </div>
     </div>
    <div id="basket" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div class="cWrapper">
            <table id="basketTable" border=0>
                <thead>
                    <tr><th>Flower</th><th>Quantity</th><th>Subtotal</th></tr>
                </thead>
                <tbody></tbody>
                <tfoot>
                    <tr><th colspan=2>Total:</th><td id="total"></td></tr>
                </tfoot>
            </table>
        </div>
        <div class="cWrapper">
            <a href="#" data-rel="back" data-role="button" data-inline=true
               data-direction="reverse">Back</a>
            <a href="#checkout" data-role="button" data-inline="true">Checkout</a>
        </div>
    </div>
   <div id="checkout" data-role="page" data-theme="b">
        <div data-role="header">
           <h1>Jacqui's Shop</h1>
        </div>
        <div data-role="content">
            <label for="name">Name: </label>
            <input id="name" placeholder="Your Name">
  
            <label for="wrap"><span>Gift Wrap: </span></label>
            <select id="wrap" name="wrap" data-role="slider">
                <option value="yes" selected>Yes</option>
                <option value="no">No</option>
            </select>
  
            <fieldset data-role="controlgroup" data-type="horizontal">
                <legend>Shipping:</legend>
                <input type="radio" name="ship" id="overnight" checked />
                <label for="overnight">Overnight</label>
                <input type="radio" name="ship" id="23day"/>
                <label for="23day">2-3 days</label>
                <input type="radio" name="ship" id="710day"/>
                <label for="710day">7-10 days</label>
            </fieldset>
  
            <div class="cWrapper">
                <a href="#" data-rel="back" data-role="button" data-inline="true"
                   data-direction="reverse">Back</a>
                <a href="#" id="submit" data-role="button"
                   data-inline="true">Submit Order</a>
            </div>
        </div>
    </div>
</body>
</html>

The new page is called checkout. I kept this form simple, prompting the user for a name and providing choices for gift wrapping and the shipping method. You can see how the page appears in Figure 33-5. I have shown the page in the portrait orientation because it allows me to display all of the elements without having to scroll.

9781430263883_Fig33-05.jpg

Figure 33-5. The checkout page

When the user taps the Submit Order button, I gather the data from the different pages in the HTML document and write the result as a JSON string to the console. The following is an example of such a string:

{"carnation":"3","rose":"1","orchid":"1",
    "name":"Adam Freeman","wrap":"yes","shipping":"23day"}

Summary

In this chapter, I took some of the core features of jQuery Mobile and combined them to create a simple mobile implementation of the flower shop example. By its nature, jQuery Mobile is a lot simpler than jQuery UI. The main challenge is to design an approach that gives the user the information he requires within the confines of a small screen.

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

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