Tuesday, January 20, 2015

jQuery and event delegation

Here's a trick that I had forgotten, but that can make a difference: the delegation of event handlers in jQuery.

The Usual Way


So, let's say you have a listing and its content will be refreshed programatically. A common use case would be a search function that generates a list of results, such as listviews being populated in a mobile app (think jQuery Mobile here for the moment).

If the LI elements were already on the page, then you'd simply do this to assign an event handler to all LI items in the result listing:
$('#results li.result a.moreinfo').click(function () {
    var data = $(this).closest('li').data('rawinfo');
    showMoreInfo(data);
});
But as you know, when the app first starts there are no LIs nor any a.moreinfo elements, so in fact nothing is assigned thosre event handlers. Besides, performing a new search would blow away those LI elements and their event handlers, replacing them with brand new ones that don't have event handlers. So, you assign event handlers to the elements as they are loaded into the result listing:
var listing = $('#results').empty();
for (var i=0, l=results.length; i<l; i++) {
    var li = $('<li></li>').data('rawdata', results[i]).text(results[i]).appendTo(listing);
    li.click(function () {
        var data = $(this).closest('li').data('rawinfo');
        showMoreInfo(data);
    });
}
So, it's no huge bother to assign those event handlers in the loop. After all, that's where you're creating the elements so it's natural that you would define the handlers there. Having those two pieces of code together really does make sense.

But that easy way does involve some runtime overhead. If you have 500 search results, then during your loop you instantiated 500 anonymous functions, and performed 500 event-listener bindings. That consumed very little bit of time, perhaps 1-2 ms apiece... hey wait, that's a significant fraction of a second!

Do it At The End


An optional approach, is to assign your event handlers at the end of your search-and-make-listing function, like this:
// load the listing with event-less LI elements of your results...
var listing = $('#results').empty();
for (var i=0, l=results.length; i<l; i++) {
    var li = $('<li></li>').data('rawdata', results[i]).text(results[i].name).appendTo(listing);
}

// now at the end re-scan that section of the DOM and assign event handlers
$('#results li').click(function () {
    var data = $(this).closest('li').data('rawinfo');
    showMoreInfo(data);
});
This method also works well: one bulk assignment, and in a position where you know the elements are in the DOM. Not bad. Not quite as clean and organized as having defined event handlers back when the page first loaded, but it does keep the two chunks side-by-side (mostly) and does avoid a step of processing 500 times.

The Delegated Way


But you can also give this a try. Using the on function and specifying a selector element in between the event and the function, the parent itself will listen for clicks... on the specified elements.
// in the application initialization, the #results panel hosts the event handler
// the LIs don't have their own handlers, so destroying them when you empty() will not destroy the event handlers
// assign once and forget it!
$('#results').on('click', 'li', function () {
    var data = $(this).closest('li').data('rawinfo');
    showMoreInfo(data);
});

//some time later ...  our make-a-list-of-results function is slimmer, noww that we're not
// instantiating functions and setting event bindings
var listing = $('#results').empty();
for (var i=0, l=results.length; i<l; i++) {
    var li = $('<li></li>').data('rawdata', results[i]).text(results[i].name).appendTo(listing);
}
The key is that small syntax change: if you stick a jQuery selector in between the event type and the callback function, then it changes the meaning.
// all LI elements under #results get a click handler
$('#results li').on('click', function () {});

//  #results gets a click handler, with an additional check that the event was on a LI element
$('#results').on('click', 'li', function () {});

This technique combines the best of both: you define the event handler at the start of your code, and have only one event handler running, but when time comes to populate the loop that handler will automagically be effective on newly-created LIs as they enter the #results element.

Potential for Confusion


The down side here, is the up side of the original way:
  • Having the elements created in the loop, and their handlers created in the loop, has both bits of code right next to each other where they're obviously related and are visually together.
  • Having the event handlers set up on line 10 and the elements created in the loop on line 1450, means some guesswork for the dev who inherits your work. "I see the span.maplink but I don't see where it had the click behavior asssigned." This is especially true if you didn't use an explicit class name, e.g. something really generic like $('#results li')
So, as always, be generous with commenting your code. A simple note like "click handlers on the result panel are defined up in initResultListing()" can save a half-hour of searching and head-scratching.

Learn More


http://learn.jquery.com/events/event-delegation/

Saturday, January 17, 2015

CartoDB.JS - interactivity, and faking it

