jQuery Mobile - adding Local Storage
So about a week or so ago I had an idea about a simple jQuery Mobile application that would make use of Local Storage. That was a week ago. Turns out - the "simple" application turned out to be a royal pain in the rear. Not because of Local Storage, but because of some misconceptions and lack of knowledge on my part in jQuery Mobile. What followed was a couple painful days (and more than a few curse words) but after all of that, I feel like I've got a better understanding of jQuery Mobile and got to play with some new features. So with that being said, let's get to the app.
My idea was a rather simple one. Given a collection of art, allow the user to browse categories and view individual pieces of art. I've done this before as a jQuery Mobile example. But what I thought would be interesting is to add a simple "Favorites" system. As you browse through the art you can select a piece you like, add it to your favorites, and then later have a quicker way to access them. To make things even more interesting, I thought I'd make use of Local Storage. Local Storage is an HTML5 feature, and unfortunately, it isn't quite as sexy as Canvas so it doesn't get as many cool demos. But it's one of those - you know - useful things that is actually pretty well supported. Local Storage is basically a key system of data. You can store, on the browse, a key and a value. Like name="Raymond". Unlike cookies, this data is not sent to the server on every request. Rather, it just sits there on the client ready to be used by JavaScript. You've got access to both a permanent (localStorage) and session based (sessionStorage) API. The excellent DiveIntoHTML5 talks about Local Storage here. I won't talk any more about the API as it's rather quite simple and the Dive site explains it more than well enough. Before getting into this version though, let's quickly look at the initial, simpler version.
My application consists of three HTML files, all powered by ColdFusion. The home page will list categories, the category page will list art, and the detail page will show just an art piece. Let's start with the index page.
2
3<!DOCTYPE html>
4<html>
5 <head>
6 <title>Art Browser</title>
7 <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.css" />
8 <script src="http://code.jquery.com/jquery-1.6.1.min.js"></script>
9 <script src="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.js"></script>
10</head>
11<body>
12
13<div data-role="page">
14
15 <div data-role="header">
16 <h1>Art Browser</h1>
17 </div>
18
19 <div data-role="content">
20 <ul data-role="listview" data-inset="true">
21 <cfoutput query="categories">
22 <li><a href="category.cfm?id=#mediaid#&media=#urlEncodedFormat(mediatype)#">#mediatype#</a> <span class="ui-li-count">#total#</span></li>
23 </cfoutput>
24 </ul>
25 </div>
26
27</div>
28
29</body>
30</html>
Note that I begin by asking for media types. Our database categories art by a media type and I'll be considering that my categories. The getMediaTypes method returns a query which means I can simply loop over it in my content.

Next up we have the category page - which is really just a slightly different version of the last one. Note though the use of the Home icon.
2<cfparam name="url.id" default="">
3<cfset art = application.artservice.getArt(mediatype=url.id)>
4
5<!DOCTYPE html>
6<html>
7 <head>
8 <cfoutput><title>Art Category - #url.media#</title></cfoutput>
9 <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.css" />
10 <script src="http://code.jquery.com/jquery-1.6.1.min.js"></script>
11 <script src="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.js"></script>
12</head>
13<body>
14
15<div data-role="page">
16
17 <cfoutput>
18 <div data-role="header">
19 <a href="index.cfm" data-icon="home" data-iconpos="notext">Home</a>
20 <h1>#url.media#</h1>
21 </div>
22 </cfoutput>
23
24 <div data-role="content">
25 <cfif art.recordCount>
26 <ul data-role="listview" data-inset="true">
27 <cfoutput query="art">
28 <li><a href="art.cfm?id=#artid#">#artname#</a></li>
29 </cfoutput>
30 </ul>
31 <cfelse>
32 Sorry, no art in this category.
33 </cfif>
34 </div>
35
36
37</div>
38
39</body>
40</html>

And finally, let's look at our detail page.
2<cfset art = application.artservice.getArtPiece(url.id)>
3
4<!DOCTYPE html>
5<html>
6 <head>
7 <cfoutput><title>Art - #art.name#</title></cfoutput>
8 <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.css" />
9 <script src="http://code.jquery.com/jquery-1.6.1.min.js"></script>
10 <script src="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.js"></script>
11</head>
12<body>
13
14<div data-role="page">
15
16 <cfoutput>
17 <div data-role="header">
18 <a href="index.cfm" data-icon="home" data-iconpos="notext">Home</a>
19 <h1>#art.name#</h1>
20 </div>
21 </cfoutput>
22
23 <cfoutput>
24 <div data-role="content">
25 <b>Artist: </b> #art.artist#<br/>
26 <b>Price: </b> #dollarFormat(art.price)#<br/>
27 #art.description#
28 <p/>
29 <img src="#art.image#">
30 </div>
31 </cfoutput>
32
33</div>
34
35</body>
36</html>
This page is even simpler. We just get the art detail and render it within the page. Nothing fancy at all - not yet anyway.

