Skip to content

Creating a Checklist

September 17, 2013

In comics, you go to sales and conventions with your ‘want list’, a list of all the back issues that you want to buy. I’ve seen people use all sorts of things from hand-written paper to printed output from apps to desktop apps that had a mobile app that would sync when you got back home. The most extreme I’ve seen is a guy who came with a big thick binder filled with Excel reports…this guy had a big want list.

Also, a lot of people have worked out ‘pull’ lists with stores. This is slang for setting up a store membership and giving them a list of issues that you want to buy every month. They order a copy just for you and ‘pull’ it out of the stock they receive before it goes on sale and leave it behind the counter, along with anything else on your list. You show up on when you can and buy.

Because I’ve created my own custom system for tracking my comics, I decided that writing out my want and pull lists full-hand on paper was getting old, and that I should just create a custom web-page for it. The current advances in HTML 5, particularly offline storage and the application cache make it possible to make a single web page ‘sticky’, leaving it in your list of apps and allowing you to view and work with it, even when your device isn’t connected to the internet.

So that’s what I did. The screenshots below show you how the app looks.

This is what the page looks like when it first loads…a list of comics I’m looking to buy, a tabbed interface to get to the pull screen, and some button functionality to maintain a running tally of what I’ve bought when I’m out in the field, so to speak.

need-screen

This is the pull tab, much more static then the previous screen, mainly for checking in the store if something is on my list. Note that you see the last issue I’ve bought after the title, useful for helping me determine whether I’ve bought the book on the stands or not (in an age of multiple covers and other gimmicks, it’s easy to buy the same book twice).

pull-screen

So, going about this pulled me in a lot of directions at once, but things have finally settled down, and I have a page that relies on the following:

  • HTML 5
  • CSS 3
  • XSLT/PHP and SSI on the server, with a small dash of Apache configuration
  • Javascript

A mix of PHP and SSI is strange, but I’m using PHP exclusively to generate the XSLT reports and for nothing else, so it stayed. The SSI is there strictly to add the reports and a dash of time formatting.

The usage is fairly simple: once downloaded via the application cache and stored via offline storage, the app is fairly static. When I’m at a convention and am hunting for back issues, I consult the first tab. If I actually buy a back issue on the list, I tap the number, changing the colour of the button. This tells me I’ve bought the issue when consulting it later. When I’ve finshed updating my files at home, I connect my device to the net and re-request the page, triggering the cache to refresh the page with the new data.

This seems simple enough, but I chased my tail for months (I did this in bits and pieces when I had time), doing version after version with bugs and weird behavior until I finally got all the pieces in order. The application cache was one of the larger sticking points, as detailed in this article.

I’m going to walk through the HTML and Javascript for this, and leave the CSS out…there isn’t much there worth talking about…it’s fairly standard fare.

The HTML

Here’s the HTML for the main page, with SSI intact:


<!DOCTYPE html>
<html lang="en" manifest="manifest/manifest.manifest">
<head>
	<meta charset="utf-8" />

	<meta name="viewport" content="width=device-width" />
	<meta name="apple-mobile-web-app-capable" content="yes" />
	<meta name="format-detection" content="telephone=no" />

	<link rel="icon" type="image/png" href="xtra/favicon.png" />
	<link rel="apple-touch-icon" type="image/png" href="xtra/iphone_icon.png" />
	<link rel="apple-touch-startup-image" type="image/png" href="xtra/iphone_startup.png" />

	<link rel="stylesheet" media="screen" href="xtra/want.css" />

	<title>Need it, Pull it.</title>
</head>
<body>
	<header>
		<h1>Need it, Pull it.</h1>
		<nav>
			<ul>
				<li class="need">Need it</li>
				<li class="pull">Pull List</li>
			</ul>
		</nav>
	</header>
	<section>
		<div class="bag" id="need">
			<h2>Need it</h2>
			<!--#include virtual="/XMLDatabases/transform.php?type=Comics&report=wantlist_wanted"-->
		</div>
		<div class="bag" id="pull">
			<h2>Pull List</h2>
			<!--#include virtual="/XMLDatabases/transform.php?type=Comics&report=wantlist_buylist"-->
		</div>
	</section>
	<footer>
		© <!--#config timefmt="%Y-%m-%d" -->
		<time pubdate="pubdate" datetime="<!--#echo var="DATE_LOCAL" -->">
			<!--#config timefmt="%Y" --><!--#echo var="DATE_LOCAL" -->
		</time>
		Jeff Wyonch
	</footer>

	<script type="text/javascript" src="xtra/zepto.min.js"></script>
	<script type="text/javascript" src="xtra/want.js"></script>
	<script type="text/javascript">$(document).ready(bootstrap);</script>