CartoDB is neat stuff. At work I don't get to use it as often as I would like, since we already have a PostGIS server. And their JavaScript library is pretty slick too: Leaflet, some Layer implementations, etc. It doesn't have quite everything built in but it does have plenty of tools to build.

In this case, what we had was some basic interactivity with a bunch of fields, a mouseover handler to show the polygon's project_name in a tooltip, and a click handler to open a popup with more info. But then, we needed to be able to trigger false clicks onto features based on their ID# so that it would act as if it had been really clicked, opening the informational popup and all.

Sadly, this  is impossible... but we can fake it!

Scenario

So, let me lay out a scenario for you, that starts off as typical CartoDB and then get a little weird.
  • We have a Leaflet map, and a CartoDB layer on it. The CartoDB layer has only Sublayer 0, and we use that to show polygons on the map. (it's filtered by search results using setSQL(), but that won't be relevant here)
  • The CartoDB layer has interactivity set using the project_name field, and has a mouseover effect to show a tooltip. It also has a click handler, which makes use of those same interactive fields. Again, pretty standard.
  • The interactivity is for rather a lot of fields beyond the project name. Although only the project_name is in the tooltip, we grab a lot more fields and also add a click handler to the interactivity. Thus, on a click we can create an informational popup without making a server hit and have an instant popup. (don't go overboard on lengthy field data being stuffed into those utfgrids, but it works fine for 7 fields at 5-20 letters apiece)
  • And then, now that the project is about to launch...
    We want an URL param that will trigger a click on one of the features. Given the cartodb_id of a project, we want to somehow find that feature on the map and then make a click event on it, so as to trigger that popup bubble.

The Basic Interactivity Code

For starters, the code for adding the layer to the L.Map and then adding interactivity, is pretty straightforward. The unusual part is that we ask for a lot of fields in the interactivity, and not the usual practice of asking for only the 1-2 that form your tooltip.

// CartoDB settings
var CARTODB_USERNAME  = "myaccount";
var CARTODB_TABLENAME = "projects";

// the L.Map and the CartoDB project overlay
var MAP, PROJVIZ;

// the CartoCSS colors
var CARTOCSS = "\
#projects {\
    line-width:1;\
    line-color:rgb(224,77,13);\
    polygon-fill:rgb(211,134,23);\
}";

// add the CartoDB overlay: the project areas
cartodb.createLayer(MAP, {
    user_name: CARTODB_USERNAME,
    type: 'cartodb',
    sublayers: [{
        sql: "SELECT * FROM projects",
        cartocss: CARTOCSS
    }]
}).addTo(MAP).done(function(layer) {
    // create a global reference for easy access
    PROJVIZ = layer;

    // your usual interactivity, but with an unusual number of fields
    // since we wasnt simple tooltips but also information-packed popups
    PROJVIZ.setInteraction(true).setInteractivity('cartodb_id,project_name,grantee,year,state,acres,dollarvalue,bboxs,bboxw,bboxn,bboxe');
    PROJVIZ.on('featureOver', function(event,latlng,pos,data) {
        document.getElementById('map').title = data.project_name;
    }).on('mouseout', function(event,latlng,pos,data) {
        document.getElementById('map').title = "";
    }).on('featureClick', function(event,latlng,pos,data) {
        var html = data.project_name;
        // then a bunch more fields: html += data.acres + data.grantee + ...
        MAP.openPopup(html,latlng);
    });
});
Point being, our interactivity asks for a lot of fields for popup purposes, and is added to the map, and works gorgeously. Basically copy-paste LeafletJS technique.





But I Don't Want To Click!

So then the request came in, that if we give an ID# in the query string, the map should automagically zoom to that feature and trigger that very same click. The cop-out would be that we do a SQL search and also create a different popup, ...

But then we would miss out on a lot of the other complexity that eventually crawled into that featureClick callback over time, such as highlighting stuff, fetching a chart based on the acres and dollarvalue, zooming the map to the given bbox. And duplicating that code would just be not great.

So, back up a bit and fix up that featureClick anonymous callback, to use a named function instead. You really should have done this in the first place, keeping your anonymous callbacks brief and not "visually polluting" that CartoDB "done" handler way back.
on('featureClick', function(event,latlng,pos,data) {
    openClickPopup(latlng,data);
});

function openClickPopup(latlng,data) {
    var html = data.project_name;
    // then a bunch more fields: html += data.acres + data.grantee + ...
    MAP.openPopup(html,latlng);
    // and about 20 lines of other feature-creep
}
Now we have a way in: the openClickPopup() accepts a latlng (where th anchor the popup) and some data (field attributes for a record in the projects table), and doesn't care where it came from. So now we really can do a SQL query and just grab *, then hand off not to a true featureClick event handler, but to the featureClick handler's real handler.


Bur wait... The latlng. The featureClick event has event.latlng for the click, butthe projects table doesn't have such a thing. Even if our SQL asks for the centroid, that's not solid: a L-shaped or U-shaped polygon would clearly have a centroid not even inside the shape. So hooray for ST_PointOnSurface()

// make a SQL query to get the same fields as we use in the interactivity (* is good)
// and also grab a lat and lng so we can fake a location
// given a latlng and a "data" of field attribs, we can use openClickPopup()
// ST_PointOnSurface() is an excellent alternative to ST_CENTROID()
var id = params.param('id');
var sql = new cartodb.SQL({ user: CARTODB_USERNAME });
sql.execute("SELECT *, ST_X(ST_POINTONSURFACE(the_geom)) AS lng, ST_Y(ST_POINTONSURFACE(the_geom)) AS lat FROM projects WHERE cartodb_id = {{id}}", { id:id }).done(function(records) {
    // compose the same params we would normally have, and hand them off
    var data   = data.records[0];
    var latlng = L.latLng([data.y,data.x]);

    // for extra verisimilitude, you can in fact trigger a click event on the CartoDB layer
    // this may make more sense than simply calling openClickPopup() for your specific need
    OVERLAYS['projects'].trigger('featureClick', null, latlng, null, data, 0);
    //openClickPopup(latlng,data);
}).error(function(errors) {
    alert(errors[0]);
});


So there we have it. In fact, it was nothing really special, as much as just pointing out the benefits of following some elementary practices and abstracting out your event handlers (featureClick) and their intended consequences (popups, highlights, automatic search, charts, ...).





Wednesday, January 7, 2015

BattleTech Calculator

I finally released a personal app, as opposed to my usual map-based ones for work -- a calculator for the board game BattleTech.

In this board game from the 80s, giant robots run around blowing each other up. It involves a lot of dice and a fair amount of math; not simple, but tedious. It's so tedious we really did spend 5 hours going back and forth counting on our fingers, looking at the rules, losing our place as far as which fingers were which, ...

So, the calculator is a nice selector-based and checkbox-based tool: pick your target's status, your status, and whether there are trees in the way, and voila. It turned 5 hours of finger-counting into 2.5 hours of roboty-blowey-uppy action.

So, if you're interested:



Friday, January 2, 2015

Framework7 -- HTML5 framework for Cordova Mobile

It's been a long quiet time, I know. I've been working on a lot of stuff, but nothing had a "wow, folks would be interested to hear about this" sort of flavor.

But, as is typical at the time of the winter solstice holiday, clients went on vacation, backlogged lower-priority work got done, and eventually my inbox flatlined. That feels good... and opens me up to get back onto  thread I've been promising since September - - picking a new HTML5 toolkit, to use with Phonegap/Cordova.

To reiterate, our basic wish list (which could be interpreted as a gripe list with jQuery Mobile) is as follows:
  • Structured and configured in HTML and CSS, as opposed to code. Not that I myself am opposed to code, as much is that the designers are the ones who call the shots, so anything strongly based on prefab widgets declared in code is likely to run afoul of the designers wanting something that the code just doesn't do.
  • Highly configurable CSS. jQuery mobile is configurable and flexible, to a degree. Then again, it has a whole lot of decor already in place that we often need to override. Again, the clients of the designers specifically pick things that the toolkit doesn't do, since they don't like to look like a default kitchen sink demo. :-)
  • Not buggy. JQM has a lot of bugs that keep biting us. Some are in the design (text input do not get the same styling, not the same with, are visually inconsistent) and some are functional (a loading spinner won't show at all if the following AJAX call takes one second, buttons continue to look after the finger has been removed from the screen).
  • Performant. JQM is a bit laggy on two-year-old devices. Turning off page transitions and editing CSS to remove shadows helps somewhat, but then again ideally it would lack these shadows to begin with.
A few months ago I discovered this one: Framework7 http://www.idangero.us/framework7/ And over the solstice holiday break, I finally got to play with it and I really like it a lot. I used it to write a personal app, a calculator for playing BattleTech (a  board game from the 80s).

  • It is largely based on HTML and utility CSS classes, which is perfect for our needs.
  • It does not use jQuery, but their own lighter-weight DOM library with a syntax nearly identical to jQuery. It's not exactly the same, however, and lacks some features. (see below)
  • Visually, it's quite lightweight. The navigation bars are white and blue by default, thin outlines, and an otherwise not-very-cluttered visual feel. This is as opposed to the somewhat-heavy toolbars and buttons used by JQM.
  • When I needed to make arbitrary changes, it proved quite capable. For example, the panels are 100% wide and with no margin simply by adding a new CSS class with a margin:0, or by omitting a wrapper DIV which introduces a margin. Making the tab button 100% wide, was equally simple.
  • Adding FontAwesome took literally moments. Adding FontAwesome was necessary because the framework's own CSS classes do not include a rich set of icons. So this bullet point is both a point against Framework7, then that point rewarded back again -- the framework does not come with a rich set of icons, but has no problem working with your choice of icons. This point is then awarded a second time, because icons on buttons and panels can be quite arbitrary and flexible: rather than using data-icon="" attributes on elements which specifically supported, you declare your arbitrary <i class="fa"></i> elements wherever you like. This came in handy, for example, adding a X button to the slideout panel in order to close it.
  • It feels very performant and slick, even on an iPhone 4. This three-year-old phone represents the trailing edge of what we will be supporting for the next year or two. And under this framework, even this slightly obsolete hardware is plenty fast.
  • The resulting application is very consistent in its look between Android and iOS platforms. For us, this is preferable over a framework which tries to "look native" on both platforms -- the designers want a consistent outcome and not stuff that "looks default". And It looks like they'll get it.
I see a lot of promise with this one. Still, I should point out the problems I have had with it as well.
  • The documentation is incomplete and incorrect in parts. For example, their own "starting example" (at the time) does not render properly since they have one of the class names misspelled. The documentation for individual widgets such as checkboxes, consists of a lengthy paragraph of HTML forming an example, and it's up to you to visually tease apart which elements are the relevant ones since there are rarely any additional instructions.
  • The DOM library (called Dom7) is not exactly as functional as jQuery. For example, it does not have the :selected pseudo so you cannot pick the <OPTION> element itself and pull additional data-xxx="" attributes beyond the value itself. If you need to do more complex selections like that, you would need to iterate and compare value. Also, to touch upon the documentation, the narrative component of the documentation for Dom7 consists of one sentence: "Just what you already know" followed by a snippet of what looks like jQuery code (but it's Dom7). Then follows a list of supported functions, but not a in-depth description of what sorts of selectors are and are not supported; I had to discover myself the limitation about option:selected
  • Their documentation recommends that you assign $$ to Dom7, so you can act similar to jQuery with selectors such as $$('span.highlight') but not conflict with some other library which uses $. However, $$ is already defined and their advice is basically for you to override their $$ with a new $$ to Dom7. If your own assignment of $$ fails, you get some wonky behavior. The symptom here, is that $$ still returns elements but it's an array of raw DOM elements and not Dom7 object wrappers -- as a result, your call to $$('span.highlight') returns a set of <SPAN> elements, but your call to show() fails with "show is not a function" when you know perfectly well it's a function! For this reason, I recommend that you ignore their advice and assign to $ instead. At least that way, a mismatched brace somewhere in your 2000 lines, won't lead you on a wild goose chase (or is it a red herring fishing trip?) hunting for $$ results which exist but do not obey.

All told, I think we have a winner. If you are using PhoneGap or some other HTML web view type deal to write your hybrid mobile apps, you'll probably want to check this one out. Framework7 http://www.idangero.us/framework7/


Oh, right; my app

If you're interested in the BattleTech app, you can check it out over on Github:
https://github.com/gregallensworth/BattleTechCalculator

This app does not use the framework's wrapper classes for checkboxes, select elements, and so forth. This particular app is so heavy with dozens of checkboxes and a two-column layout, that we needed the most compact layout available. Although I gladly used the navbar, left slideout panel, and tab bar, this app is not representative of Framework7's decoration of form elements.

But even this is a good thing: you are not forced to use these decorated checkboxes and select elements, and this is a great scenario for we were allowed not to use them and thus achieve a more compact layout than would have been possible with JQM. So even in a case where we don't want the decor at all, Framework7 still came up a winner.