Bmeter: Pure Client Side Single Page App

Hackathon, yay! I was tasked with upgrading the Bchirometer, a simple single page app (SPA), to a narrow (first?) UI, given a bunch of sketches in PNGs.

Devlog

  1. Rewriting, really. Repo was a mess of drafts, and… nu.

  2. App uses Open Knesset's data, but otherwise it's independent — separate (sub)domain. Not even same servers: it's on S3, because pure client side, aka static site.

  3. GitHub, fork, clone. ;o)!
  4. Repo was a mess — crumbs from brain storming, prototyping, POCs… cruft. My task: new UI (and UX) — narrow first SPA. New UX meant algorithms changed, too. Ended up rewriting everything in CoffeeScript, Stylus, etc.
  5. Static HTML: nice that can simply open it with file:///$PROJECTS/Bchirometer/bchirometer.oknesset.org/index.html… except this doesn't really work: "file:" scheme breaks AJAX (which we're not using), and anything we load from CDNs with scheme-less URLs, which is the recommended practice, eg:
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
  6. So, should really serve our static HTML files — and all other assets — over HTTP. It's a one-liner with Python, but I prefer Node… except, 74892 results for ‘simple http server’ in NPM! $#@! Nu, http-server: a simple zero-configuration command-line http server, does the trick:
    $ sudo npm install http-server -g
    $ http-server
    Starting up http-server, serving ./ on: http://0.0.0.0:8080
    Hit CTRL-C to stop the server
    [Fri, 20 Feb 2015 17:51:32 GMT] "GET /category.html" "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/34.0.1847.116 Chrome/34.0.1847.116 Safari/537.36"
    ...
    
    Happy.

Technologies

  1. Django, as a REST backend for this project? It just doesn't work. Performance is so bad we had to populate our lists (static HTML we generated from JSON returned by slow, unreliable queries) offline, so the app uses constants — updates won't show.
    1. Regardless of the unreliability and (under)performance OK's Django based REST API, we needed to manually configure/filter the data, I was told, to compensate/correct modeling problems: the tool must show data relevant to the upcoming elections only, some of which is missing, or something, from OK's API.
    2. Presumably, the data we compute over is pretty constant, and the tool would only be used right before elections?
    3. Nevertheless, there's room for more automation: hand stitching JSONs is, well, yuck. I'd want to generate HTML directly, during build, with a script and minimal manual configuration. Python, if there's need for closer integration with OK's main site/app, or… I'd do it in CoffeeScript, obviously.
  2. Bootstrap?
    1. Why? WHY?! TB is obsolete, bloated, and bad practice. They (OK) don't even have an interesting skin (aka theme) I'd want to inherit (ie reuse).
    2. If only for performance reasons: my hand coded CSS weights only about 4KiB, whereas TB's (no optimizations attempted) is like 200KiB.
  3. Underscore? For some collection manipulation methods? I scrapped the convoluted, impenetrable logic, replaced with just a few lines of jQuery:
    # Recalculate final scores.
    $ '#agendas-list>li'
    .each ->
    	a=$ @
    	# Voted?
    	if a.find('button.selected:not(.indifferent)').length isnt 0
    		dis_agree=switch
    			when a.find('button.selected.agree').length isnt 0 then 1.0
    			else -1.0
    		ps=$.parseJSON a.attr 'data-parties-scores' #??? Optimize by parsing once and setting a.data!
    		for own party,score of ps
    			do (party,score)->
    				final[party] or=[] # Initialize to empty array, once.
    				final[party].push score*dis_agree # Add score.
    Apparently, jQuery's built-in sort detaches nodes, so we append them back:
    ol=$ '#parties-list' # Parent <ol>.
    ch=ol.children().get() # Children <li>.
    .sort (x,y)->if (parse_score x)<(parse_score y) then 1 else -1
    ol.append ch # Re-attach them.
  1. CoffeeScript and Stylus! Highest level coding I've done in 20 years (since 4GLs): logic/behavior merely ~180LOC, styling in ~260LOC, respectively. Too much static content inlined manually into the HTML by others, or I would've generated it with Teacup as well.
  2. Lifecycle, specifically continuous(?) deployment: nu, I believe automation is useful even in quick-and-dirty hacks such as this, but really had no time to set it up; "static" files (index.html and a few assets) were manually uploaded to S3. Generated assets (JS, CSS) therefore "attached" to the repo. Just don't forget to "cake build" before "git commit"…
  3. (Confusingly many octopodes ("#"): mean fragment prefix in URLs, ID selectors in CSS, and comments in CoffeeScript.)