</body>
</html>

I have CSS at the top and Javascript at the bottom, for all the performance goodness that entails. I also had to add a meta tag to turn off auto-detection for phone numbers, because my iPod was somehow determining ‘132’ was a phone number. I also have the shiny new HTML 5 header, section, footer, and time elements, so the page is a little more semantic than the usual sea of div‘s.

The XSLT reports are fairly simple, too.

The want list outputs a series of the following:


<h3><span class="title">Akiko. V1.</span><span class="pub">Sirius.</span></h3>
<ul class="clear">
	<li>47</li>
	<li>48</li>
</ul>

And the pull list is roughly the same without the issues a tweak to the title:


<h3>
	<span class="title">X-Men. V4.</span>
	<span class="pub">Marvel.</span>
	<span class="lastBought"># 3.</span>
</h3>

You’ll also notice I used Zepto, an almost feature-complete clone of jQuery, with a seriously reduced footprint in regards to filesize. Zepto was designed for use with modern mobile devices, and drops support for IE entirely, and early versions of Firefox, Webkit, and Opera. I consider this a decent tradeoff, given all I have is an iPod Touch right now. If I really needed this to work on a Windows phone, swapping back for jQuery wouldn’t involve much effort.

Finally, at the bottom, I have a fairly standard jQuery-like onload invocation. Because I’m not doing much more than a few simple effects and localstorage checks, there isn’t really a need to do anything more complex than a simple old-school boot loader at the bottom of the page.

The Javascript

I generally start off building Javascript with either pseudo-code, a checklist of features, or both. I find it helps immensely to have a skeleton of some sort prior to rolling up your sleeves.

While the skeleton changed a dozen times or more for this app, here’s what it looked like at the end:


/*
STEPS
~~~~~~~~

Boot
*- can we run in this browser?
*- create localstorage object to store ids
*- if it already exists, toggle classes on all matched items
*- set up interface

Clicking a tab
*- standard toggle behavior

Clicking an issue
*- compare ids to localstorage
*- if issue hasn't been bought, add to localstorage and toggle to bought
*- if issue has been bought, remove from localstorage and toggle to unbought

Cache update
*- if there's an update,
	*- show message
	*- empty temp and localstorage
	*- force-reload page

*/

Anything finished gets a ‘*’ as I go along.

The Nude Bomb

Ah, how I’ve been waiting to bring up a reference to the first time Maxwell Smart was featured in a film. As a bizarre sidenote, there is a fight in an amusement park that has the Cylons from the original Battlestar Galactica. But…back to the point.

Nowadays, we’re told that if we don’t wrap our Javascript in anonymous functions the World. Will. End. Global variables are even worse…they’ll blow up the sun.

And, well…no. While larger applications need that type of organization, small ones don’t. I feel programming approaches should shape themselves to the contours and size of the task at hand.

In the case of this application, there was so little code it didn’t make sense to go the full route. I can always ramp up to something more complex later, but only as I need it.

So, you’re going to see at least one global variable and a whole bunch of naked (get it? naked?) functions sitting in the global scope here. You’ll also notice that wrapping it all up in an anonymous function bow would be fairly simple if that time ever came. If you can’t look at commando Javascript without blushing, stop reading now.

Tools first

The first thing I’m going to do is add a slew of utilities. Full disclosure: these were added as I needed them, but for the sake of expediency here, I’m going to pretend these were all here from the start.


/* General Utilities */

function isLocalStorage() {
	return ('localStorage' in window) && window['localStorage'] !== null;
}

function isApplicationCache() {
	return !!window.applicationCache;
}

