In early 2015 I was given a brief to create a mobile sales app for my employers travelling salespeople to use when they visit customers. Despite the relative simplicity my 4 day deadline was still insane, although I did make it with something functional, ugly and full of bugs. What I present below is a complete rewrite of that code over the course of a few weeks, still a few feature gaps and such but all the important stuff is there.
The project as pitched was relatively simple: an attractive presentation of all our products on the first page, ability to set quantities and adjust prices and order details on the second, and take a signature on the third. Once all the data was entered, it would be sent to our web server which would store it in a database and ping the information off to head office.
I immediately knew this project would be targeting Android as it’s the cheapest way to do this, but the threat of other devices is ever present so I made a choice to use Apache Cordova and jQuery Mobile and build a web app that could run on everything. I’ve previously written the same application with both the native Android tools and then with Cordova+jQuery Mobile (although I still need to write about it on here), and found the development process much faster as a web app and the results to be near identical in every respect.
Let’s start at the top!
Walkthrough
Our first page in the process is indeed a list of products. You can filter the view by category, click an item to launch it’s popup and choose variations, and click any of the images in the top row of that popup to view a big version.
When a product is added, the count on the basket increases and a notification is shown. All the products the customer is interested in are added, and then clicking the basket yields a larger popup listing its contents, and giving the option to create either an order or quote.
For these images I’ve chosen an order – the quote is effectively the same but with less options and no prices. As you can see, the order screen again lists the basket, but this time includes a quantity and price entry field, with the latter automatically populated from the standard price list (the netting funnel in the image above has no list price, hence 0.00). All the input fields are tied to a change event to recalculate all the price information when altered. On this page we also pick/add customer and delivery address information, as well as a week-commencing delivery date and any notes attached to the order.
Picking either a customer or address yields a near identical popup which by default, lists everything in the database. A keydown event on the search box filters the database as you type, providing a smooth and fast experience. Alternatively, the second image above shows the popup for adding a new customer – we can also pick/add an address to link to them.
Once these details are entered they appear on the order page, and we can fill our last few pieces of data. The week commencing delivery date section lists every Monday in the selected month and year.
The final page reiterates all the order and customer details, and provides a signature box for the customer. The Save button stores all the data in the database, and attempts to submit it to the server. We do a connectivity check with the Cordova connectivity plugin, and if we aren’t online or encounter errors syncing some items we store them in a queue on the device for later.
The Sync button on the start page now displays the number of items queued to sync.
The last little bit of miscellany worthy of mention is the “Check for updates” button on the front page. A companion php script on the web server is capable of parsing and importing new/updated customer, address and pricing information from CSV files, and our app can then grab the new data. Slightly boring image because there isn’t anything new, but it works really nice.
I also won’t be included images of that php script (yet) because it’s totally lacking any styling at the moment, and thus looks like the rest of the internet did in 1992.
Implementation
For the most part jQuery Mobile provides an excellent framework for a web app once you’ve learnt the concepts. Based on previous experience with the framework and performance issues therein, I made a few choices to minimize the apps runtime footprint.
Incidently, if you didn’t know that web apps are slow, well they really are, and for an excellent in depth explanation I thoroughly recommend this article by Drew Crawford – it’s very long, but worth it. TL;DR: web apps are around 5x slower than native code, 50x slower than x86 C++, the language isn’t getting any faster and garbage collection will screw you over at every turn. This is a point to consider when planning an app: if you need big-scale features, you probably need native.
Our first note is on code structure. jQuery Mobile offers two options are far as static HTML page structure: multi-page with all your “pages” in a single HTML document, with jQm sorting them out at runtime, or; one page per HTML file, with AJAX requests made for new pages. Obviously, many smaller files means you aren’t loading and holding everything in memory from the get-go, but then what do you do with your Javascript? A lot of code is tied up in big collections of generic functions, but each page is going to have some custom, specific instructions – some have a few hundred lines.
In my first “4 day challenge” version of this code I just created a single file to hold all page-specific code, but this very quickly became cumbersome and unwieldy. Seperating in to seperate files is fine but you’d still be loading all the same code on app start, effectively just adding a bunch of new HTTP requests. I implemented a very simple hybrid approach, using jQuery’s $.getScript method to load each pages Javascript file when the page request is made.
var Page, Pages, app = { _loadedJS:[], init:function(){ if (!navigator.connection) { this.onDeviceReady(); }else document.addEventListener('deviceready', this.onDeviceReady, false); }, onDeviceReady:function(){ $(document).bind("pagebeforeshow",app.pageBeforeShow); $(document).bind("pagebeforehide",app.pageBeforeHide); }, pageBeforeShow:function(e){ app.currentPage = e.target.id; //load page's JS if (app._loadedJS.indexOf(app.currentPage) < 0){ $.getScript("pagejs/"+app.currentPage.toLowerCase()+".js", function(){ app._loadedJS.push(app.currentPage); Page = Pages[e.target.id]; if (Pages.hasOwnProperty(e.target.id) && Pages[e.target.id].hasOwnProperty("beforeshow"))Pages[e.target.id].beforeshow(); } ); return; } Page = Pages[e.target.id]; if (Pages.hasOwnProperty(e.target.id) && Pages[e.target.id].hasOwnProperty("beforeshow"))Pages[e.target.id].beforeshow(); }, pageBeforeHide:function(e){ if (Pages.hasOwnProperty(e.target.id) && Pages[e.target.id].hasOwnProperty("beforehide"))Pages[e.target.id].beforehide(); } }; app.init();
In the code above you can see we attach to “pagebeforeshow” and “pagebeforehide” events from jQm on the document, so we’re notified everytime a page changes. When we receive a beforeshow request we are given the ID of the requested page, so we check against this in an array holding all our loaded Javascript page ID’s, and we use $.getScript if we need to. Each page’s Javascript file includes all functions within an element of the global Pages object, and while the page is active all of its functions are also available in Page for ease of use.
Pages["index"] = { beforeshow:function(){ //display sync count Page.updateSyncButtonBubble(); }, updateSyncButtonBubble:function(){ var count = syncUtils.getTotalSyncCount(); if (count == 0){ $("#sync_count").css("display","none"); }else $("#sync_count").css("display","inline").text(count+""); } };
As you can see, each page can include functions titled beforeshow and beforehide, allowing it to run initialization and shutdown code where necessary. Above is the sum total of the code for the start page – all we need to do is update the pending sync item count. In the sequence of load events, beforeshow is fired once the page HTML document has been AJAX’d in, but before the transition animation has started, so we’re able to make changes to our new page before the user see’s it.
Data is stored using IndexedDB, which is supported on all modern platform versions. It’s a No-SQL database and stores generic Javascript objects – you still have “tables” with a primary key and additional keys that we can query against, but you aren’t limited to the table definition, any additional values can be stored in previously undefined columns.
In the initial prototype I used Jens Arp’s IDBWrapper, which provides the IndexedDB API on top of the older WebSQL and localStorage standards and removes some of the nuts and bolts that needn’t be worried about in a simple application (e.g, transactions). This is a great wrapper but leaves a lot to be desired; IndexedDB is generally very limited in querying ability – there is no analogue to “WHERE x LIKE ‘%text%'” wildcard searches, for example – but IDBWrapper provides no niceties on top of the basic KeyRange functionality. For this reason I switched to a more full-featured library in the final release, Dexie.
Dexie is chainable like jQuery, so we can condense our code nicely, and provides a Linq-a-like syntax to build queries. It still lacks things like wildcard searches because that’s a limitation of IndexedDB, but does provide a filter() method where you can test against each data row with Javascript. It also optionally supports transactions for safety, for example where your application ends while processing a large batch of operations within a transaction all changes would be rolled back to leave the database in a good state.
A final few tips for jQuery Mobile, concerning popups. Popups are brilliant, define them, run $(“#myPopup”).popup(“open”) and it appears! Super.
But once you start using them for many things you may run in to a few problems… First, the easy one: if you’re targeting Android 4 you may sometimes have popups that don’t show. This is an interesting bug in the rendering engine used for 4.x but the fix is simple, add the following single instruction to your CSS document.
.ui-popup { -webkit-transform:translateZ(0) }
I don’t know why this isn’t part of jQm as standard given the bug is known and those suggested fixes don’t work, but credit goes to this excellent forum poster for fixing this one.
With that sorted you’re probably creating loads of popups to do all manner of useful things! In this app I have about 8 “global” popups which are loaded with the main page, and some of the other pages contain some custom popups. There is an issue with all this popping up however, and that’s what happens if you try to open a popup while one is already open.
Well, really what doesn’t happen.
Among the generic popups are all the add and pick customer and address popups, and you may have noticed in the screenshots above the “add customer” dialog has buttons for picking or adding an address which launch those respective popups. By default, jQuery mobile will do nothing when those buttons are clicked. It turns out you can make this work, but you have to close the first popup first… and when I say it needs to be closed first, this includes the close animation, so some short timeouts are also required. To make this easy, I threw together this function which you’re free to steal.
openPopup:function(sel, chain){ if (typeof sel == "undefined" || sel.length == 0) { return; } if (typeof chain == "undefined") chain = true; var active = appUtils.getActivePopup(); if (active.length > 0 && !(active.length == 1 && active == sel)){ if (chain){ $(sel).on("popupafterclose", function(){ setTimeout( function(){ $(active).popup("open"); $(sel).off("popupafterclose"); }, 100 ); } ); } $(active).on("popupafterclose", function(){ setTimeout( function(){ $(sel).popup("open"); $(active).off("popupafterclose"); }, 100 ); } ).popup("close"); }else $(sel).popup("open"); }, getActivePopup:function() { var elem = $(".ui-popup-active"); if (elem.length > 0) { return elem.find("*[data-role='popup']"); } else return []; }
The primary function, openPopup, takes two parameters: sel is a selector string for the target popup, and chain is a boolean to decide whether, if the new popup closes an existing one, the previous popup should be reopened when the new popup is closed. Slightly confusing but I hope you got that.
From this we check for an already open popup. If there is one we utilize the “popupafterclose” events to queue the opening of our new popup, and restoration of the current popup if chain is true. Dead simple function, but very useful. In this app I’ve also tied some custom events in to those generic popups, to allow callbacks to the source of the popup so we can, for example, display the picked customer’s details on the order page, as well as storing them in the order details ready to save and submit.
That’s gonna be about it for now, hope you enjoyed the read and can make use of the tips.
— B