You can demo this here: http://www.coldfusionjedi.com/demos/artbrowser/v1/
Ok, ready to go crazy? I decided on two main changes to my application. First, art pieces would have a new button, Add to Favorites (or Remove from Favorites). Once clicked, I'd use a jQuery Mobile dialog to prompt the user if they were sure. (Normally I hate crap like that. Don't second guess me. But I wanted to try dialogs in jQuery Mobile.) If the user confirms the action, I then simply update local storage to store the value. Since you can only store simple values, I used built in JSON features to store complex data about the art piece (really just the ID and name).
On the home page, I had, what I thought, was a simple thing to do. When the page loads, simply fill out a dynamic list of the user favorites. Here's where things really took a turn for the worst for me. I want to give a huge shout out to user aaraonpadoshek who helped me out on the jQuery Mobile forums. I'll show the new home page and explain what changed.
2
3<!DOCTYPE html>
4<html>
5 <head>
6 <meta name="viewport" content="width=device-width, initial-scale=1">
7 <title>Art Browser</title>
8 <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.css" />
9 <script src="http://code.jquery.com/jquery-1.6.1.min.js"></script>
10 <script src="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.js"></script>
11 <script>
12 //Credit: http://diveintohtml5.org/storage.html
13 function supports_html5_storage() {
14 try {
15 return 'localStorage' in window && window['localStorage'] !== null;
16 } catch (e) {
17 return false;
18 }
19 }
20 function supports_json() {
21 try {
22 return 'JSON' in window && window['JSON'] !== null;
23 } catch (e) {
24 return false;
25 }
26 }
27
28 $(document).ready(function() {
29
30 //only bother if we support storage
31 if (supports_html5_storage() && supports_json()) {
32
33 //when art detail pages load, show button
34 $('div.artDetail').live('pageshow', function(event, ui){
35 //which do we show?
36 var id = $(this).data("artid");
37 if (!hasInStorage(id)) {
38 $(".addToFavoritesDiv").show();
39 $(".removeFromFavoritesDiv").hide();
40 }
41 else {
42 $(".addToFavoritesDiv").hide();
43 $(".removeFromFavoritesDiv").show();
44 }
45 });
46
47 //When clicking the link in details pages to add to fav
48 $(".addToFavoritesDiv a").live('vclick', function(event) {
49 var id=$(this).data("artid");
50 $.mobile.changePage("addtofav.cfm", {role:"dialog",data:{"id":id}});
51 });
52
53 //When clicking the link in details pages to add to fav
54 $(".removeFromFavoritesDiv a").live('vclick', function(event) {
55 var id=$(this).data("artid");
56 $.mobile.changePage("removefromfav.cfm", {role:"dialog",data:{"id":id}});
57 });
58
59 //When confirming the add to fav
60 $('.addToFavoritesButton').live('vclick', function(event, ui){
61 var id=$(this).data("artid");
62 var label=$(this).data("artname");
63 addToStorage(id,label);
64 $("#addToFavoritesDialog").dialog("close");
65 });
66
67 //When confirming the remove from fav
68 $('.removeFromFavoritesButton').live('vclick', function(event, ui){
69 var id=$(this).data("artid");
70 var label=$(this).data("artname");
71 removeFromStorage(id,label);
72 $("#removeFromFavoritesDialog").dialog("close");
73 });
74
75
76 $('#homePage').live('pagebeforeshow', function(event, ui){
77 //get our favs
78 var favs = getStorage();
79 var $favoritesList = $("#favoritesList");
80 if (!$.isEmptyObject(favs)) {
81 if ($favoritesList.size() == 0) {
82 $favoritesList = $('<ul id="favoritesList" data-inset="true"></ul>');
83
84 var s = "<li data-role=\"list-divider\">Favorites</li>";
85 for (var key in favs) {
86 s+= "<li><a href=\"art.cfm?id="+key+"\">"+favs[key]+"</a></li>";
87 }
88 $favoritesList.append(s);
89 $("#homePageContent").append($favoritesList);
90 $favoritesList.listview();
91 } else {
92 $favoritesList.empty();
93 var s = "<li data-role=\"list-divider\">Favorites</li>";
94 for (var key in favs) {
95 s+= "<li><a href=\"art.cfm?id="+key+"\">"+favs[key]+"</a></li>";
96 }
97 $favoritesList.append(s);
98 $favoritesList.listview("refresh");
99 }
100 } else {
101 // remove list if it exists and there are no favs
102 if($favoritesList.size() > 0) $favoritesList.remove();
103 }
104 });
105
106 //Adding to storage
107 function addToStorage(id,label){
108 if (!hasInStorage(id)) {
109 var data = getStorage();
110 data[id] = label;
111 saveStorage(data);
112 }
113 }
114
115 //loading from storage
116 function getStorage(){
117 var current = localStorage["favorites"];
118 var data = {};
119 if(typeof current != "undefined") data=window.JSON.parse(current);
120 return data;
121 }
122
123 //Checking storage
124 function hasInStorage(id){
125 return (id in getStorage());
126 }
127
128 //Adding to storage
129 function removeFromStorage(id,label){
130 if (hasInStorage(id)) {
131 var data = getStorage();
132 delete data[id];
133 console.log('removed '+id);
134 saveStorage(data);
135 }
136 }
137
138 //save storage
139 function saveStorage(data){
140 console.log("To store...");
141 console.dir(data);
142 localStorage["favorites"] = window.JSON.stringify(data);
143 }
144
145 }
146
147 });
148 </script>
149
150</head>
151<body>
152
153<div data-role="page" id="homePage">
154
155 <div data-role="header">
156 <h1>Art Browser</h1>
157 </div>
158
159 <div data-role="content" id="homePageContent">
160 <ul data-role="listview" data-inset="true">
161 <cfoutput query="categories">
162 <li><a href="category.cfm?id=#mediaid#&media=#urlEncodedFormat(mediatype)#">#mediatype#</a> <span class="ui-li-count">#total#</span></li>
163 </cfoutput>
164 </ul>
165
166 </div>
167
168</div>
169
170</body>
171</html>
Ok - a bit more going on here. I'll take it step by step. On top I've got two utility functions taken based on code from the DiveIntoHTML5 site. One checks for local storage support and one for JSON. It's probably overkill for mobile, but it doesn't hurt. Notice that I check both of these functions before I do anything else. It occurs to me that I wrapped up a lot of code in that IF and I should have simply exited the document.ready event handler instead.
I begin by using the "pageshow" event for my art detail page to decide if I should show the "Add to" or "Remove from" buttons. The hasInStorage function is defined later on and is just a utility I wrote for my code to quickly see if a particular art piece is favorited. I'll show that art page in a bit so you can see the HTML differences.
The next two functions listen for clicks on the new buttons. Notice the "vclick" listener. This is not - as far as I know - actually documented. At least 5 of my gray hairs this week came from this. Apparently this is the new way to listen in for click events on multiple devices. It's in the jQuery Mobile blog, but again, it isn't documented. When I went live and tested my code, it had worked fine in Chrome but not at all in iOS or Android. Apparently this is why. Very frustrating! Notice - when you click, I use the built in changePage utility to load a page. But this is the cool thing - I can turn this into a dialog by adding a role attribute. So basically - addtofav.cfm and removefromfav.cfm are normal pages - but because of how I tell jQuery Mobile to load them, turn turn into dialogs. Sweet.
Moving down - the next two event handlers are for the actual confirmations. Nothing special there. They call my utility functions defined later on to change local storage values.
Ok - so here is the part I really struggled with and where aaraonpadoshek helped. I needed a way to say, "When the page loads, write out the list." Unfortunately, the pageshow event, which runs every time, also runs before the page initializes. Read that again - it runs every time the page shows and also before it's even fully drawn. There's a pageinit method which does run after the page initializes but only runs once. So when I used pageshow and tried to change my list, I got an error because jQuery Mobile hadn't added the magical unicorn dust yet to make it pretty. When I used pageinit it worked... once. Here's where Aaron's code helped. Notice we have pagebeforeshow being listened for now. It now detects in the list exists in the DOM. If it doesn't, we create it and initialize it ourselves as a list view. If it does exist, we update it using refresh. I'll be honest and say this still is a bit... fuzzy... in my mind. But it works! And that's good enough for me. I've got a bit of DRY going on there with the display but I'll fix that later.
Moving down - you can now see my functions for working with local storage. To be honest, it's all pretty trivial. I've got a function to add and remove, to check for existence, and to get and persist. I added wrappers for them because I'm using JSON to store the data. Now let's look at the update to art.cfm:
2<div class="removeFromFavoritesDiv" style="display:none"><a href="" data-role="button" data-artid="#art.id#">Remove from Favorites</a></div>
That's the two buttons. Notice they are both hidden by default. Also note the use of data-artid to store in the primary key I'll use later. Now let's look at addtofav.cfm. I won't bother with the remove as it's pretty much the same.
2<cfset art = application.artservice.getArtPiece(url.id)>
3
4<!DOCTYPE html>
5<html>
6 <head>
7 <title>Add to Favorites?</title>
8 <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.css" />
9 <script src="http://code.jquery.com/jquery-1.6.1.min.js"></script>
10 <script src="http://code.jquery.com/mobile/1.0b1/jquery.mobile-1.0b1.min.js"></script>
11</head>
12<body>
13
14<div data-role="page" id="addToFavoritesDialog">
15
16 <div data-role="header">
17 <h1>Add to Favorites?</h1>
18 </div>
19
20 <div data-role="content">
21 <p>
22 <cfoutput>
23 <a href="" data-role="button" data-theme="b" data-artid="#url.id#" data-artname="#art.name#" class="addToFavoritesButton">Yes!</a>
24 <a href="art.cfc?id=#url.id#" data-rel="back" data-role="button">No thank you</a>
25 </cfoutput>
26 </p>
27 </div>
28
29</div>
30
31</body>
32</html>
Nothing fancy here either. Just simple content with some buttons. Here's a shot of the art view:

