Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 2140 lines (1873 sloc) 89.118 kB
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
1 Building the User UI using HTML5
2 ================================
391b226 @pmuir Fix author info
pmuir authored
3 Marius Bogoevici
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
4
ed0c866 @mbogoevici Structure improvements
mbogoevici authored
5 What Will You Learn Here?
6 -------------------------
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
7
8 You've just implemented the business services of your application, and exposed them through RESTful endpoints. Now you need to implement a flexible user interface that can be easily used with both desktop and mobile clients. After reading this tutorial, you will understand our front-end design and the choices that we made in its implementation. Topics covered include:
9
10 * Creating single-page applications using HTML5, JavaScript and JSON
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
11 * Using JavaScript frameworks for invoking RESTful endpoints and manipulating page content
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
12 * Feature and device detection
13 * Implementing a version of the user interface that is optimized for mobile clients using JavaScript frameworks such as jQuery mobile
14
15 The tutorial will show you how to perform all these steps in JBoss Developer Studio, including screenshots that guide you through. For those of you who prefer to watch and learn, the included video shows you how we performed all the steps.
16
17 First, the basics
18 -----------------
19
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
20 In this tutorial, we will build a single-page application. All the necessary code: HTML, CSS and JavaScript is retrieved within a single page load. Rather than refreshing the page every time the user changes a view, the content of the page will be redrawn by manipulating the DOM in JavaScript. The application uses REST calls to retrieve data from the server.
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
21
22 [[single-page-app_image]]
23 .Single page application
8ade984 @mbogoevici More updates to POH5 tutorial
mbogoevici authored
24 image::gfx/single-page-app.png[]
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
25
ed0c866 @mbogoevici Structure improvements
mbogoevici authored
26 Client-side MVC Support
27 ~~~~~~~~~~~~~~~~~~~~~~~
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
28
29 Because this is a moderately complex example, which involves multiple views and different types of data, you will use a client-side MVC framework to structure your application, which provides amongst others:
8ade984 @mbogoevici More updates to POH5 tutorial
mbogoevici authored
30
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
31 * routing support within your single page application;
32 * event-driven interaction between views and data;
33 * simplified CRUD invocations on RESTful services.
34
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
35 The client-side MVC framework that we use in this application in `backbone.js`. Its general architecture is shown below:
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
36
37 [[use-of-backbone_image]]
38 .Backbone interaction in Ticket Monster
04809d7 @mbogoevici Adding missing image
mbogoevici authored
39 image::gfx/backbone-usage.png[]
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
40
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
41 Modularity
42 ~~~~~~~~~~
43
44 As the application becomes more modular, the code also becomes more fragmented in order to support good separation of concerns. Ensuring that all the modules of the application are loaded properly at runtime becomes a more complex task as the application size increases. For conquering this complexity, we will be using the Asynchronous Module Definition mechanism as implemented by the `require.js` library.
45
46
47 .Asynchronous Module Definition
48 ***********************************************************************
49 The Asynchronous Module Definition (AMD) API specifies a mechanism for defining modules such that the module and its dependencies can be asynchronously loaded. This is particularly well suited for the browser environment where synchronous loading of modules incurs performance, usability, debugging, and cross-domain access problems.
50 ***********************************************************************
51
52 Templating
53 ~~~~~~~~~~
54
55 Instead of manipulating the DOM directly, and mixing up HTML with the JavaScript code, we create HTML markup fragments separately as templates, to be applied when the application views render.
56
57 In this application we use the templating support provided by `underscore.js`.
58
59 Mobile and desktop versions
60 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
61
62 We will build two variants of the single-page-application: one for the desktop and one for mobile clients. This is because the page flow and structure, as well as feature set are slightly different in each case. Because they are very similar, we will cover the desktop version of the application first, and then we will explain what is different in the case of the mobile version.
63
64 Setting up the structure
65 ------------------------
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
66
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
67 Before we start developing the user interface, we need to set up the general application structure and copy the library files. So the first thing to do is to create the directory structure.
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
68
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
69 [[ui-directory-structure]]
70 .File structure for our web application
71 image::gfx/ui-file-structure.png[]
c42fe31 @mbogoevici POH5 tutorial - start
mbogoevici authored
72
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
73 We will copy all stylesheets in the `resources/css` folder, `resources/img` contains the images and `resources/templates` contains the HTML templates for generating the views.
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
74
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
75 `resources/js` will contain the JavaScript code, split between `resource/js/lib` - which contains the libraries used by the application and `resources/js/app` which contains the application code. The latter will contain the application modules, in subsequent subdirectories, for models, collections, routers and views.
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
76
a8820cb @mbogoevici Add mobile content
mbogoevici authored
77 The first step in implementing our solution is copying the corresponding files into the `resources/css` and `resources/js/lib` folders:
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
78
a8820cb @mbogoevici Add mobile content
mbogoevici authored
79 * require.js - for AMD support, along with its plugins
80 ** text - for loading text files, in our case the HTML templates
81 ** order - for enforcing load ordering if necessary
82 * jQuery - general purpose library for HTML traversal and manipulation
83 * Underscore - JavaScript utility library (and a dependency of Backbone)
84 * Backbone - Client-side MVC frame
85 * Bootstrap - UI components and stylesheets for page structuring
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
86
87 Then, we will create the main page of the application (which will be actually referenced by the browser).
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
88
89 .src/main/webapp/desktop-index.html
ae6253a @mbogoevici Some diagrams
mbogoevici authored
90 [source,html]
91 -------------------------------------------------------------------------------------------------------
92 <!DOCTYPE html>
93 <html>
94 <head>
95 <title>Ticket Monster</title>
96 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
97 <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0;">
98
99 <link type="text/css" rel="stylesheet" href="resources/css/screen.css"/>
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
100 <link rel="stylesheet" href="resources/css/bootstrap.css" type="text/css" media="all"/>
ae6253a @mbogoevici Some diagrams
mbogoevici authored
101 <script data-main="resources/js/main-desktop" src="resources/js/libs/require.js"></script>
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
102
103 <!-- Add JavaScript library for IE6-8 support of HTML5 elements -->
ae6253a @mbogoevici Some diagrams
mbogoevici authored
104 <!--[if lt IE 9]>
105 <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
106 <![endif]-->
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
107
ae6253a @mbogoevici Some diagrams
mbogoevici authored
108 </head>
109 <body>
110
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
111 <!--
112 The main layout of the page - contains the menu and the 'content' &lt;div/&gt; in which all the
113 views will render the content.
114 -->
ae6253a @mbogoevici Some diagrams
mbogoevici authored
115 <div id="container">
116 <div id="menu">
117 <div class="navbar">
118 <div class="navbar-inner">
119 <div class="container">
120 <a class="brand">JBoss Ticket Monster</a>
121 <ul class="nav">
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
122 <li><a href="#events">Events</a></li>
123 <li><a href="#venues">Venues</a></li>
124 <li><a href="#bookings">Bookings</a></li>
125 <li><a href="#about">About</a></li>
ae6253a @mbogoevici Some diagrams
mbogoevici authored
126 </ul>
127 </div>
128 </div>
129 </div>
130 </div>
131 <div id="content" class="container-fluid">
132 </div>
133 </div>
134
135 <footer style="">
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
136 <div style="text-align: center;"><img src="resources/img/logo.png" alt="HTML5"/></div>
ae6253a @mbogoevici Some diagrams
mbogoevici authored
137 </footer>
138
139 </body>
140 </html>
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
141 -------------------------------------------------------------------------------------------------------
ae6253a @mbogoevici Some diagrams
mbogoevici authored
142
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
143 As you can see, the page does not contain too much. It loads the custom stylesheet of the application, as well as the one required by Twitter bootstrap, sets up instructions for loading the application scripts and defines a general structure of the page.
ae6253a @mbogoevici Some diagrams
mbogoevici authored
144
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
145 The actual HTML code of the page contains a menu definition which will be present on all the pages, as well as an empty element named `content` which is essentially a placeholder for the application views. When a view is displayed, it will apply a template and populate it.
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
146
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
147 The JavaScript code of the page is loaded by `require.js`, according to the module definition contained in `resources/js/main-desktop.js`, as follows:
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
148
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
149 .src/main/webapp/resources/js/main-desktop.js
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
150 -------------------------------------------------------------------------------------------------------
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
151 /**
152 * Shortcut alias definitions - will come in handy when declaring dependencies
153 * Also, they allow you to keep the code free of any knowledge about library
154 * locations and versions
155 */
156 require.config({
157 paths: {
158 jquery:'libs/jquery-1.7.1',
159 underscore:'libs/underscore',
160 text:'libs/text',
161 order:'libs/order',
162 bootstrap: 'libs/bootstrap',
163 utilities: 'app/utilities',
164 router
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
165 }
166 });
167
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
168 // Backbone is not AMD-ready, so a individual module is declared
169 define("backbone", [
170 // the order plugin is used to ensure that the modules are loaded in the right order
171 'order!jquery',
172 'order!underscore',
173 'order!libs/backbone'], function(){
174 return Backbone;
175 });
176
177 // Now we declare all the dependencies
178 require([
179 'order!jquery',
180 'order!underscore',
181 'order!backbone',
182 'text',
183 'order!bootstrap',
184 ], function(){
185 console.log('all loaded')
186 });
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
187 -------------------------------------------------------------------------------------------------------
188
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
189 As you see, this module loads all the utility libraries. Later on, when we will have written the application code too, we will load it here as well.
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
190
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
191 Visualising Events
192 ------------------
6e315b7 @mbogoevici POH5 continued
mbogoevici authored
193
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
194 The first use case that we implement is event navigation. The users will be able to view the list of events and select the one that they want to attend. After doing so, they will select a venue, and will be able to choose a performance date and time.
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
195
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
196 The Event model
197 ~~~~~~~~~~~~~~~
198
199 We will define a Backbone model for holding individual event data. Nearly each domain entity (booking, event, venue) is represented by a corresponding Backbone model.
200
201 .src/main/webapp/resources/js/app/models/event.js
202 -------------------------------------------------------------------------------------------------------
203 /**
204 * Module for the Event model
205 */
206 define([
207 'backbone' // depends and imports Backbone
208 ], function (Backbone) {
209 /**
210 * The Event model class definition
211 * Used for CRUD operations against individual events
212 */
213 var Event = Backbone.Model.extend({
214 urlRoot:'rest/events' // the URL for perfoming CRUD operations
215 });
216 // export the Event class
217 return Event;
218 });
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
219 -------------------------------------------------------------------------------------------------------
220
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
221 The `Event` model can perform CRUD operations directly against the REST services.
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
222
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
223 .Backbone Models
224 ***********************************************************************
225 Backbone models contain the interactive data as well as a large part of the logic surrounding it: conversions, validations, computed properties, and access control. The can also perform CRUD operations with the REST service.
226 ***********************************************************************
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
227
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
228 The Events collection
229 ~~~~~~~~~~~~~~~~~~~~~
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
230
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
231 We will define a Backbone collection for handling groups of events (like the events list).
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
232
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
233 .src/main/webapp/resources/js/app/collections/events.js
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
234 -------------------------------------------------------------------------------------------------------
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
235 /**
236 * Module for the Events collection
237 */
238 define([
239 // Backbone and the collection element type are dependencies
240 'backbone',
241 'app/models/event'
242 ], function (Backbone, Event) {
243 /**
244 * Here we define the Bookings collection
245 * We will use it for CRUD operations on Bookings
246 */
247 var Events = Backbone.Collection.extend({
248 url:"rest/events", // the URL for performing CRUD operations
249 model: Event,
250 id:"id", // the 'id' property of the model is the identifier
251 comparator:function (model) {
252 return model.get('category').id;
253 }
254 });
255 return Events;
256 });
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
257 -------------------------------------------------------------------------------------------------------
258
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
259 By mapping the model and collection to a REST endpoint you can perform CRUD operations without having to invoke the services explicitly. You will see how that works a bit later.
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
260
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
261 .Backbone Collections
262 ***********************************************************************
263 Collections are ordered sets of models. They can handle events which are fired as a result of a change to a
264 individual member, and can perform CRUD operations for syncing up contents against RESTful services.
265 ***********************************************************************
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
266
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
267 The EventsView view
268 ~~~~~~~~~~~~~~~~~~~
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
269
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
270 Now that we have implemented the data components of the example, we need to create the view that displays them.
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
271
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
272 .src/main/webapp/resources/js/app/views/desktop/events.js
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
273 -------------------------------------------------------------------------------------------------------
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
274 define([
275 'backbone',
276 'utilities',
277 'text!../../../../templates/desktop/events.html'
278 ], function (
279 Backbone,
280 utilities,
281 eventsTemplate) {
282
283 var EventsView = Backbone.View.extend({
284 events:{
285 "click a":"update"
286 },
287 render:function () {
288 var categories = _.uniq(
289 _.map(this.model.models, function(model){
290 return model.get('category')
291 }, function(item){
292 return item.id
293 }));
294 utilities.applyTemplate($(this.el), eventsTemplate, {categories:categories, model:this.model})
295 $(this.el).find('.item:first').addClass('active');
296 $(".collapse").collapse()
297 $("a[rel='popover']").popover({trigger:'hover'});
298 return this
299 },
300 update:function () {
301 $("a[rel='popover']").popover('hide')
302 }
303 });
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
304
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
305 return EventsView;
306 });
307 -------------------------------------------------------------------------------------------------------
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
308
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
309 The view is attached to a DOM element (the `el` property). When the `render` method is invoked, it manipulates the DOM and renders the view. We could have achieved that by writing these instructions directly in the method, but that would make it hard to change the page design later on. Rather than that, we will create a template and apply it, thus separating the HTML view code from the view implementation.
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
310
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
311 .src/main/webapp/resources/templates/desktop/events.html
312 [source,html]
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
313 -------------------------------------------------------------------------------------------------------
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
314 <div class="row-fluid">
315 <div class="span3">
316 <div id="itemMenu">
317
318 <%
319 _.each(categories, function (category) {
320 %>
321 <div class="accordion-group">
322 <div class="accordion-heading">
323 <a class="accordion-toggle" style="color: #fff; background: #000;"
324 data-target="#category-<%=category.id%>-collapsible" data-toggle="collapse"
325 data-parent="#itemMenu"><%= category.description %></a>
326 </div>
327 <div id="category-<%=category.id%>-collapsible" class="collapse in accordion-body">
328 <div id="category-<%=category.id%>" class="accordion-inner">
329
330 <%
331 _.each(model.models, function (model) {
332 if (model.get('category').id == category.id) {
333 %>
334 <p><a href="#venues/<%=model.attributes.id%>" rel="popover"
335 data-content="<%=model.attributes.description%>"
336 data-original-title="<%=model.attributes.name%>"><%=model.attributes.name%></a></p>
337 <% }
338 });
339 %>
340 </div>
341 </div>
342 </div>
343 <% }); %>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
344 </div>
345 </div>
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
346 <div id='itemSummary' class="span9">
347 <div class="row-fluid">
348 <div class="span11">
349 <div id="eventCarousel" class="carousel">
350 <!-- Carousel items -->
351 <div class="carousel-inner">
352 <%_.each(model.models, function(model) { %>
353 <div class="item">
354 <img src='rest/media/<%=model.attributes.mediaItem.id%>'/>
355
356 <div class="carousel-caption">
357 <h4><%=model.attributes.name%></h4>
358
359 <p><%=model.attributes.description%></p>
360 <a class="btn btn-danger" href="#events/<%=model.id%>">Book tickets</a>
361 </div>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
362 </div>
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
363 <% }) %>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
364 </div>
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
365 <!-- Carousel nav -->
366 <a class="carousel-control left" href="#eventCarousel" data-slide="prev">&lsaquo;</a>
367 <a class="carousel-control right" href="#eventCarousel" data-slide="next">&rsaquo;</a>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
368 </div>
369 </div>
370 </div>
371 </div>
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
372 </div>
373 -------------------------------------------------------------------------------------------------------
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
374
375
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
376 Besides applying the template and preparing the data that will be used to fill it (the `categories` and `model` entries in the map), this method also performs the JavaScript calls that are required to initialize the UI components (in this case the Twitter Bootstrap carousel and popover).
377
378 A view can also listen to events fired by children of it's `el` root element. In this case, the `update` method is set up within the `events` property of the class to listen to clicks on anchors.
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
379
380 Now that the views are in place, you will need to add a routing rule to the application. We will create a router and add our first routes.
381
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
382 Routing
383 ~~~~~~~
384
385 We will continue by defining a Router which provides linkable, bookmarkable and shareable URLs for the various locations in our application.
386
387 .src/main/webapp/resources/js/app/router/desktop/router.js
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
388 -------------------------------------------------------------------------------------------------------
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
389 /**
390 * A module for the router of the desktop application
391 */
392 define("router", [
393 'jquery',
394 'underscore',
395 'backbone',
396 'app/collections/events',
397 'app/views/desktop/events',
398 ],function ($,
399 _,
400 Backbone,
401 Events,
402 EventsView) {
403
404 /**
405 * The Router class contains all the routes within the application -
406 * i.e. URLs and the actions that will be taken as a result.
407 *
408 * @type {Router}
409 */
410
411 var Router = Backbone.Router.extend({
412 routes:{
413 "":"events", // listen to #events
414 "events":"events" // listen to #events
415 },
416 events:function () {
417 //initialize the events collection
418 var events = new Events();
419 // create an events view
420 var eventsView = new EventsView({model:events, el:$("#content")});
421 // render the view when the collection elements are fetched from the
422 // RESTful service
423 events.bind("reset",
424 function () {
425 eventsView.render();
426 }).fetch();
427 });
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
428
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
429 // Create a router instance
430 var router = new Router();
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
431
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
432 // Begin routing
433 Backbone.history.start();
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
434
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
435 return router;
436 });
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
437 -------------------------------------------------------------------------------------------------------
438
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
439 Remember, this is a single page application. You will be able to navigate either using urls such as `http://localhost:8080/ticket-monster/desktop-index.html#events` or from with relative urls from within the application itself (this being exactly what the main menu does). The portion after the hash sign represents the url within the page, the one on which the router will act. The `routes` property maps urls to controller function. In the example above, we have two controller functions.
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
440
441 `events` handles the `#events` URL and will retrieve the events in our application through a REST call. You don't have to do the REST call yourself, it will be triggered the `fetch` invocation on the `Events` collection (remember our earlier point about mapping collections to REST urls?). The `reset` event on the collection is invoked when the data from the server is received and the collection is populated, and this triggers the rendering of the events view (which is bound to the `#content` div). Notice how the whole process is orchestrated in an event-driven fashion - the models, views and controllers interact through events.
442
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
443 Once the router has been defined, all that remains is to cause is to be loaded by the main module definition. Because the router depends on all the other components (models, collections and views) of the application, directly or indirectly, it will be the only one that is explicitly listed in the `main-desktop` definition, which will change as follows:
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
444
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
445 .src/main/webapp/resources/js/main-desktop.js
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
446 -------------------------------------------------------------------------------------------------------
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
447 require.config({
448 paths: {
449 jquery:'libs/jquery-1.7.1',
450 underscore:'libs/underscore',
451 text:'libs/text',
452 order:'libs/order',
453 bootstrap: 'libs/bootstrap',
454 utilities: 'app/utilities',
455 router:'app/router/desktop/router'
456 }
457 });
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
458
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
459 ...
460
461 // Now we declare all the dependencies
462 require([
463 'order!jquery',
464 'order!underscore',
465 'order!backbone',
466 'text',
467 'order!bootstrap',
468 'router'
469 ], function(){
470 console.log('all loaded')
471 });
472 -------------------------------------------------------------------------------------------------------
473
474 Viewing a single event
475 ----------------------
476
477 With the events list view now in place, we can begin implementing the next step of the use case: adding a view for visualizing the details of an individual event, selecting a venue and a performance time.
478
479 We already have the models in place so all we need to do is to create a additional view and expand the router. The view comes first.
480
481 .src/main/webapp/resources/js/app/views/desktop/event-detail.js
482 -------------------------------------------------------------------------------------------------------
483 define([
484 'backbone',
485 'utilities',
486 'require',
487 'text!../../../../templates/desktop/event-detail.html',
488 'text!../../../../templates/desktop/media.html',
489 'text!../../../../templates/desktop/event-venue-description.html',
490 'bootstrap'
491 ], function (
492 Backbone,
493 utilities,
494 require,
495 eventDetailTemplate,
496 mediaTemplate,
497 eventVenueDescriptionTemplate) {
498 var EventDetail = Backbone.View.extend({
499 events:{
500 "click input[name='bookButton']":"beginBooking",
501 "change select[id='venueSelector']":"refreshShows",
502 "change select[id='dayPicker']":"refreshTimes"
503 },
504 render:function () {
505 $(this.el).empty()
506 utilities.applyTemplate($(this.el), eventDetailTemplate, this.model.attributes);
507 $("#bookingOption").hide();
508 $("#venueSelector").attr('disabled', true);
509 $("#dayPicker").empty();
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
510 $("#dayPicker").attr('disabled', true)
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
511 $("#performanceTimes").empty();
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
512 $("#performanceTimes").attr('disabled', true)
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
513 var self = this
514 $.getJSON("rest/shows?event=" + this.model.get('id'), function (shows) {
515 self.shows = shows
516 $("#venueSelector").empty().append("<option value='0'>Select a venue</option>");
517 $.each(shows, function (i, show) {
518 $("#venueSelector").append("<option value='" + show.id + "'>"
519 + show.venue.address.city + " : " + show.venue.name + "</option>")
520 });
521 $("#venueSelector").removeAttr('disabled')
522 if ($("#venueSelector").val()) {
523 $("#venueSelector").change()
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
524 }
525 })
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
526 },
527 beginBooking:function () {
528 require("router").navigate('/book/' +
529 $("#venueSelector option:selected").val() + '/' + $("#performanceTimes").val(), true)
530 },
531 refreshShows:function (event) {
532 $("#dayPicker").empty();
533
534 var selectedShowId = event.currentTarget.value;
535
536 if (selectedShowId != 0) {
537 var selectedShow = _.find(this.shows, function (show) {
538 return show.id == selectedShowId
539 });
540 this.selectedShow = selectedShow;
541 utilities.applyTemplate($("#eventVenueDescription"), eventVenueDescriptionTemplate, {venue:selectedShow.venue});
542 var times = _.uniq(_.sortBy(_.map(selectedShow.performances, function (performance) {
543 return (new Date(performance.date).withoutTimeOfDay()).getTime()
544 }), function (item) {
545 return item
546 }));
547 utilities.applyTemplate($("#venueMedia"), mediaTemplate, selectedShow.venue)
548 $("#dayPicker").removeAttr('disabled')
549 $("#performanceTimes").removeAttr('disabled')
550 _.each(times, function (time) {
551 var date = new Date(time)
552 $("#dayPicker").append("<option value='" + date.toYMD() + "'>"
553 + date.toPrettyStringWithoutTime() +
554 "</option>")
555 })
556 this.refreshTimes()
557 $("#bookingWhen").show(100)
558 } else {
559 $("#bookingWhen").hide(100)
560 $("#bookingOption").hide()
561 $("#dayPicker").empty()
562 $("#venueMedia").empty()
563 $("#eventVenueDescription").empty()
564 $("#dayPicker").attr('disabled', true)
565 $("#performanceTimes").empty()
566 $("#performanceTimes").attr('disabled', true)
567 }
568
569 },
570 refreshTimes:function () {
571 var selectedDate = $("#dayPicker").val();
572 $("#performanceTimes").empty()
573 if (selectedDate) {
574 $.each(this.selectedShow.performances, function (i, performance) {
575 var performanceDate = new Date(performance.date);
576 if (_.isEqual(performanceDate.toYMD(), selectedDate)) {
577 $("#performanceTimes").append("<option value='" + performance.id + "'>"
578 + performanceDate.getHours().toZeroPaddedString(2) + ":" + performanceDate.getMinutes().toZeroPaddedString(2) + "</option>")
579 }
580 })
581 }
582 $("#bookingOption").show()
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
583 }
584
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
585 });
586
587 return EventDetail;
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
588 });
589 -------------------------------------------------------------------------------------------------------
590
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
591 This view is already a bit more complex than the global events view. The main reason for that happening is that certain portions of the page need to be updated whenever the user chooses a different venue.
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
592
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
593 [[ui-event-detail]]
594 .On the event details page some fragments need to be re-rendered when the user changes the venue
595 image::gfx/ui-event-details.png[]
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
596
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
597 The view responds to three different events:
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
598
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
599 * a of the current venue will trigger a reload of the details and venue image, as well as of the performance times. The application will retrieve the latter information through a RESTful web service;
600 * changing the performance day will cause the performance time selector to reload;
601 * once the venue and performance time and date have been selected, the user can navigate to the booking page.
602
603 The corresponding templates for the three fragments used above are shown below
604
605 .src/main/webapp/resources/templates/desktop/event-detail.html
606 [source,html]
607 -------------------------------------------------------------------------------------------------------
608 <div class="row-fluid" xmlns="http://www.w3.org/1999/html">
609 <h2 class="page-header"><%=name%></h2>
610 </div>
611 <div class="row-fluid">
612 <div class="span4 well">
613 <div class="row-fluid"><h3 class="page-header span6">What?</h3>
614 <img width="100" src='rest/media/<%=mediaItem.id%>'/></div>
615 <div class="row-fluid">
616 <p>&nbsp;</p>
617
618 <div class="span12"><%= description %></div>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
619 </div>
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
620 </div>
621 <div class="span4 well">
622 <div class="row-fluid"><h3 class="page-header span6">Where?</h3>
623 <div class="span6" id='venueMedia'/>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
624 </div>
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
625 <div class='row-fluid'><select id='venueSelector'/>
626 <div id="eventVenueDescription"/>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
627 </div>
628 </div>
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
629 <div id='bookingWhen' style="display: none;" class="span2 well">
630 <h3 class="page-header">When?</h3>
631 <select class="span2" id="dayPicker">
632 <option value="-1">Select a day</option>
633 </select>
634 <select class="span2" id="performanceTimes"/>
635 <option value="-1">Select a time</option>
636 </select>
637
638 <div id='bookingOption'><input name="bookButton" class="btn btn-primary" type="button"
639 value="Order tickets"></div>
640 </div>
641 </div>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
642 -------------------------------------------------------------------------------------------------------
643
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
644 .src/main/webapp/resources/templates/desktop/event-venue-description.html
645 [source,html]
646 -------------------------------------------------------------------------------------------------------
647 <address>
648 <p><%= venue.description %></p>
649 <p><strong>Address:</strong></p>
650 <p><%= venue.address.street %></p>
651 <p><%= venue.address.city %>, <%= venue.address.country %></p>
652 </address>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
653 -------------------------------------------------------------------------------------------------------
654
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
655 .src/main/webapp/resources/templates/desktop/event-venue-description.html
656 [source,html]
657 -------------------------------------------------------------------------------------------------------
658 <address>
659 <p><%= venue.description %></p>
660 <p><strong>Address:</strong></p>
661 <p><%= venue.address.street %></p>
662 <p><%= venue.address.city %>, <%= venue.address.country %></p>
663 </address>
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
664 -------------------------------------------------------------------------------------------------------
665
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
666 Now that the view has actually been created, we need to add it to the router:
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
667
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
668 .src/main/webapp/resources/js/app/router/desktop/router.js
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
669 -------------------------------------------------------------------------------------------------------
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
670 /**
671 * A module for the router of the desktop application
672 */
673 define("router", [
674 ...
675 'app/models/event',
676 ...,
677 'app/views/desktop/event-detail'
678 ],function (
679 ...
680 Event,
681 ...
682 EventDetailView) {
683
684 var Router = Backbone.Router.extend({
685 routes:{
686 ...
687 "events/:id":"eventDetail",
688 },
689 ...
690 eventDetail:function (id) {
691 var model = new Event({id:id});
692 var eventDetailView = new EventDetailView({model:model, el:$("#content")});
693 model.bind("change",
694 function () {
695 eventDetailView.render();
696 }).fetch();
697 });
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
698 });
699 -------------------------------------------------------------------------------------------------------
700
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
701 As you can see, this is extremely similar to the previous view and route, except that right now the application can also navigate routes such as `http://localhost:8080/ticket-monster/desktop-index#events/1`. This can be entered directly in the browser or it can be navigated as a relative path to `#events/1` from within the applicaton, which is what the collapsible events menu above does.
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
702
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
703 With this in place, all that remains is to implement the final view of this use case, creating the bookings.
704
705 Creating Bookings
706 -----------------
707
708 The user has chosen an event, a venue and a performance time, so the last step in our implementation is creating a booking. Users can select one of the available sections for the show's venue, and once they do so, they will be able to enter a number of tickets for each category available for this show (Adult, Child, etc.) and add them to the current order. Once they do so, a summary view gets updated. Users can also choose to remove tickets from the order. When the order is complete, they can enter the contact information (e-mail address) and can submit it to the server.
709
710 First, we will add the new view:
711
712 .src/main/webapp/resources/js/app/views/desktop/create-booking.js
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
713 -------------------------------------------------------------------------------------------------------
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
714 define([
715 'backbone',
716 'utilities',
717 'require',
718 'text!../../../../templates/desktop/booking-confirmation.html',
719 'text!../../../../templates/desktop/create-booking.html',
720 'text!../../../../templates/desktop/ticket-categories.html',
721 'text!../../../../templates/desktop/ticket-summary-view.html',
722 'bootstrap'
723 ],function (
724 Backbone,
725 utilities,
726 require,
727 bookingConfirmationTemplate,
728 createBookingTemplate,
729 ticketEntriesTemplate,
730 ticketSummaryViewTemplate){
731
732
733 var TicketCategoriesView = Backbone.View.extend({
734 id:'categoriesView',
735 events:{
736 "change input":"onChange"
737 },
738 render:function () {
739 if (this.model != null) {
740 var ticketPrices = _.map(this.model, function (item) {
741 return item.ticketPrice;
742 });
743 utilities.applyTemplate($(this.el), ticketEntriesTemplate, {ticketPrices:ticketPrices});
744 } else {
745 $(this.el).empty();
746 }
747 return this;
748 },
749 onChange:function (event) {
750 var value = event.currentTarget.value;
751 var ticketPriceId = $(event.currentTarget).data("tm-id");
752 var modifiedModelEntry = _.find(this.model, function(item) { return item.ticketPrice.id == ticketPriceId});
753 if ($.isNumeric(value) && value > 0) {
754 modifiedModelEntry.quantity = parseInt(value);
755 }
756 else {
757 delete modifiedModelEntry.quantity;
758 }
759 }
760 });
761
762 var TicketSummaryView = Backbone.View.extend({
763 tagName:'tr',
764 events:{
765 "click i":"removeEntry"
766 },
767 render:function () {
768 var self = this;
769 utilities.applyTemplate($(this.el), ticketSummaryViewTemplate, this.model.bookingRequest);
770 },
771 removeEntry:function () {
772 this.model.tickets.splice(this.model.index, 1);
773 }
774 });
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
775
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
776 var CreateBookingView = Backbone.View.extend({
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
777
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
778 events:{
779 "click input[name='submit']":"save",
780 "change select":"refreshPrices",
781 "keyup #email":"updateEmail",
782 "click input[name='add']":"addQuantities",
783 "click i":"updateQuantities"
784 },
785 render:function () {
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
786
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
787 var self = this;
788 $.getJSON("rest/shows/" + this.model.showId, function (selectedShow) {
789
790 self.currentPerformance = _.find(selectedShow.performances, function (item) {
791 return item.id == self.model.performanceId;
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
792 });
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
793
794 var id = function (item) {return item.id;};
795 // prepare a list of sections to populate the dropdown
796 var sections = _.uniq(_.sortBy(_.pluck(selectedShow.ticketPrices, 'section'), id), true, id);
797 utilities.applyTemplate($(self.el), createBookingTemplate, {
798 sections:sections,
799 show:selectedShow,
800 performance:self.currentPerformance});
801 self.ticketCategoriesView = new TicketCategoriesView({model:{}, el:$("#ticketCategoriesViewPlaceholder") });
802 self.ticketSummaryView = new TicketSummaryView({model:self.model, el:$("#ticketSummaryView")});
803 self.show = selectedShow;
804 self.ticketCategoriesView.render();
805 self.ticketSummaryView.render();
806 $("#sectionSelector").change();
807 });
808 },
809 refreshPrices:function (event) {
810 var ticketPrices = _.filter(this.show.ticketPrices, function (item) {
811 return item.section.id == event.currentTarget.value;
812 });
813 var ticketPriceInputs = new Array();
814 _.each(ticketPrices, function (ticketPrice) {
815 ticketPriceInputs.push({ticketPrice:ticketPrice});
816 });
817 this.ticketCategoriesView.model = ticketPriceInputs;
818 this.ticketCategoriesView.render();
819 },
820 save:function (event) {
821 var bookingRequest = {ticketRequests:[]};
822 var self = this;
823 bookingRequest.ticketRequests = _.map(this.model.bookingRequest.tickets, function (ticket) {
824 return {ticketPrice:ticket.ticketPrice.id, quantity:ticket.quantity}
825 });
826 bookingRequest.email = this.model.bookingRequest.email;
827 bookingRequest.performance = this.model.performanceId
828 $.ajax({url:"rest/bookings",
829 data:JSON.stringify(bookingRequest),
830 type:"POST",
831 dataType:"json",
832 contentType:"application/json",
833 success:function (booking) {
834 this.model = {}
835 $.getJSON('rest/shows/performance/' + booking.performance.id, function (retrievedPerformance) {
836 utilities.applyTemplate($(self.el), bookingConfirmationTemplate, {booking:booking, performance:retrievedPerformance })
837 });
838 }}).error(function (error) {
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
839 if (error.status == 400 || error.status == 409) {
840 var errors = $.parseJSON(error.responseText).errors;
841 _.each(errors, function (errorMessage) {
842 $("#request-summary").append('<div class="alert alert-error"><a class="close" data-dismiss="alert">×</a><strong>Error!</strong> ' + errorMessage + '</div>')
843 });
844 } else {
845 $("#request-summary").append('<div class="alert alert-error"><a class="close" data-dismiss="alert">×</a><strong>Error! </strong>An error has occured</div>')
846 }
847
848 })
849
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
850 },
851 addQuantities:function () {
852 var self = this;
853
854 _.each(this.ticketCategoriesView.model, function (model) {
855 if (model.quantity != undefined) {
856 var found = false;
857 _.each(self.model.bookingRequest.tickets, function (ticket) {
858 if (ticket.ticketPrice.id == model.ticketPrice.id) {
859 ticket.quantity += model.quantity;
860 found = true;
861 }
862 });
863 if (!found) {
864 self.model.bookingRequest.tickets.push({ticketPrice:model.ticketPrice, quantity:model.quantity});
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
865 }
866 }
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
867 });
868 this.ticketCategoriesView.model = null;
869 $('option:selected', 'select').removeAttr('selected');
870 this.ticketCategoriesView.render();
871 this.updateQuantities();
872 },
873 updateQuantities:function () {
874 // make sure that tickets are sorted by section and ticket category
875 this.model.bookingRequest.tickets.sort(function (t1, t2) {
876 if (t1.ticketPrice.section.id != t2.ticketPrice.section.id) {
877 return t1.ticketPrice.section.id - t2.ticketPrice.section.id;
878 }
879 else {
880 return t1.ticketPrice.ticketCategory.id - t2.ticketPrice.ticketCategory.id;
881 }
882 });
883
884 this.model.bookingRequest.totals = _.reduce(this.model.bookingRequest.tickets, function (totals, ticketRequest) {
885 return {
886 tickets:totals.tickets + ticketRequest.quantity,
887 price:totals.price + ticketRequest.quantity * ticketRequest.ticketPrice.price
888 };
889 }, {tickets:0, price:0.0});
890
891 this.ticketSummaryView.render();
892 this.setCheckoutStatus();
893 },
894 updateEmail:function (event) {
895 if ($(event.currentTarget).is(':valid')) {
896 this.model.bookingRequest.email = event.currentTarget.value;
897
898 } else {
899 delete this.model.bookingRequest.email;
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
900 }
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
901 this.setCheckoutStatus();
902 },
903 setCheckoutStatus:function () {
904 if (this.model.bookingRequest.totals != undefined && this.model.bookingRequest.totals.tickets > 0 && this.model.bookingRequest.email != undefined && this.model.bookingRequest.email != '') {
905 $('input[name="submit"]').removeAttr('disabled');
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
906 }
907 else {
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
908 $('input[name="submit"]').attr('disabled', true);
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
909 }
910 }
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
911 });
912
913 return CreateBookingView;
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
914 });
915 -------------------------------------------------------------------------------------------------------
916
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
917 The code above may be surprising: after all, we said that we were going to add a single view, but instead, we have added three! The reason is that this view makes use of subviews (`TicketCategoriesView` and `TicketSummaryView`) for re-rendering parts of the main view. Whenever the user changes the current section selection, it will display a list of available tickets, by price category. Whenever the user adds the tickets to the main request, the current summary will be re-rendered. Changes in quantities or the target email may enable or disable the submission button - the booking request data is re-validated in the process. We do not create separate modules for the subviews, since they are not referenced outside the module itself.
918
919 The user submission is handled by the `save` method which constructs the a JSON object in the format required by a POST at `http://localhost:8080/ticket-monster/rest/bookings`, and performs the AJAX call. In case of a successful response, a confirmation view is rendered. On failure, a warning is displayed and the user may continue to edit the form.
920
921 The corresponding templates for the views above are shown below:
922
923 .src/main/webapp/resources/templates/desktop/booking-confirmation.html
924 [source,html]
925 -------------------------------------------------------------------------------------------------------
926 <div class="row-fluid">
927 <h2 class="page-header">Booking #<%=booking.id%> confirmed!</h2>
928 </div>
929 <div class="row-fluid">
930 <div class="span5 well">
931 <h4 class="page-header">Checkout information</h4>
932 <p><strong>Email: </strong><%= booking.contactEmail %></p>
933 <p><strong>Event: </strong> <%= performance.event.name %></p>
934 <p><strong>Venue: </strong><%= performance.venue.name %></p>
935 <p><strong>Date: </strong><%= new Date(booking.performance.date).toPrettyString() %></p>
936 <p><strong>Created on: </strong><%= new Date(booking.createdOn).toPrettyString() %></p>
937 </div>
938 <div class="span5 well">
939 <h4 class="page-header">Ticket allocations</h4>
940 <table class="table table-striped table-bordered" style="background-color: #fffffa;">
941 <thead>
942 <tr>
943 <th>Ticket #</th>
944 <th>Category</th>
945 <th>Section</th>
946 <th>Row</th>
947 <th>Seat</th>
948 </tr>
949 </thead>
950 <tbody>
951 <% $.each(_.sortBy(booking.tickets, function(ticket) {return ticket.id}), function (i, ticket) { %>
952 <tr>
953 <td><%= ticket.id %></td>
954 <td><%=ticket.ticketCategory.description%></td>
955 <td><%=ticket.seat.section.name%></td>
956 <td><%=ticket.seat.rowNumber%></td>
957 <td><%=ticket.seat.number%></td>
958 </tr>
959 <% }) %>
960 </tbody>
961 </table>
962 </div>
963 </div>
964 <div class="row-fluid" style="padding-bottom:30px;">
965 <div class="span2"><a href="#">Home</a></div>
966 </div>
967 -------------------------------------------------------------------------------------------------------
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
968
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
969 .src/main/webapp/resources/templates/desktop/create-booking.html
970 [source,html]
971 -------------------------------------------------------------------------------------------------------
972 <div class="row-fluid">
973 <div class="span12">
974 <h2><%=show.event.name%>
975 <small><%=show.venue.name%>, <%=new Date(performance.date).toPrettyString()%></p></small>
976 </h2>
977 </div>
978 </div>
979 <div class="row-fluid">
980 <div class="span5 well">
981 <h4 class="page-header">Select tickets</h4>
982
983 <div id="sectionSelectorPlaceholder" class="row-fluid">
984 <div class="control-group">
985 <label class="control-label" for="sectionSelect">Section</label>
986 <div class="controls">
987 <select id="sectionSelect">
988 <option value="-1" selected="true">Choose a section</option>
989 <% _.each(sections, function(section) { %>
990 <option value="<%=section.id%>"><%=section.name%> - <%=section.description%></option>
991 <% }) %>
992 </select>
993 </div>
994 </div>
995 </div>
996 <div id="ticketCategoriesViewPlaceholder" class="row-fluid"></div>
997 </div>
998 <div id="request-summary" class="span5 offset1 well">
999 <h4 class="page-header">Order summary</h4>
1000 <div id="ticketSummaryView" class="row-fluid"/>
1001 <h4 class="page-header">Checkout</h4>
1002 <div class="row-fluid">
1003 <input type='email' id="email" placeholder="Email" required/>
1004 <input type='button' class="btn btn-primary" name="submit" value="Checkout"
1005 style="margin-bottom:9px;" disabled="true"/>
1006 </div>
1007 </div>
1008 </div>
1009 -------------------------------------------------------------------------------------------------------
1010
1011 .src/main/webapp/resources/templates/desktop/ticket-categories.html
1012 [source,html]
1013 -------------------------------------------------------------------------------------------------------
1014 <% if (ticketPrices.length > 0) { %>
1015 <% _.each(ticketPrices, function(ticketPrice) { %>
1016 <div id="ticket-category-input-<%=ticketPrice.id%>">
1017 <div class="control-group">
1018 <label class="control-label"><%=ticketPrice.ticketCategory.description%></label>
1019
1020 <div class="controls">
1021 <div class="input-append">
1022 <input class="span2" rel="tooltip" title="Enter value" type="number" min="1"
1023 max="9"
1024 data-tm-id="<%=ticketPrice.id%>"
1025 placeholder="Number of tickets"
1026 name="tickets-<%=ticketPrice.ticketCategory.id%>"/>
1027 <span class="add-on" style="margin-bottom:9px">@ $<%=ticketPrice.price%></span>
1028 </div>
1029 </div>
1030 </div>
1031 </div>
1032 <% }) %>
1033 <div class="control-group">
1034 <label class="control-label"/>
1035 <div class="controls">
1036 <input type="button" class="btn btn-primary" name="add" value="Add tickets"/>
1037 </div>
1038 </div>
1039 <% } %>
1040 -------------------------------------------------------------------------------------------------------
1041
1042 .src/main/webapp/resources/templates/desktop/ticket-summary-view.html
1043 [source,html]
1044 -------------------------------------------------------------------------------------------------------
1045 <div class="span12">
1046 <% if (tickets.length>0) { %>
1047 <table class="table table-bordered table-condensed row-fluid" style="background-color: #fffffa;">
1048 <thead>
1049 <tr>
1050 <th colspan="5"><strong>Requested tickets</strong></th>
1051 </tr>
1052 <tr>
1053 <th>Section</th>
1054 <th>Category</th>
1055 <th>Quantity</th>
1056 <th>Price</th>
1057 <th></th>
1058 </tr>
1059 </thead>
1060 <tbody id="ticketRequestSummary">
1061 <% _.each(tickets, function (ticketRequest, index, tickets) { %>
1062 <tr>
1063 <td><%= ticketRequest.ticketPrice.section.name %></td>
1064 <td><%= ticketRequest.ticketPrice.ticketCategory.description %></td>
1065 <td><%= ticketRequest.quantity %></td>
1066 <td>$<%=ticketRequest.ticketPrice.price%></td>
1067 <td><i class="icon-trash"/></td>
1068 </tr>
1069 <% }); %>
1070 </tbody>
1071 </table>
1072 <p/>
1073 <div class="row-fluid">
1074 <div class="span5"><strong>Total ticket count:</strong> <%= totals.tickets %></div>
1075 <div class="span5"><strong>Total price:</strong> $<%=totals.price%></div></div>
1076 <% } else { %>
1077 No tickets requested.
1078 <% } %>
1079 </div>
1080 -------------------------------------------------------------------------------------------------------
1081
1082 Finally, once the view is available, we can add it's corresponding routing rule:
1083
1084 .src/main/webapp/resources/js/app/router/desktop/router.js
1085 -------------------------------------------------------------------------------------------------------
1086 /**
1087 * A module for the router of the desktop application
1088 */
1089 define("router", [
1090 ...
1091 'app/views/desktop/create-booking',
1092 ...
1093 ],function (
1094 ...
1095 CreateBooking
1096 ...
1097 ) {
1098
1099 var Router = Backbone.Router.extend({
1100 routes:{
1101 ...
1102 "book/:showId/:performanceId":"bookTickets",
1103 },
1104 ...
1105 bookTickets:function (showId, performanceId) {
1106 var createBookingView =
1107 new CreateBookingView({
1108 model:{ showId:showId,
1109 performanceId:performanceId,
1110 bookingRequest:{tickets:[]}},
1111 el:$("#content")
1112 });
1113 createBookingView.render();
1114 }
1115 });
1116 -------------------------------------------------------------------------------------------------------
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
1117
a8820cb @mbogoevici Add mobile content
mbogoevici authored
1118 This concludes the implementation of a full booking use case, starting with listing events, continuing with selecting a venue and performance time, and ending with choosing tickets and completing the order.
1119
1120 The other use cases: full booking starting from venues and visualizing existing bookings are conceptually similar, so you can just copy the remaining files in the `src/main/webapp/resources/js/app/models`, `src/main/webapp/resources/js/app/collections`,
1121 `src/main/webapp/resources/js/app/views/desktop` and the remainder of `src/main/webapp/resources/js/app/routers/desktop/router.js`.
1122
1123
6f929b4 @mbogoevici User front end - final draft
mbogoevici authored
1124 Mobile view
1125 -----------
2d639fc @mbogoevici POH5 expanded
mbogoevici authored
1126
a8820cb @mbogoevici Add mobile content
mbogoevici authored
1127 The mobile version of the application follows roughly the same architecture as the desktop version. The distinctions are mainly caused by the functional differences between the two versions, as well as the use of jQuery mobile.
1128
1129 Setting up the structure
1130 ~~~~~~~~~~~~~~~~~~~~~~~~
1131
1132 The first step in implementing our solution is copying the corresponding files into the `resources/css` and `resources/js/lib` folders:
1133
1134 * require.js - for AMD support, along with its plugins
1135 ** text - for loading text files, in our case the HTML templates
1136 ** order - for enforcing load ordering if necessary
1137 * jQuery - general purpose library for HTML traversal and manipulation;
1138 * Underscore - JavaScript utility library (and a dependency of Backbone);
1139 * Backbone - Client-side MVC framework;
1140 * jQuery Mobile - user interface system for mobile devices;
1141
1142 (If you built the desktop application following the previous example, some files may already be in place.)
1143
1144 Next, we will add the mobile main page.
1145
1146 .src/main/webapp/mobile-index.html
1147 [source,html]
1148 -------------------------------------------------------------------------------------------------------
1149 <?xml version="1.0" encoding="UTF-8"?>
1150 <!DOCTYPE html>
1151 <html>
1152 <head>
1153 <title>Ticket Monster - mobile version</title>
1154 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1155 <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0"/>
1156
1157 <link rel="stylesheet" href="resources/css/jquery.mobile-1.1.0.css"/>
1158 <link rel="stylesheet" href="resources/css/m.screen.css"/>
1159 <script data-main="resources/js/main-mobile" src="resources/js/libs/require.js"></script>
1160 </head>
1161 <body>
1162
1163 <div id="container" data-role="page" data-ajax="false"></div>
1164
1165 </body>
1166 </html>
1167 -------------------------------------------------------------------------------------------------------
1168
1169 As you can see, this page is even simpler. We just load the stylesheets for the application, and then we use `require.js` to load the JavaScript code and page fragments. All the pages will render inside the `container` element, which has a `data-role` attribute with the `page` value. This means that this is a jQuery Mobile page.
1170
1171 Then, we will add the module loader.
1172
1173 .src/main/webapp/resources/js/main-mobile.js
1174 -------------------------------------------------------------------------------------------------------
1175 /**
1176 * Shortcut alias definitions - will come in handy when declaring dependencies
1177 * Also, they allow you to keep the code free of any knowledge about library
1178 * locations and versions
1179 */
1180 require.config({
1181 paths: {
1182 jquery:'libs/jquery-1.7.1',
1183 jquerymobile:'libs/jquery.mobile-1.1.0',
1184 text:'libs/text',
1185 order: 'libs/order',
1186 utilities: 'app/utilities',
1187 router:'app/router/mobile/router'
1188 }
1189 });
1190
1191 define('underscore',[
1192 'libs/underscore'
1193 ], function(){
1194 return _;
1195 });
1196
1197 define("backbone", [
1198 'order!jquery',
1199 'order!underscore',
1200 'order!libs/backbone'
1201 ], function(){
1202 return Backbone;
1203 });
1204
1205
1206 // Now we declare all the dependencies
1207 require(['router'],
1208 function(){
1209 console.log('all loaded')
1210 });
1211 -------------------------------------------------------------------------------------------------------
1212
1213 In this application, we combine Backbone and jQuery Mobile. Each framework has its own
1214 strengths: jQuery Mobile provides the UI components and touch device support, whereas
1215 Backbone provides the MVC support. However, there is some overlap between the two, as jQuery
1216 Mobile provides its own navigation mechanism which will need to be disabled.
1217 So in the router code below you will find the customizations that need to be performed in order to
1218 get the two frameworks working together - disabling the jQuery Mobile navigation and
1219 the `defaultHandler` added to the route for handling jQuery Mobile transitions between internal
1220 pages (such as the ones generated by a nested listview).
1221
1222 .src/main/webapp/resources/js/app/router/mobile/router.js
1223 -------------------------------------------------------------------------------------------------------
1224 /**
1225 * A module for the router of the desktop application.
1226 *
1227 */
1228 define("router",[
1229 'jquery',
1230 'jquerymobile',
1231 'underscore',
1232 'backbone',
1233 'utilities'
1234 ],function ($,
1235 jqm,
1236 _,
1237 Backbone,
1238 Booking,
1239 utilities) {
1240
1241 // prior to creating an starting the router, we disable jQuery Mobile's own routing mechanism
1242 $.mobile.hashListeningEnabled = false;
1243 $.mobile.linkBindingEnabled = false;
1244 $.mobile.pushStateEnabled = false;
1245
1246 /**
1247 * The Router class contains all the routes within the application - i.e. URLs and the actions
1248 * that will be taken as a result.
1249 *
1250 * @type {Router}
1251 */
1252 var Router = Backbone.Router.extend({
1253 //no routes added yet
1254 defaultHandler:function (actions) {
1255 if ("" != actions) {
1256 $.mobile.changePage("#" + actions, {transition:'slide', changeHash:false, allowSamePageTransition:true});
1257 }
1258 }
1259 });
1260
1261 // Create a router instance
1262 var router = new Router();
1263
1264 // Begin routing
1265 Backbone.history.start();
1266
1267 return router;
1268 });
1269 -------------------------------------------------------------------------------------------------------
1270
1271 Please note that the router will be also have more responsibilities, will
1272 interact with more libraries and it will declare them as its dependencies. We won't specify
1273 them in the main loader.
1274
1275 The landing page
1276 ~~~~~~~~~~~~~~~~
1277
1278 The first page in our application is the landing page. First, we add the template for it.
1279
1280 .src/main/webapp/resources/templates/mobile/home-view.html
1281 [source,html]
1282 -------------------------------------------------------------------------------------------------------
1283 <div data-role="header">
1284 <h3>Ticket Monster</h3>
1285 </div>
1286 <div data-role="content" align="center">
1287 <img src="resources/gfx/icon_large.png"/>
1288 <h4 align="left">Find events</h4>
1289 <ul data-role="listview">
1290 <li>
1291 <a href="#events">By Category</a>
1292 </li>
1293 <li>
1294 <a href="#venues">By Location</a>
1295 </li>
1296 </ul>
1297 </div>
1298 -------------------------------------------------------------------------------------------------------
1299
1300 Now we have to add the page to the router:
1301
1302 .src/main/webapp/resources/js/app/router/mobile/router.js
1303 -------------------------------------------------------------------------------------------------------
1304 /**
1305 * A module for the router of the desktop application.
1306 *
1307 */
1308 define("router",[
1309 ...
1310 'text!../templates/mobile/home-view.html'
1311 ],function (
1312 ...
1313 HomeViewTemplate) {
1314
1315 ...
1316 var Router = Backbone.Router.extend({
1317 routes:{
1318 "":"home"
1319 },
1320 ...
1321 home:function () {
1322 utilities.applyTemplate($("#container"), HomeViewTemplate);
1323 try {
1324 $("#container").trigger('pagecreate');
1325 } catch (e) {
1326 // workaround for a spurious error thrown when creating the page initially
1327 }
1328 }
1329 });
1330 ...
1331 });
1332 -------------------------------------------------------------------------------------------------------
1333
1334 Because jQuery Mobile navigation is disabled in order to be able to take advantage of
1335 backbone's support, we need to tell jQuery Mobile explicitly to enhance the page content
1336 in order to create the mobile view - in this case, we trigger the jQuery Mobile `pagecreate`
1337 event explicitly to ensure that the page gets the appropriate look and feel.
1338
1339 The events view
1340 ~~~~~~~~~~~~~~~
1341
1342 Just as in the case of the desktop view, we would like to display a list of events first.
1343 Since mobile interfaces are more constrained, we will just show a simple list view. First
1344 we will create the appropriate Backbone view.
1345
1346 .src/main/webapp/resources/js/app/views/mobile/events.js
1347 -------------------------------------------------------------------------------------------------------
1348 define([
1349 'backbone',
1350 'utilities',
1351 'text!../../../../templates/mobile/events.html'
1352 ], function (
1353 Backbone,
1354 utilities,
1355 eventsView) {
1356
1357 var EventsView = Backbone.View.extend({
1358 render:function () {
1359 var categories = _.uniq(
1360 _.map(this.model.models, function(model){
1361 return model.get('category')
1362 }, function(item){
1363 return item.id
1364 }));
1365
1366 utilities.applyTemplate($(this.el), eventsView, {categories:categories, model:this.model})
1367 $(this.el).trigger('pagecreate')
1368 }
1369 });
1370
1371 return EventsView;
1372 });
1373 -------------------------------------------------------------------------------------------------------
1374
1375 As you can see, it is conceptually very similar to the desktop view, the main difference being the explicit
1376 hint to jQuery mobile through the `pagecreate` event invocation.
1377
1378 The next step is adding the template for rendering the view.
1379
1380 .src/main/webapp/resources/templates/mobile/events.html
1381 [source,html]
1382 -------------------------------------------------------------------------------------------------------
1383 <div data-role="header">
1384 <a data-role="button" data-icon="home" href="#">Home</a>
1385 <h3>Categories</h3>
1386 </div>
1387 <div data-role="content" id='itemMenu'>
1388 <div id='categoryMenu' data-role='listview' data-filter='true' data-filter-placeholder='Event category name ...'>
1389 <%
1390 _.each(categories, function (category) {
1391 %>
1392 <li>
1393 <a href="#"><%= category.description %></a>
1394 <ul id="category-<%=category.id%>">
1395 <%
1396 _.each(model.models, function (model) {
1397 if (model.get('category').id == category.id) {
1398 %>
1399 <li>
1400 <a href="#events/<%=model.attributes.id%>"><%=model.attributes.name%></a>
1401 </li>
1402 <% }
1403 });
1404 %>
1405 </ul>
1406 </li>
1407 <% }); %>
1408 </div>
1409 </div>
1410 -------------------------------------------------------------------------------------------------------
1411
1412 And finally, we need to instruct the router to invoke the page.
1413
1414 .src/main/webapp/resources/js/app/router/mobile/router.js
1415 -------------------------------------------------------------------------------------------------------
1416 /**
1417 * A module for the router of the desktop application.
1418 *
1419 */
1420 define("router",[
1421 ...
1422 'app/collections/events',
1423 ...
1424 'app/views/mobile/events'
1425 ...
1426 ],function (
1427 ...,
1428 Events,
1429 ...,
1430 EventsView,
1431 ...) {
1432
1433 ...
1434 var Router = Backbone.Router.extend({
1435 routes:{
1436 ...
1437 "events":"events"
1438 ...
1439 },
1440 ...
1441 events:function () {
1442 var events = new Events;
1443 var eventsView = new EventsView({model:events, el:$("#container")});
1444 events.bind("reset",
1445 function () {
1446 eventsView.render();
1447 }).fetch();
1448 }
1449 ...
1450 });
1451 ...
1452 });
1453 -------------------------------------------------------------------------------------------------------
1454
1455 Just as in the case of the desktop application, the list of events will be accessible at `#events`, like
1456 for example `http://localhost:8080/ticket-monster/mobile-index.html#events`.
1457
1458 Viewing an individual event
1459 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
1460
1461 For viewing individual event details, we need to create the view first.
1462
1463 .src/main/webapp/resources/js/app/views/mobile/event-detail.js
1464 -------------------------------------------------------------------------------------------------------
1465 define(['backbone',
1466 'utilities',
1467 'require',
1468 'text!../../../../templates/mobile/event-detail.html',
1469 'text!../../../../templates/mobile/event-venue-description.html'
1470 ], function (
1471 Backbone,
1472 utilities,
1473 require,
1474 eventDetail,
1475 eventVenueDescription) {
1476
1477 var EventDetailView = Backbone.View.extend({
1478 events:{
1479 "click a[id='bookButton']":"beginBooking",
1480 "change select[id='showSelector']":"refreshShows",
1481 "change select[id='performanceTimes']":"performanceSelected",
1482 "change select[id='dayPicker']":'refreshTimes'
1483 },
1484 render:function () {
1485 $(this.el).empty()
1486 utilities.applyTemplate($(this.el), eventDetail, this.model.attributes)
1487 $(this.el).trigger('create')
1488 $("#bookButton").addClass("ui-disabled")
1489 var self = this;
1490 $.getJSON("rest/shows?event=" + this.model.get('id'), function (shows) {
1491 self.shows = shows;
1492 $("#showSelector").empty().append("<option data-placeholder='true'>Choose a venue ...</option>");
1493 $.each(shows, function (i, show) {
1494 $("#showSelector").append("<option value='" + show.id + "'>" + show.venue.address.city + " : " + show.venue.name + "</option>");
1495 });
1496 $("#showSelector").selectmenu('refresh', true)
1497 $("#dayPicker").selectmenu('disable')
1498 $("#dayPicker").empty().append("<option data-placeholder='true'>Choose a show date ...</option>")
1499 $("#performanceTimes").selectmenu('disable')
1500 $("#performanceTimes").empty().append("<option data-placeholder='true'>Choose a show time ...</option>")
1501 });
1502 $("#dayPicker").empty();
1503 $("#dayPicker").selectmenu('disable');
1504 $("#performanceTimes").empty();
1505 $("#performanceTimes").selectmenu('disable');
1506 $(this.el).trigger('pagecreate');
1507 },
1508 performanceSelected:function () {
1509 if ($("#performanceTimes").val() != 'Choose a show time ...') {
1510 $("#bookButton").removeClass("ui-disabled")
1511 } else {
1512 $("#bookButton").addClass("ui-disabled")
1513 }
1514 },
1515 beginBooking:function () {
1516 require('router').navigate('book/' + $("#showSelector option:selected").val() + '/' + $("#performanceTimes").val(), true)
1517 },
1518 refreshShows:function (event) {
1519
1520 var selectedShowId = event.currentTarget.value;
1521
1522 if (selectedShowId != 'Choose a venue ...') {
1523 var selectedShow = _.find(this.shows, function (show) {
1524 return show.id == selectedShowId
1525 });
1526 this.selectedShow = selectedShow;
1527 var times = _.uniq(_.sortBy(_.map(selectedShow.performances, function (performance) {
1528 return (new Date(performance.date).withoutTimeOfDay()).getTime()
1529 }), function (item) {
1530 return item
1531 }));
1532 utilities.applyTemplate($("#eventVenueDescription"), eventVenueDescription, {venue:selectedShow.venue});
1533 $("#detailsCollapsible").show()
1534 $("#dayPicker").removeAttr('disabled')
1535 $("#performanceTimes").removeAttr('disabled')
1536 $("#dayPicker").empty().append("<option data-placeholder='true'>Choose a show date ...</option>")
1537 _.each(times, function (time) {
1538 var date = new Date(time)
1539 $("#dayPicker").append("<option value='" + date.toYMD() + "'>" + date.toPrettyStringWithoutTime() + "</option>")
1540 });
1541 $("#dayPicker").selectmenu('refresh')
1542 $("#dayPicker").selectmenu('enable')
1543 this.refreshTimes()
1544 } else {
1545 $("#detailsCollapsible").hide()
1546 $("#eventVenueDescription").empty()
1547 $("#dayPicker").empty()
1548 $("#dayPicker").selectmenu('disable')
1549 $("#performanceTimes").empty()
1550 $("#performanceTimes").selectmenu('disable')
1551 }
1552
1553
1554 },
1555 refreshTimes:function () {
1556 var selectedDate = $("#dayPicker").val();
1557 $("#performanceTimes").empty().append("<option data-placeholder='true'>Choose a show time ...</option>")
1558 if (selectedDate) {
1559 $.each(this.selectedShow.performances, function (i, performance) {
1560 var performanceDate = new Date(performance.date);
1561 if (_.isEqual(performanceDate.toYMD(), selectedDate)) {
1562 $("#performanceTimes").append("<option value='" + performance.id + "'>" + performanceDate.getHours().toZeroPaddedString(2) + ":" + performanceDate.getMinutes().toZeroPaddedString(2) + "</option>")
1563 }
1564 })
1565 $("#performanceTimes").selectmenu('enable')
1566 }
1567 $("#performanceTimes").selectmenu('refresh')
1568 this.performanceSelected()
1569 }
1570
1571 });
1572
1573 return EventDetailView;
1574 });
1575 -------------------------------------------------------------------------------------------------------
1576
1577 Again, this is very similar to the desktop version, the main differences being due to the specific jQuery
1578 Mobile invocations. And now we need to provide the actual page templates
1579
1580 .src/main/webapp/resources/templates/mobile/event-detail.html
1581 [source,html]
1582 -------------------------------------------------------------------------------------------------------
1583 <div data-role="header">
1584 <h3>Book tickets</h3>
1585 </div>
1586 <div data-role="content">
1587 <h3><%=name%></h3>
1588 <img width='100px' src='rest/media/<%=mediaItem.id%>'/>
1589 <p><%=description%></p>
1590 <div data-role="fieldcontain">
1591 <label for="showSelector"><strong>Where</strong></label>
1592 <select id='showSelector' data-mini='true'/>
1593 </div>
1594
1595 <div data-role="collapsible" data-content-theme="c" style="display: none;"
1596 id="detailsCollapsible">
1597 <h3>Venue details</h3>
1598
1599 <div id="eventVenueDescription">
1600 </div>
1601 </div>
1602
1603 <div data-role='fieldcontain'>
1604 <fieldset data-role='controlgroup'>
1605 <legend><strong>When</strong></legend>
1606 <label for="dayPicker">When:</label>
1607 <select id='dayPicker' data-mini='true'/>
1608
1609 <label for="performanceTimes">When:</label>
1610 <select id="performanceTimes" data-mini='true'/>
1611
1612 </fieldset>
1613 </div>
1614
1615 </div>
1616 <div data-role="footer" class="ui-bar ui-grid-c">
1617 <div class="ui-block-a"></div>
1618 <div class="ui-block-b"></div>
1619 <div class="ui-block-c"></div>
1620 <a id='bookButton' class="ui-block-e" data-theme='b' data-role="button" data-icon="check">Book</a>
1621 </div>
1622 -------------------------------------------------------------------------------------------------------
1623
1624 .src/main/webapp/resources/templates/mobile/event-venue-description.html
1625 [source,html]
1626 -------------------------------------------------------------------------------------------------------
1627 <img width="100" src="rest/media/<%=venue.mediaItem.id%>"/></p>
1628 <%= venue.description %>
1629 <address>
1630 <p><strong>Address:</strong></p>
1631 <p><%= venue.address.street %></p>
1632 <p><%= venue.address.city %>, <%= venue.address.country %></p>
1633 </address>
1634 -------------------------------------------------------------------------------------------------------
1635
1636 And finally, we need to instruct add this to the router, explicitly indicating jQuery Mobile that a
1637 transition has to take place after the view is rendered - in order to allow the page to render
1638 correctly after it has been invoked from the listview.
1639
1640 .src/main/webapp/resources/js/app/router/mobile/router.js
1641 -------------------------------------------------------------------------------------------------------
1642 /**
1643 * A module for the router of the desktop application.
1644 *
1645 */
1646 define("router",[
1647 ...
1648 'app/model/event',
1649 ...
1650 'app/views/mobile/event-detail'
1651 ...
1652 ],function (
1653 ...,
1654 Event,
1655 ...,
1656 EventDetailView,
1657 ...) {
1658
1659 ...
1660 var Router = Backbone.Router.extend({
1661 routes:{
1662 ...
1663 "events/:id":"eventDetail",
1664 ...
1665 },
1666 ...
1667 eventDetail:function (id) {
1668 var model = new Event({id:id});
1669 var eventDetailView = new EventDetailView({model:model, el:$("#container")});
1670 model.bind("change",
1671 function () {
1672 eventDetailView.render();
1673 $.mobile.changePage($("#container"), {transition:'slide', changeHash:false});
1674 }).fetch();
1675 }
1676 ...
1677 });
1678 ...
1679 });
1680 -------------------------------------------------------------------------------------------------------
1681
1682 Just as the desktop version, the mobile event detail view allows users to choose a venue
1683 and a performance time. The next step is booking some tickets.
1684
1685 Booking tickets
1686 ~~~~~~~~~~~~~~~
1687
1688 The process of booking tickets is simpler than in the case of desktop version. Users can
1689 select a section and enter the number of tickets for each category - however, there is
1690 no process of adding and removing tickets to an order, once the form is filled out, the
1691 users can submit it.
1692
1693 First, we will create the views:
1694
1695 .src/main/webapp/resources/js/app/views/mobile/create-booking.js
1696 -------------------------------------------------------------------------------------------------------
1697 define([
1698 'backbone',
1699 'utilities',
1700 'require',
1701 'text!../../../../templates/mobile/booking-details.html',
1702 'text!../../../../templates/mobile/create-booking.html',
1703 'text!../../../../templates/mobile/confirm-booking.html',
1704 'text!../../../../templates/mobile/ticket-entries.html',
1705 'text!../../../../templates/mobile/ticket-summary-view.html'
1706 ], function (
1707 Backbone,
1708 utilities,
1709 require,
1710 bookingDetailsTemplate,
1711 createBookingTemplate,
1712 confirmBookingTemplate,
1713 ticketEntriesTemplate,
1714 ticketSummaryViewTemplate) {
1715
1716 var TicketCategoriesView = Backbone.View.extend({
1717 id:'categoriesView',
1718 events:{
1719 "change input":"onChange"
1720 },
1721 render:function () {
1722 var views = {};
1723
1724 if (this.model != null) {
1725 var ticketPrices = _.map(this.model, function (item) {
1726 return item.ticketPrice;
1727 });
1728 utilities.applyTemplate($(this.el), ticketEntriesTemplate, {ticketPrices:ticketPrices});
1729 } else {
1730 $(this.el).empty();
1731 }
1732 $(this.el).trigger('pagecreate');
1733 return this;
1734 },
1735 onChange:function (event) {
1736 var value = event.currentTarget.value;
1737 var ticketPriceId = $(event.currentTarget).data("tm-id");
1738 var modifiedModelEntry = _.find(this.model, function(item) { return item.ticketPrice.id == ticketPriceId});
1739 if ($.isNumeric(value) && value > 0) {
1740 modifiedModelEntry.quantity = parseInt(value);
1741 }
1742 else {
1743 delete modifiedModelEntry.quantity;
1744 }
1745 }
1746 });
1747
1748 var TicketSummaryView = Backbone.View.extend({
1749 render:function () {
1750 utilities.applyTemplate($(this.el), ticketSummaryViewTemplate, this.model.bookingRequest)
1751 }
1752 });
1753
1754 var ConfirmBookingView = Backbone.View.extend({
1755 events:{
1756 "click a[id='saveBooking']":"save",
1757 "click a[id='goBack']":"back"
1758 },
1759 render:function () {
1760 utilities.applyTemplate($(this.el), confirmBookingTemplate, this.model)
1761 this.ticketSummaryView = new TicketSummaryView({model:this.model, el:$("#ticketSummaryView")});
1762 this.ticketSummaryView.render();
1763 $(this.el).trigger('pagecreate')
1764 },
1765 back:function () {
1766 require("router").navigate('book/' + this.model.bookingRequest.show.id + '/' + this.model.bookingRequest.performance.id, true)
1767
1768 }, save:function (event) {
1769 var bookingRequest = {ticketRequests:[]};
1770 var self = this;
1771 _.each(this.model.bookingRequest.tickets, function (collection) {
1772 _.each(collection, function (model) {
1773 if (model.quantity != undefined) {
1774 bookingRequest.ticketRequests.push({ticketPrice:model.ticketPrice.id, quantity:model.quantity})
1775 };
1776 })
1777 });
1778
1779 bookingRequest.email = this.model.email;
1780 bookingRequest.performance = this.model.performanceId;
1781 $.ajax({url:"rest/bookings",
1782 data:JSON.stringify(bookingRequest),
1783 type:"POST",
1784 dataType:"json",
1785 contentType:"application/json",
1786 success:function (booking) {
1787 utilities.applyTemplate($(self.el), bookingDetailsTemplate, booking)
1788 $(self.el).trigger('pagecreate');
1789 }}).error(function (error) {
1790 alert(error);
1791 });
1792 this.model = {};
1793 }
1794 });
1795
1796
1797 var CreateBookingView = Backbone.View.extend({
1798
1799 events:{
1800 "click a[id='confirmBooking']":"checkout",
1801 "change select":"refreshPrices",
1802 "blur input[type='number']":"updateForm",
1803 "blur input[name='email']":"updateForm"
1804 },
1805 render:function () {
1806
1807 var self = this;
1808
1809 $.getJSON("rest/shows/" + this.model.showId, function (selectedShow) {
1810 self.model.performance = _.find(selectedShow.performances, function (item) {
1811 return item.id == self.model.performanceId;
1812 });
1813 var id = function (item) {return item.id;};
1814 // prepare a list of sections to populate the dropdown
1815 var sections = _.uniq(_.sortBy(_.pluck(selectedShow.ticketPrices, 'section'), id), true, id);
1816
1817 utilities.applyTemplate($(self.el), createBookingTemplate, { show:selectedShow,
1818 performance:self.model.performance,
1819 sections:sections});
1820 $(self.el).trigger('pagecreate');
1821 self.ticketCategoriesView = new TicketCategoriesView({model:{}, el:$("#ticketCategoriesViewPlaceholder") });
1822 self.model.show = selectedShow;
1823 self.ticketCategoriesView.render();
1824 $('a[id="confirmBooking"]').addClass('ui-disabled');
1825 $("#sectionSelector").change();
1826 });
1827
1828 },
1829 refreshPrices:function (event) {
1830 if (event.currentTarget.value != "Choose a section") {
1831 var ticketPrices = _.filter(this.model.show.ticketPrices, function (item) {
1832 return item.section.id == event.currentTarget.value;
1833 });
1834 var ticketPriceInputs = new Array();
1835 _.each(ticketPrices, function (ticketPrice) {
1836 var model = {};
1837 model.ticketPrice = ticketPrice;
1838 ticketPriceInputs.push(model);
1839 });
1840 $("#ticketCategoriesViewPlaceholder").show();
1841 this.ticketCategoriesView.model = ticketPriceInputs;
1842 this.ticketCategoriesView.render();
1843 $(this.el).trigger('pagecreate');
1844 } else {
1845 $("#ticketCategoriesViewPlaceholder").hide();
1846 this.ticketCategoriesView.model = new Array();
1847 this.updateForm();
1848 }
1849 },
1850 checkout:function () {
1851 this.model.bookingRequest.tickets.push(this.ticketCategoriesView.model);
1852 this.model.performance = new ConfirmBookingView({model:this.model, el:$("#container")}).render();
1853 $("#container").trigger('pagecreate');
1854 },
1855 updateForm:function () {
1856
1857 var totals = _.reduce(this.ticketCategoriesView.model, function (partial, model) {
1858 if (model.quantity != undefined) {
1859 partial.tickets += model.quantity;
1860 partial.price += model.quantity * model.ticketPrice.price;
1861 return partial;
1862 }
1863 }, {tickets:0, price:0.0});
1864 this.model.email = $("input[type='email']").val();
1865 this.model.bookingRequest.totals = totals;
1866 if (totals.tickets > 0 && $("input[type='email']").val()) {
1867 $('a[id="confirmBooking"]').removeClass('ui-disabled');
1868 } else {
1869 $('a[id="confirmBooking"]').addClass('ui-disabled');
1870 }
1871 }
1872 });
1873 return CreateBookingView;
1874 });
1875 -------------------------------------------------------------------------------------------------------
1876
1877 The views follow the same view/subview breakdown principles as in the case of the desktop
1878 application, except that the summary view is not rendered inline but after a page
1879 transition.
1880
1881 The next step is creating the page fragment templates. First, the actual page.
1882
1883 .src/main/webapp/resources/templates/mobile/create-booking.html
1884 [source,html]
1885 -------------------------------------------------------------------------------------------------------
1886 <div data-role="header">
1887 <h1>Book tickets</h1>
1888 </div>
1889 <div data-role="content">
1890 <p>
1891 <h3><%=show.event.name%></h3>
1892 </p>
1893 <p>
1894 <%=show.venue.name%>
1895 <p>
1896 <p>
1897 <small><%=new Date(performance.date).toPrettyString()%></small>
1898 </p>
1899 <div id="sectionSelectorPlaceholder">
1900 <div data-role="fieldcontain">
1901 <label for="sectionSelect">Section</label>
1902 <select id="sectionSelect">
1903 <option value="-1" selected="true">Choose a section</option>
1904 <% _.each(sections, function(section) { %>
1905 <option value="<%=section.id%>"><%=section.name%> - <%=section.description%></option>
1906 <% }) %>
1907 </select>
1908 </div>
1909 </div>
1910 <div id="ticketCategoriesViewPlaceholder" style="display:none;"/>
1911
1912 <div class="fieldcontain">
1913 <label>Contact email</label>
1914 <input type='email' name='email' placeholder="Email"/>
1915 </div>
1916 </div>
1917 <div data-role="footer" class="ui-bar">
1918 <a href="#" data-role="button" data-icon="delete">Cancel</a>
1919 <a id="confirmBooking" data-icon="check" data-role="button" disabled>Checkout</a>
1920 </div>
1921 -------------------------------------------------------------------------------------------------------
1922
1923 Next, the fragment that contains the input form for tickets, which will be re-rendered
1924 whenever the section selection changes.
1925
1926 .src/main/webapp/resources/templates/mobile/ticket-entries.html
1927 [source,html]
1928 -------------------------------------------------------------------------------------------------------
1929 <% if (ticketPrices.length > 0) { %>
1930 <form name="ticketCategories">
1931 <h4>Select tickets by category</h4>
1932 <% _.each(ticketPrices, function(ticketPrice) { %>
1933 <div id="ticket-category-input-<%=ticketPrice.id%>"/>
1934
1935 <fieldset data-role="fieldcontain">
1936 <label for="ticket-<%=ticketPrice.id%>"><%=ticketPrice.ticketCategory.description%>($<%=ticketPrice.price%>)</label>
1937 <input id="ticket-<%=ticketPrice.id%>" data-tm-id="<%=ticketPrice.id%>" type="number" placeholder="Enter value"
1938 name="tickets"/>
1939 </fieldset>
1940 <% }) %>
1941 </form>
1942 <% } %>
1943 -------------------------------------------------------------------------------------------------------
1944
1945 Before submitting the request to the server, the order will be confirmed:
1946
1947 .src/main/webapp/resources/templates/mobile/confirm-booking.html
1948 [source,html]
1949 -------------------------------------------------------------------------------------------------------
1950 <div data-role="header">
1951 <h1>Confirm order</h1>
1952 </div>
1953 <div data-role="content">
1954 <h3><%=show.event.name%></h3>
1955 <p><%=show.venue.name%></p>
1956 <p><small><%=new Date(performance.date).toPrettyString()%></small></p>
1957 <p><strong>Buyer:</strong> <emphasis><%=email%></emphasis></p>
1958 <div id="ticketSummaryView"/>
1959
1960 </div>
1961
1962 <div data-role="footer" class="ui-bar">
1963 <div class="ui-grid-b">
1964 <div class="ui-block-a"><a id="cancel" href="#" data-role="button" data-icon="delete">Cancel</a></div>
1965 <div class="ui-block-b"><a id="goBack" data-role="button" data-icon="back">Back</a></div>
1966 <div class="ui-block-c"><a id="saveBooking" data-icon="check" data-role="button">Buy!</a></div>
1967 </div>
1968 </div>
1969 -------------------------------------------------------------------------------------------------------
1970
1971 This page contains a summary subview:
1972
1973 .src/main/webapp/resources/templates/mobile/ticket-summary-view.html
1974 [source,html]
1975 -------------------------------------------------------------------------------------------------------
1976 <table>
1977 <thead>
1978 <tr>
1979 <th>Section</th>
1980 <th>Category</th>
1981 <th>Price</th>
1982 <th>Quantity</th>
1983 </tr>
1984 </thead>
1985 <tbody>
1986 <% _.each(tickets, function(ticketRequest) { %>
1987 <% _.each(ticketRequest, function(model) { %>
1988 <% if (model.quantity != undefined) { %>
1989 <tr>
1990 <td><%= model.ticketPrice.section.name %></td>
1991 <td><%= model.ticketPrice.ticketCategory.description %></td>
1992 <td>$<%= model.ticketPrice.price %></td>
1993 <td><%= model.quantity %></td>
1994 </tr>
1995 <% } %>
1996 <% }) %>
1997 <% }) %>
1998 </tbody>
1999 </table>
2000 <div data-theme="c">
2001 <h4>Totals</h4>
2002 <p><strong>Total tickets: </strong><%= totals.tickets %></p>
2003 <p> <strong>Total price: $</strong><%= totals.price %></p>
2004 </div>
2005 -------------------------------------------------------------------------------------------------------
2006
2007 And finally, the page that displays the booking confirmation.
2008
2009 .src/main/webapp/resources/templates/mobile/booking-details.html
2010 [source,html]
2011 -------------------------------------------------------------------------------------------------------
2012 <div data-role="header">
2013 <h1>Booking complete</h1>
2014 </div>
2015 <div data-role="content">
2016 <table id="confirm_tbl">
2017 <thead>
2018 <tr>
2019 <td colspan="5" align="center"><strong>Booking <%=id%></strong></td>
2020 <tr>
2021 <tr>
2022 <th>Ticket #</th>
2023 <th>Category</th>
2024 <th>Section</th>
2025 <th>Row</th>
2026 <th>Seat</th>
2027 </tr>
2028 </thead>
2029 <tbody>
2030 <% $.each(_.sortBy(tickets, function(ticket) {return ticket.id}), function (i, ticket) { %>
2031 <tr>
2032 <td><%= ticket.id %></td>
2033 <td><%=ticket.ticketCategory.description%></td>
2034 <td><%=ticket.seat.section.name%></td>
2035 <td><%=ticket.seat.rowNumber%></td>
2036 <td><%=ticket.seat.number%></td>
2037 </tr>
2038 <% }) %>
2039 </tbody>
2040 </table>
2041 </div>
2042 <div data-role="footer" class="ui-bar">
2043 <div class="ui-block-b"><a id="back" href="#" data-role="button" data-icon="back">Back</a></div>
2044 </div>
2045 -------------------------------------------------------------------------------------------------------
2046
2047 The last step is tying the view into the router.
2048
2049 .src/main/webapp/resources/js/app/router/desktop/router.js
2050 -------------------------------------------------------------------------------------------------------
2051 /**
2052 * A module for the router of the desktop application
2053 */
2054 define("router", [
2055 ...
2056 'app/views/mobile/create-booking',
2057 ...
2058 ],function (
2059 ...
2060 CreateBookingView
2061 ...) {
2062
2063 var Router = Backbone.Router.extend({
2064 routes:{
2065 ...
2066 "book/:showId/:performanceId":"bookTickets",
2067 ...
2068 },
2069 ...
2070 bookTickets:function (showId, performanceId) {
2071 var createBookingView = new CreateBookingView({
2072 model:{showId:showId, performanceId:performanceId,
2073 bookingRequest:{tickets:[]}},
2074 el:$("#container")
2075 });
2076 createBookingView.render();
2077 },
2078 ...
2079 );
2080 });
2081 -------------------------------------------------------------------------------------------------------
2082
2083 Device detection
2084 ------------------
2085
2086 Now we have two distinct single-page applications and we can point users to any of them
2087 easily. But instead letting the user figure out which page do they want to get to, we could
2088 simply redirect them to one of the pages based on the device that they have.
2089
2090 To this end, we are using `Modernizr.js` a JavaScript library that help us detect
2091 device capabilities - and which you can use for much more thank just desktop vs. mobile
2092 detection: it can identify which features from the HTML5 set are supported by a particular
2093 browser at runtime, which is extremely helpful for implementing progressive enhancement in
2094 applications.
2095
2096 So, the first step is to copy `modernizr.js` into `src/main/webapp/resources/js/libs`. Then,
2097 you will add the `src/main/webapp/index.html` file with the following content:
2098
2099 .src/main/webapp/index.html
fe26432 @mbogoevici Removed JavaScript source highlighting temporarily, PDF does not rend…
mbogoevici authored
2100 [source,html]
a8820cb @mbogoevici Add mobile content
mbogoevici authored
2101 -------------------------------------------------------------------------------------------------------
2102 <!DOCTYPE html>
2103 <html>
2104 <head>
2105 <script type="text/javascript" src="resources/js/libs/modernizr-2.0.6.js"></script>
2106
2107 <!--
2108 A simple check on the client. For touch devices or small-resolution screens
2109 show the mobile client. By enabling the mobile client on a small-resolution screen
2110 we allow for testing outside a mobile device (like for example the Mobile Browser
2111 simulator in JBoss Tools and JBoss Developer Studio).
2112 -->
2113 <script type="text/javascript">
2114 if (Modernizr.touch || Modernizr.mq("only all and (max-width: 480px)")) {
2115 location.replace('mobile-index.html')
2116 } else {
2117 location.replace('desktop-index.html')
2118 };
2119 </script>
2120 </head>
2121 <body>
2122
2123 </body>
2124 </html>
2125 -------------------------------------------------------------------------------------------------------
2126
2127 Now you can navigate to an URL like `http://localhost:8080/ticket-monster/` with either
2128 a mobile device or a desktop browser, and you will be redirected to the appropriate page.
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
Something went wrong with that request. Please try again.