Developing with HTML5, CoffeeScript and Twitter's Bootstrap
This article is the fourth in a series about my adventures developing a Fitness Tracking application with HTML5, Play Scala, CoffeeScript and Jade. Previous articles can be found at:
- Integrating Scalate and Jade with Play 1.2.3
- Trying to make CoffeeScript work with Scalate and Play
- Integrating HTML5 Boilerplate with Scalate and Play
Developing Features
After getting my desired infrastructure setup, I started coding like a madman. The first feature I needed was a stopwatch to track the duration of a workout, so I started writing one with CoffeeScript. After spending 20 minutes playing with dates and setTimeout, I searched and found a stopwatch jQuery plug-in. I added this to my app, deployed it to Heroku, brought up the app on my iPhone 3G, clicked Start and started riding my bike to work.
When I arrived, I unlocked my phone and discovered that the time had stopped. At first, I thought this was a major setback. My disappointed disappeared when I found a Super Neat JavaScript Stopwatch and Kåre Byberg's version that worked just fine. This stopwatch used setTimeout, so by keeping the start time, the app on the phone would catch up as soon as you unlocked it. I ported Kåre's script to CoffeeScript and rejoiced in my working stopwatch.
# Created by Kåre Byberg © 21.01.2005. Please acknowledge if used # on other domains than http://www.timpelen.com. # Ported to CoffeeScript by Matt Raible. Also added hours support. flagClock = 0 flagStop = 0 stopTime = 0 refresh = null clock = null start = (button, display) -> clock = display startDate = new Date() startTime = startDate.getTime() if flagClock == 0 $(button).html("Stop") flagClock = 1 counter startTime, display else $(button).html("Start") flagClock = 0 flagStop = 1 counter = (startTime) -> currentTime = new Date() timeDiff = currentTime.getTime() - startTime timeDiff = timeDiff + stopTime if flagStop == 1 if flagClock == 1 $(clock).val formatTime timeDiff, "" callback = -> counter startTime refresh = setTimeout callback, 10 else window.clearTimeout refresh stopTime = timeDiff formatTime = (rawTime, roundType) -> if roundType == "round" ds = Math.round(rawTime / 100) + "" else ds = Math.floor(rawTime / 100) + "" sec = Math.floor(rawTime / 1000) min = Math.floor(rawTime / 60000) hour = Math.floor(rawTime / 3600000) ds = ds.charAt(ds.length - 1) start() if hour >= 24 sec = sec - 60 * min + "" sec = prependZeroCheck sec min = min - 60 * hour + "" min = prependZeroCheck min hour = prependZeroCheck hour hour + ":" + min + ":" + sec + "." + ds prependZeroCheck = (time) -> time = time + "" # convert from int to string unless time.charAt(time.length - 2) == "" time = time.charAt(time.length - 2) + time.charAt(time.length - 1) else time = 0 + time.charAt(time.length - 1) reset = -> flagStop = 0 stopTime = 0 window.clearTimeout refresh if flagClock == 1 resetDate = new Date() resetTime = resetDate.getTime() counter resetTime else $(clock).val "00:00:00.0" @StopWatch = { start: start reset: reset }
The Scalate/Jade template to render this stopwatch looks as follows:
script(type="text/javascript" src={uri("/public/javascripts/stopwatch.coffee")}) #display input(id="clock" class="xlarge" type="text" value="00:00:00.0" readonly="readonly") #controls button(id="start" type="button" class="btn primary") Start button(id="reset" type="button" class="btn :disabled") Reset :plain <script type="text/coffeescript"> $(document).ready -> $('#start').click -> StopWatch.start this, $('#clock') $('#reset').click -> StopWatch.reset() </script>
Next, I wanted to create a map that would show your location. For this, I used Merge Design's HTML 5 Geolocation Demo as a guide. The HTML5 Geo API is pretty simple, containing only three methods:
// Gets the users current position navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options); // Request repeated updates of position watchId = navigator.geolocation.watchPosition(successCallback, errorCallback); // Cancel the updates navigator.geolocation.clearWatch(watchId);
After rewriting the geolocation example in CoffeeScript, I ended up with the following code in my map.coffee script. You'll notice it uses Google Maps JavaScript API to show an actual map with a marker.
# Geolocation with HTML 5 and Google Maps API based on example from maxheapsize: # http://maxheapsize.com/2009/04/11/getting-the-browsers-geolocation-with-html-5/ # This script is by Merge Database and Design, http://merged.ca/ -- if you use some, # all, or any of this code, please offer a return link. map = null mapCenter = null geocoder = null latlng = null timeoutId = null initialize = -> if Modernizr.geolocation navigator.geolocation.getCurrentPosition showMap showMap = (position) -> latitude = position.coords.latitude longitude = position.coords.longitude mapOptions = { zoom: 15, mapTypeId: google.maps.MapTypeId.ROADMAP } map = new google.maps.Map(document.getElementById("map"), mapOptions) latlng = new google.maps.LatLng(latitude, longitude) map.setCenter(latlng) geocoder = new google.maps.Geocoder() geocoder.geocode({'latLng': latlng}, addAddressToMap) addAddressToMap = (results, status) -> if (status == google.maps.GeocoderStatus.OK) if (results[1]) marker = new google.maps.Marker({ position: latlng, map: map }) $('#location').html('Your location: ' + results[0].formatted_address) else alert "Sorry, we were unable to geocode that address." start = -> timeoutId = setTimeout initialize, 500 reset = -> if (timeoutId) clearTimeout timeoutId @Map = { start: start reset: reset }
The template to show the map is a mere 20 lines of Jade:
script(type="text/javascript" src="//www.google.com/jsapi") script(type="text/javascript" src="//maps.googleapis.com/maps/api/js?sensor=false") :css .demo-map { border: 1px solid silver; height: 200px; margin: 10px auto; width: 280px; } #map(class="demo-map") p(id="location") span(class="label success") New | Fetching your location with HTML 5 geolocation... script(type="text/javascript" src={uri("/public/javascripts/map.coffee")}) :javascript Map.start();
The last two features I wanted were 1) distance traveled and 2) drawing the route taken on the map. For this I learned from A Simple Trip Meter using the Geolocation API. As I was beginning to port the JS to CoffeeScript, I thought, "there's got to be a better way." I searched and found Js2coffee to do most of the conversion for me. If you know JavaScript and you're learning CoffeeScript, this is an invaluable tool.
I tried out the trip meter that evening on a bike ride and noticed it said I'd traveled 3 miles when I'd really gone 6. I quickly figured out it was only calculating start point to end point and not taking into account all the turns in between. To view what was happening, I integrated my odometer.coffee with my map using Google Maps Polylines. Upon finishing the integration, I discovered two things, 1) HTML5 geolocation was highly inaccurate and 2) geolocation doesn't run in the background.
I was able to solve the first problem by passing in {enableHighAccuracy: true} to navigator.geolocation.watchPosition(). Below are two screenshots showing before high accuracy and after. Both screenshots are from the same two-block walk.
The second issue is a slight show-stopper. PhoneGap might be able to solve the problem, but I'm currently using a workaround → turning off auto-lock and keeping Safari in the foreground.
Making it look good
After I got all my desired features developed, I moved onto making the app look good. I started by using SASS for my CSS and installed Play's SASS module. I then switched to LESS when I discovered and added Twitter's Bootstrap to my project. At first I used Play's LESS module (version 0.3), but ran into compilation issues. I then tried Play's GreenScript module, but gave up on it when I found it was incompatible with the CoffeeScript module. Switching back to the LESS module and using the "0.3.compatibility" version solved all remaining issues.
You might remember that I integrated HTML5 Boilerplate and wondering why I have both Bootstrap and Boilerplate in my project. At this point, I don't think Boilerplate is needed, but I've kept it just in case it's doing something for HTML5 cross-browser compatibility. I've renamed its style.css to style.less and added the following so it has access to Bootstrap's variables.
/* Variables from Bootstrap */ @import "libs/variables.less";
Then I made my app look a lot better with layouts, stylish forms, a fixed topbar and alerts. For example, here's the CoffeeScript I wrote to display geolocation errors:
geolocationError = (error) -> msg = 'Unable to locate position. ' switch error.code when error.TIMEOUT then msg += 'Timeout.' when error.POSITION_UNAVAILABLE then msg += 'Position unavailable.' when error.PERMISSION_DENIED then msg += 'Please turn on location services.' when error.UNKNOWN_ERROR then msg += error.code $('.alert-message').remove() alert = $('<div class="alert-message error fade in" data-alert="alert">') alert.html('<a class="close" href="#">×</a>' + msg); alert.insertBefore($('.span10'))
Then I set about styling up the app so it looked good on a smartphone with CSS3 Media Queries. Below is the LESS code I used to hide elements and squish the widths for smaller devices.
@media all and (max-device-width: 480px) { /* hide scrollbar on mobile */ html { overflow-y:hidden } /* hide sidebar on mobile */ .home .span4, .home .page-header, .topbar form { display: none } .home .container { width: 320px; } .about { .container, .span10 { width: 280px; } .span10 { padding-top: 0px; } }
Tools
In the process of developing a stopwatch, odometer, displaying routes and making everything look good, I used a number of tools. I started out primarily with TextMate and its bundles for LESS, CoffeeScript and Jade. When I started writing more Scala, I installed the Scala TextMate Bundle. When I needed some debugging, I switched to IntelliJ and installed its Scala plugin. CoffeeScript, LESS and HAML plugins (for Jade) were already installed by default. I also used James Ward's Setup Play Framework with Scala in IntelliJ.
Issues
I think it's obvious that my biggest issue so far is the fact that a webapp can't multitask in the background like a native app can. Beyond that, there's accuracy issues with HTML5's geolocation that I haven't seen in native apps.
I also ran into a caching issue when calling getCurrentPosition(). It only worked the first time and I had to refresh my browser to get it to work again. Strangely enough, this only happened on my desktop (in Safari and Firefox) and worked fine on my iPhone. Unfortunately, it looks like PhoneGap has issues similar to this.
My workaround for no webapp multitasking is turning off auto-lock and leaving the browser in the foreground while I exercise. The downside to this is it really drains the battery quickly (~ 3 hours). I constantly have to charge my phone if I'm testing it throughout the day. The testing is a real pain too. I have to deploy to Heroku (which is easy enough), then go on a walk or bike ride. If something's broke, I have to return home, tweak some things, redeploy and go again. Also, there's been a few times where Safari crashes halfway through and I lose all the tracking data. This happens with native apps too, but seemingly not as often.
If you'd like to try the app on your mobile phone and see if you experience these issues, checkout play-more.com.
Summary
Going forward, there's still more HTML5 features I'd like to use. In particular, I'd like to play music while the fitness tracker is running. I'd love it if cloud music services (e.g. Pandora or Spotify) had an API I could use to play music in a webapp. Soundcloud might be an option, but I've also thought of just uploading some MP3s and playing them with the <audio> tag.
I've really enjoyed developing with all these technologies and haven't experienced much frustration so far. The majority has come from integrating Scalate into Play, but I've resolved most problems. Next, I'll talk about how I've improved Play's Scalate support and my experience working with Anorm.