And here's a shot of the dialog.

And finally - the new home page:

Whew! Done. By the way, I'll also point out another issue I had. When I first tested on a mobile device, the text was incredibly small. I got a nice tweet from @jquerymobile pointing out that in beta1, you need to include a new meta tag in your page templates:
Adding that helped right away. Ok - that's it. I've included a zip below and you can play with this yourself via the uber Demo button. Enjoy.

https://gist.github.com/1081987/f6203727eb979c715f...
I thought there was a way to do a diff on gists.github.com but forgot how (if it existed)
FYI (to all) - about to go on a 9 day vacation so may be a bit slow to respond.
doesn't work for me when I first visit the page, the favourites list doesn't render. If I go back to the home page from an internal page, the favourites list does render and works beautifully from then onwards. Is there a trick to pagebeforeshow when first landing on the page?
Good work anyway
First off - "simple JS" - my solution does use JavaScript. So that doesn't make sense.
Secondly - "webdb/indexdb" - first off - indexdb/webdb isn't supported as well as local storage. Secondly, the feature doesn't make sense for what I'm doing. You would use indexdb for storing lots of data that needs a SQL-style search interface. My need was to store a few things that I'd always fetch directly. I'm not saying you _couldn't_ use indexdb here, but I really believe local storage makes more sense.
To your last point about needing N pages for the detail. Technically, you do not. See: http://jquerymobile.com/demos/1.0/docs/pages/page-...
http://jquerymobile.com/demos/1.0/docs/pages/page-...
It's the page before the one I sent. Sorry.
invalid 'in' operand getStorage()
[Break On This Error]
return (id in getStorage());
Not sure if my install of firefox may have an issue or if you are able to replicate this.
You can probably change:
function hasInStorage(id){
return (id in getStorage());
}
to this:
function hasInStorage(id){
var current = getStorage();
return current.hasOwnProperty((id);
}
I'm writing this just in the comment field so it may not be exactly right.
Tried that, but says current is NULL. I'm seeing the same issue when trying your demo in firefox.
I started down this path because the "Add to Favorites" and "Remove" buttons don't render so it led me to: hasInStorage(id).
Are you able to see something different? Does the demo work in firefox on your end? It works perfectly in chrome and safari on my end.
http://www.coldfusionjedi.com/demos/artbrowser/v5
Just found this plug-in too that helps for checking if your keys are stored: https://addons.mozilla.org/en-US/firefox/addon/fir...
As an FYI, there is a Chrome extension for LocalStorage viewing too. One I worked on. :)
https://chrome.google.com/webstore/detail/bpidlidm...
Hey, have you tested this on IE? The fav buttons render and I can see that localStorage and JSON are supported in the latest version of IE, but IE does a full page refresh when navigating from the addtofav dialog screen to the categories. I'm guessing it handles jQuery mobile a bit differently and this can most likely be alleviated with some type of $.mobile.changePage() update.
I adapted your favorites example and implemented in an app I'm building for a conference: http://gt12.adlnet.mobi
If you go the the schedule and drill down to the sessions you'll see where I incorporated it. Pretty sweet! Do you have an inkling of why it might not work on Windows Phone? The lates OS supports localStorage.
Do you have a sample code for the application.
Nice tutorial! I have a doubt. Is there a way to copy from Android clipboard to localStorage using jQuery ou native javascript?
Thanks
I just new at this and I'm working on an new app
and I wonder where you can change to categoryname to something else?
For example "Jewelry", "Painting" and so on...
I didn't found It anywhere in your sample cfm files. Where is those categories saved?
And how do I change it?
I would appreciate it if you could light me in this matter. :)
not sure how it works together with jqm
jqm i know a bit more about
I wanted to make favorites on my mobile app and i need it to be local in the phone
ive been searching the web for a good solution and yours were the closest to my idea
i simply have learn how this works
any chance you could share this favorites files to cf ?
if yes would you please send to my mail?
I would be deeply grateful and maybe I can put thanks in the app info with your name if thats ok ? :)