function strip( string, mode ) {
	if ( !string ) return;
	var mode,
		space = /[\s]+/g,    // normalize whitespace
		lead = /^\s+/,       // trim leading whitespace
		trail = /\s+$/,      // trim trailing whitespace
		ends = /^\s+|\s+$/g; // trim leading/trailing whitespace
	switch( mode ) {
		case 'T': string = string.replace( ends, '' );   break;
		case 'L': string = string.replace( lead, '' );   break;
		case 'R': string = string.replace( trail, '' );  break;
		case 'S': string = string.replace( space, ' ' ); break;
		default : string = string.replace( space, ' ' ).replace( ends, '' );
	}
	return string;
}

function echo( string, mode ) {
	if ( !string ) return;
	if ( !mode && typeof console == 'undefined' ) mode = 'A';
	switch( mode ) {
		case 'W': document.write( string ); break;
		case 'A': alert( string );          break;
		default : console.log( string );
	}
}

The first two functions are copied verbatim from Mark Pilgrim’s amazing Dive into HTML 5 site. These are the only two checks I need to make to ensure the browser can run the code, for now. If I port the app to things other than an iPod, I can always add Modernizr later. For efficiency’s sake, this is all I need for now.

The strip and echo functions are things I wrote myself, and while there’s no brain surgery here, I use them over and over in projects. I prefer to have all my regular expressions in variables so I can comment them (this gives my brain a mental kick when I come back to them later). While both of these functions can be whittled down to only the functionality I need, I use them so often I just paste them in to my code.

Boot to the head

Every time I code a bootstrap function, The Frantics play in my head. I’m going to declare my single global variable, and my bootstrap function next.


var temp = '';

function bootstrap() {
	if(capable()) {
		/* hide interface till ready */
		$('section').children().hide();

		/* set up localstorage  */
		setLocal();

		/* assign listeners */
		$('header nav ul li').click(showTab);
		$('#need').delegate('ul li','click',processIssue);

		/* all is ready, show interface */
		$('section #need').show();
	} else {
		return;
	}
}

Again, no brain surgery. The lone global var here is really just a space to hold the only variable I’m actually going to store in localstorage, so I can manipulate it in memory while the app’s running.

The bootstrap function takes care of running the compatibility check right at the start, then sets up the environment and checks the state of the app (have I actually bought any comics). I’m going to walk through these in blocks, starting with the compatibility check first.

Can we even run this thing?

The app is so small that I really only needed to check whether it could be run once.


function capable() {
	/* return boolean if browser can run the app */
	var ready = false, local = isLocalStorage(),
	cache = isApplicationCache();
	if (local && cache) ready = true;
	return ready;
}

All this does is return true or false. The bootstrap function either quits or continues to run. Easy peasy. This streamlines all the remaining code, giving us a filesize savings. Scaling up from here can only go so far…at some point, this would have to check for only the really large pieces of functionality needed, or be refactored.

Setting up localstorage

After making sure all the tabs are hidden until the interface is set up, the next step is determining the state of the application: is the localstorage variable we need present and does it have content? If it does, we need to make sure issues that have been bought are displayed properly in the interface.


function setLocal() {
	if (localStorage['bought'] == undefined) {
		localStorage['bought'] = '';
	} else {
		temp = localStorage['bought'];
		if (localStorage['bought'] != '') {
			paintIssues();	
		}
	}
}

function paintIssues() {
	var issues = localStorage['bought'].split(' '),
		i, loop = issues.length, item;
	for (i = 0; i < loop; i++) {
		item = parseInt(issues[i]);
		$("#need li").eq(item).toggleClass('bought');
	}
}

At this point, to understand what’s going on here, I need to explain what’s being stored in localstorage. A string. That’s it. That’s all I need.

Every time I tap an issue, all I’m doing is storing the DOM position of what was tapped as a space-seperated integer inside a string. The bought variable inside localstorage looks something like 0 5 9 12 22.

I can hear people’s teeth grinding right now…he’s using the DOM POSITION?!! Well…yeah. Remember, the app, once downloaded, doesn’t change until it’s refreshed only after I’ve updated the files back at the server. So, the DOM here is stable until the application cache signals an update, at which point locastorage is wiped anyway.

This is the only loop in the whole application. One of the great things about localstorage variables is that they can only be strings, so you can manipulate them with the built-in string functions in Javascript. As a sidenote, I know I could use the ECMAScript trim method instead of my own utility, but using a custom function helps scale this if I need it to work on other platforms with older versions of Javscript installed.