FontAwesome: non-awesome CSS surprises

  1. ".fa" sets the "font" shorthand property, thus breaking font-size and line-height; eg, can't do <h2 class="fa fa-something"> to just add an icon — it breaks h2's styling.
    .fa{font:normal normal normal 14px/1 FontAwesome;font-size:inherit;...
    WTF? This really sucks — forces me to add fake, non-semantic, markup for icons. That's what their examples do: <i class="fa fa-book"></i>. It feels both wrong and a missed opportunity. Fix how?

Page.js: window.location?

  1. I often use Page.js as a nice alternative (wrapper) to the History API, and a simple "router", eg:
    show_page=(p)->$('.page').hide().filter(p).show()
    page_turner=(p)->return ->show_page p
    page 'about',page_turner '#about'
    page 'categories',page_turner '#categories'
    
    Then, can switch between "pages" in an SPA simply with links: <a href="about">.
  2. But, here, I wanted to set a page's state — menu of links to same page:
    <li><a class="button fa fa-binoculars" href="/agendas#[99]">שקיפות</a></li>
    <li><a class="button fa fa-bus" href="/agendas#[102]">תחבורה ציבורית</a></li>
    And handle variations with something like:
    page 'agendas',->
    	cid=location.hash
    	…
    	show_page '#agendas'
    
    Except, weirdly enough, when navigating these links, my route handler is called before window.location is updated!
  3. Solution: use Page.js's context parameter (or route parameters?):
    page 'agendas',(context)->
    	cid=context.hash # Gives just the URL fragment, without "#" or "#!" prefix.
    

SPA and reloading

  1. But, Bmeter really is a static, pure client side, app, no server API, no need (not even SEO) for, nor ability (official deployed to S3!) to, handle different URLs and serve different HTML content.
  2. I typically use Page.js to override hyperlinks (location changes) that would've otherwise caused page reloads, forcing SPA behavior in the client side, but still providing multi-page content to crawlers, ie for SEO. So, "pages" would still have distinct, pretty URLs, but Page.js would prevent reloads while still supporting navigation history ("back" is prominent in handhelds), bookmarking, sharing, etc.
  3. Solution was to use the old (predating the History API) URL fragments based (hashbang, "#!") convention:
    ><li><a class="button fa fa-tree" href="#!agendas[114]">איכות הסביבה</a></li
    ><li><a class="button fa fa-paw" href="#!agendas[103]">זכויות בעלי חיים</a></li>
    
    Then, setting Page.js's "hashbang" option, dropping the "#!" prefix from routes, but prefixing redirections:
    page 'results:votes?',(context)-> # Nav to results page.
    	# Guard against missing votes: redirect home.
    	unless context.params.votes then console.log 'Missing votes; redirecting.'; page '#!splash'
    	…
    page.start hashbang:yes # Begin listening to location changes.
    
  4. Bonus feature of this scheme: URL path independence, so can "deploy" (ie, host the files) anywhere, no path redirection necessary, can even rename index.html, or open it locally (file:///)… even reloading works regardless of which "page" we're in.

Stylus

  1. This is how I organize my .styl files:
    1. Sort rules by selector specificity (low to high), then alphabetically, recursively. (Specificity order (increasing): universal (ignored), elements and pseudo elements (except div, span), class and attributes and pseudo classes (except :not), ID, inline, !important. Where are user and agent default stylesheets?)
    2. Avoid needless nesting; ie, prefer lowest specificity.
    3. Prefer nesting overrides at target, not parent. Eg, "input → #foo &" instead of "#foo input".
    4. And DRY (don't repeat yourself), obviously.
  2. But, can't edit parents? (Couldn't use "root" selector — seems broken.)
    ol
    	li
    		display inline-block
    	&#parties-list li //???
    		display block
    
    Can't nest the last rule because can't override parent? Stylus's "&" just quotes and concatenates entire path, which seems… nu.
    #FOO& //-> "#FOOol li", which is broken.
    &#FOO //-> "ol li#FOO", but no way to get "ol#FOO li"?
    
    Actually, I shouldn't have nested the li — this violates "avoid needless nesting" — so that example isn't actually a problem. But, generally? Nu, should really find a real usecase…

Flexbox

  1. Should use it more, instead of inline-block and text-align:center, or even margin:auto…
  2. Amazingly, non-standard vendor prefixes (and other fixes) still needed in many browsers!
    // Ugly hacks around stupid browsers?
    display -webkit-box // Old: iOS 6-, Safari 3.1-6
    display -moz-box // Old: Firefox 19- (buggy but mostly works)
    display -ms-flexbox // Tweener: IE 10
    display -webkit-flex // New: Chrome
    display flex // Correct, official spec: Opera 12.1, Firefox 20+
    -webkit-flex-direction row // Safari bug workaround. PrefixFree didn't handle it.
    -webkit-flex-wrap nowrap
    flex-flow row nowrap

Staging

  1. During development, I deployed to my WebFaction account as a static HTML site: bmeter.decodecode.net.
  2. Rsync: after the initial deployment (with Thunar and sftp://$USER@decodecode.net/$PATH/webapps/bmeter, not Git), updating easily:
    $ rsync --recursive --times --verbose --exclude-from=.gitignore . $USER@decodecode.net:/$PATH/webapps/bmeter/
    
    I added ".git/" to .gitignore just for Rsync. Not using "--existing" because still adding/renaming files.
  3. .htaccess?
  1. Analytics
  2. Facebook and Twitter

--
The real world is a special case