I recently built a simple web-based CRM tool only to run into a problem: my users didn’t have internet access all the time, so couldn’t reach it when on the road. Primary use was going to be on phones and tablets, so I could have built a native app, but with both Android and iOS devices to support it would have meant learning two new coding environments or a framework which could publish to both.
Having played with some of the features in HTML5 I was aware it had the ability to work offline, storing data locally. So I did a bit of research and began knocking something together. I thought I’d outline some of the lessons I learned on the way.
Making a web page available offline
To make a file available offline, you need to create a cache manifest. Some of the older tutorials I found name it with a .manifest file type (e.g. offline.manifest, cache.manifest) but this didn’t work for me, you need to name it with a .appcache file extension (e.g. offline.appcache).
In there you need to reference all the files and resources you want available offline, and you can also set which files should never be cached as well as what to do if someone tries to access a page which is not available.
To make it easier, I created a completely separate file (offline.html) to do everything I wanted when there was no internet access, using Ajax, in one page.
I did add support to my .htaccess for a cache-manifest content type, but it worked fine before I had done that.
Application cache ‘gotchas’
One gotcha I found with the manifest was that my users were never going to visit the offline.html page when using the ‘live’ site, so I need to get their browser to download the necessary files when they visited any page. Initially I did this by adding the manifest reference to my shared header code, so it appeared on every page. Do no do this.
Any page you add the manifest reference to will also be cached, on top of the files in the manifest. Apart from wasting space, you’ll find that the cached version is used in preference over the ‘live’ version, so even if you do have access to the site the local cached copy is served, which means you get out-of-date pages or weird things happening (on search pages, for example). It took a bit of time for me to figure out what was going on.
To get around this I used the hidden iframe approach outlined by Jake Archibald (and that article has a good summary of all of the problems with application cache).
Another thing to look out for is if you update any of the files listed in the manifest, they won’t automatically be updated in the local cache of a visitor unless the manifest itself has been updated, so include a date/time/version reference you can update each time any of the files in the manifest is changed.
Storing data offline
For my CRM application I wanted to store a list of customers and account numbers to be selected from so as to avoid anyone entering an incorrect account number and the system not being able to find it when saving to the server. HTML5 provides a number of ways to store data offline:
- WebSQL – Well supported in the two main mobile browsers, but has been deprecated in favour of IndexedDB.
- IndexedDB – Not supported in Safari or either of the two mobile browsers.
- Web/Local Storage – Not a database like the other options, but allows storage of key-value pairs. Well supported in desktop and mobile browsers.
I’d have preferred to use one of the database solutions as I was going to need to query it, but there was no guarantee WebSQL would be supported in future versions of the two mobile clients I was interested in and with IndexedDB not yet supported I was left with little choice.
Rather than create thousands of entries, one for each customer (or whatever you’re storing), the easier approach is to store it in one entry as a JSON string. I used the same approach for storing the submitted form data for any customer notes/actions.
Note that there is a 5Mb size limit for storage (you can ask for more from the user, but only for WebSQL/IndexedDB databases) and you can only store strings, no objects or anything else (largely, though some browsers may allow).
A note on mobile UIs
Originally I planned to show the customer list as one large dropdown, but with so many entries it was unwieldy and there is no default type-to-search functionality on mobile as there is with desktop browsers. I looked at a couple of combobox approaches, which allowed searching, but I had issues with selecting entries in one or both mobile browsers I had to support.
In the end I made a text box that triggered an Ajax search when typed in (simply iterating through the customer list) and then displayed any matches below, the user then clicks to select the customer so notes/actions could be entered against them. I had a bit of fun getting the right trigger event as they seem to be handled differently by iOS and Android, in the end I settled on keydown rather than keypress.
As I said, any form submissions were saved into local storage, but I didn’t want the user to have to manually synchronise this back to the server when they were connected. Thankfully there are some events triggered when the browser detects the state of the connection, so I called a function from the ‘online’ event which looped through the locally stored items, checking if they were data for the server (as opposed to data I’d stored for the offline use) and then firing a call via Ajax to a simple API before removing the entry from the local store.
One thing to look out for is sending data that includes things like ampersands (&) in the JSON data you submit (if you don’t plan to decode first) as they’ll be interpreted as a separate variable, to avoid this you need to encode the form data before you save it.
It took a bit of trial and error as well as some head scratching, but in the end I was quite pleased with the results and the functionality I could get (albeit for a relatively simple application). It shows the power that is starting to appear in HTML5, though some locked-down standards (especially for offline databases) would be nice.
Hopefully this article will help you avoid some of the pitfalls I ran into.