Making issues tapdance

The next part of bootstrap sets up listeners for the tabs and delegates listeners for all the issue buttons. Again, I’m using the DOM position to create a unique ID for each issue. On tap, the issue button’s display state toggles to either bought or unbought, and the change is stored in localstorage. The temp global var is also updated.


function processIssue() {
	var no = $('#need li').index(this);
	$(this).toggleClass('bought');
	if ( $(this).attr('class') == 'bought') {
		store(no);
	} else {
		remove(no);
	}
}

function store(no) {
	var bool = there(no);
	if ( bool == false ) {
		temp = temp + ' ' + no;
		temp = strip(temp);
		localStorage['bought'] = temp;
	}
}

function remove(no) {
	var bool = there(no);
	if ( bool == true ) {
		temp = temp.replace( no, '' );
		temp = strip(temp);
		if (temp == undefined) temp = '';
		localStorage['bought'] = temp;
	}
}

function there(no) {
	if (temp.indexOf( no ) == -1) {
		return false;
	} else {
		return true;
	}
}

While I could have incorporated the logic of the there method into the two preceding functions, that’s code duplication, which I try to avoid whenever possible. And yes, I could’ve used a ternary operater there (and in several other places), but I find them hard to read.

And…that’s about it

Well, almost. I still have to add the last function to toggle the main pull and need tabs.


function showTab() {
	$('section .bag').hide();
	$('#' + $(this).attr('class')).toggle();
	return false;
}

And…now it’s done. The whole Javascript library (apart from Zepto) clocks in at about 166 lines (including the huge comment at the start) and roughly 3K before minimization.

Adding the cache manifest

The last step to making this all work offline is adding the cache manifest. In order to get this working, I had to add some Apache configuration; first, to get Apache to serve the .manifest filetype with the correct MIME type, and then make it parsable via SSI. Here’s the manifest.manifest file:


CACHE MANIFEST

CACHE:
../xtra/favicon.png
../xtra/iphone_icon.png
../xtra/iphone_startup.png
../xtra/zepto.min.js
../xtra/want.css
../xtra/want.js

<!--#include virtual="/XMLDatabases/transform.php?type=Comics&report=cachebust"-->

The SSI include here adds a few lines to the end of the manifest file:


# Wanted : 347
# Series Wanted : 38 
# Buylist : 41

These are comments meant to do only one thing: make the browser aware that the cache manifest file has changed. Even if every file listed in that manifest has changed, unless the manifest file itself changes, the browser won’t signal an update. Because these numbers will change when I update files at the server, this accomplishes the goal with the minimum of fuss.

App updates

As it turns out, there’s one last bit of Javascript we need to add. Now that the manifest file is in place, and will alert the browser to an update, we need to make sure the user applies the update. All that happens when the cache manifest changes is an internal event…unless you program a response to the event, the browser will not download new files and update the app.

The last bit of code was found on the net in quite a few tutorials, and even though it doesn’t match the programming style of the rest of the Javascript, it’s workable for now.


window.applicationCache.addEventListener('updateready', function(e) {
	if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
		window.applicationCache.swapCache();
		if (confirm('A new version of this page is available. Load it?')) {
			temp = '', localStorage['bought'] = '';
			window.location.reload();
		}
	} else {
		// Manifest didn't change.
	}
}, false);

After I come home from a hard day of conventioning, I update my files with the back-issue goodies I bought, triggering the manifest to change. When I load the page on my iPod while it’s connected, it triggers the updateready event, which brings up a dialog to reload the app. If I click ‘yes’, the localstorage and global variables are reset to empty strings and the page reloads with the newly downloaded appcache files.

Last thoughts

I learned quite a bit about working with offline apps, but know I still have a long way to go. Some of the resources linked to in the article will help with general HTML 5 app stuff.

Unfortunately, the only way to add the code to github would be to add the megabytes of XML and XSLT files that support it underneath, and I don’t think those would be an appropriate use of github.

There are a lot of general tweaks that could be made to this, and I’ll probably revisit it in the future.

I’d love to hear feedback regarding this. Remember to be gentle in the comments area.

Advertisements
No comments yet

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: