Making the Toast and Tea
Going Mobile with pyramid_jqm
Tres Seaver
Moving from traditional web development to mobile development is hard.
Re-using what we as web developers know can ease the pain.
pyramid_jqm capitalizes on that reuse
... especially compared to traditional web development
Ugly new failure modes
Livelocks and deadlocks and netsplits, oh my!
Your bread-and-butter skills should be transferable:
Generating "customized" HTML is natural
Javscript can put "lively" UX in play
... even for curmudgeons like me
Caching and other tuning mechanisms are familiar ground.
Apps run using "stock" browser components.
Testable on desktops (but watch out for performance!)
Mark Pilgrim's "Dive into HTML5" is the essential docs.
(OK, not really HTML5)
But JSON rocks!
... and feels "Pythonic"
Return a Python dict or list directly.
Super testable.
Blazing speed compared to rendering HTML.
Factoring a web app to expose JSON can even benefit the traditional desktop UX:
The localStorage element stores key-value pairs as strings.
Think "cookies on steriods", local to the "page" (the DOM).
setItem, getItem, and removeItem are the API. Like Python, you can use square brackets.
You have to convert values yourself.
Lawnchair puts a slicker, more cross-platform face on it.
The geolocation element provides access to the API.
Can use GPS, cell tower triangulation, IP mapping
API takes asynchronous callback function:
function show_map(position) { var latitude = position.coords.latitude; var longitude = position.coords.longitude; // Now show a map..... } navigator.geolocation.getCurrentPosition(show_map);
Offline mode: the cache.manifest file gives fine-grained control over resources
Resources can be "never cache", "always cache", or "fallback"
Each page in an app should reference the same cache.manifest URL:
<!DOCTYPE HTML> <html manifest="/cache.manifest">
MIMEtype for that URL must be text/cache-manifest
HTML forms :(
and then add thumb-typing :|
Improved form elements are especially useful for mobile devices.
Declarative, client-side validation (required, plus custom types) can be an easy mobile win.
Performance: JS in HTML may be noticeably slower than native code
"Deep" access to platform APIs may be missing
Integration with platform-specific stores missing.
Supports all popular mobile device platforms and modern browsers:
Built on the jQuery and jQuery UI foundation
Active, friendly community
Unfied touch / mouse event model.
AJAX page loads avoid "refresh" pain
Progressive enhancement
Responsive design:
Flexible, easily themeable design.
Create a new virtual environment:
$ /opt/Python-2.7.2/bin/virtualenv \ --no-site-packages /path/to/jqmdemo
Install pyramid and pyramid_jqm from PyPI:
$ cd /path/to/jqmdemo $ bin/easy_install pyramid pyramid_jqm
Generate the app skeleton via paster create:
$ mkdir src && cd src $ ../bin/paster create \ -t pyramid_jqm_starter ploneconf
Install the generated package:
$ cd ploneconf $ ../../bin/python setup.py develop
Start the server:
$ ../../bin/paster serve --reload development.ini
Profit!
Simple, semantic HTML:
<div data-role="page" id="home"> <div data-role="header"> <h1>pyramid_jqm</h1> </div> <div data-role="content"> <ul data-role="listview" data-inset="true"> <li data-role="list-divider">Demos</li> <li> <a href="#about">About</a> </li>
Hash URLs refer to other "pages" (divs) in the DOM.
Static HTML provides markers:
<p>This application is using <i>pyramid_jqm</i> version <span id="about-pyramid-jqm-version"></span> and Pyramid version <span id="about-pyramid-version"></span>.</p>
... for update by jQuery:
function about_pageshow(div) { $.getJSON(api_prefix + '/versions.json', function (data) { $('#about-pyramid-jqm-version').text(data.pjqm_version); $('#about-pyramid-version').text(data.pyramid_version); }).error(jqxhr_error); }
Google Maps API initialized using "asynch" API (for bundling):
var script = document.createElement("script"); script.type = "text/javascript"; script.src = "http://maps.google.com/maps/api/js?sensor=false&callback=pyramid.init_google_maps"; document.body.appendChild(script);
Using jquery.Deferred to manage async completions:
var google_maps_ready = new $.Deferred(); //... function init_google_maps() { var map_options = { zoom: 15, mapTypeId: google.maps.MapTypeId.ROADMAP, mayTypeControl: false, //... zoomControl: true; }; google_maps_ready.resolve(map_options); }
Geolocation API is queried, with completion through another deferred:
var device_location_ready = new $.Deferred(); //... function set_device_location(loc) { device_location_ready.resolve(loc); } if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( set_device_location, fail_device_location, {'timeout': 2000}); }
When device location and maps API ready, initialize the map:
$.when(google_maps_ready, device_location_ready).done( function (map_options, loc) { var point = new google.maps.LatLng(loc.coords.latitude, loc.coords.longitude), local_map_options = {'center': point}, canvas = $('#map-canvas')[0], map, marker; $.extend(local_map_options, map_options); map = new google.maps.Map(canvas, local_map_options); map_ready.resolve(map); });
Simple, structural HTML:
<form id="personalinfo-form" method="post" action="/change_personalinfo"> <fieldset data-role="fieldcontain"> <label for="personalinfo-email">Email:</label> <input type="email" name= "email" id="personalinfo-email" class="required"/>
Simple Pyramid view code for drop-down population:
countrylist = [ {'abbr': 'US', 'name': 'United States'}, {'abbr': 'CA', 'name': 'Canada'}, #... ] @view_config(name='countries.json', renderer='json', http_cache=cache_one_day) def countries(request): return countrylist
Populating the drop-down client side:
var countries_select = $('#personalinfo-country'); //... countries_api(function (data) { countries_select[0].options.length = 0; $.each(data, function (index, item) { countries_select.append( $('<option value="' + item.abbr + '">' + item.name + '</option>')); }); countries_select.selectmenu('refresh'); });
Static list has only the header:
<ul id ="dynpages-languages" data-role="listview" data-inset="true"> <li data-role="list-divider">Languages</li> </ul>
Add links (via JSON) to server-side URLs:
var ul = $('#dynpages-languages'); webframeworks_api(function (data) { ul.listview(); $.each(data, function (index, lang) { var li = $('<li><a href="/language/' + lang.name + '">' + lang.desc + '</a></li>'); ul.append(li); }); ul.listview('refresh'); });
New pages get folded into the main DOM.
Table of Contents | t |
---|---|
Exposé | ESC |
Full screen slides | e |
Presenter View | p |
Source Files | s |
Slide Numbers | n |
Toggle screen blanking | b |
Show/hide slide context | c |
Notes | 2 |
Help | h |