How I used backbone.js in Lifesquare to make for a better signup progress

June 21st, 2012 § Comments Off on How I used backbone.js in Lifesquare to make for a better signup progress § permalink

This year has been interesting. When not working the day job or finding time to brew I’ve been working hard on getting Lifesquare‘s consumer website off the ground. Lifesquare centers around QR codes that people attach to things that are always with them (e.g. bike helmet, wallet, iPhone, car insurance) and in the event of an accident an EMS team can use the Lifesquare app (available only to EMS professionals) to scan your QR code to find out any information they might need to know in an emergency. This is great because every second counts in a first responder situation and if you’re unable to communicate to the EMS team they might not know to avoid certain medications that could cause serious allergic reaction. It’s a fantastic idea and their product definitely adds to the betterment of humanity – two things that I pretty much require when working for someone.

All of the mobile app and server stuff was pretty much taken care of when I was brought on board; what was needed was a consumer side where people could sign up, create patient accounts and enter information about those patients. Sounds like something that has been done before but the team at Lifesquare threw in some fun twists that helps them stand out from generic, boring data entry workflows.

Level up backbone.js

I was given pretty much free reign on the front-end code so I based my work on backbone.js. The absolute best part of using backbone is that you don’t have to subscribe to any one way of doing anything. The tools are given to you to assemble an application in whatever way makes most sense for you. There are some basic things I like to do when starting any backbone-based app to augment the framework and create a bit cleaner architecture.

Organize further

Backbone provides structure but not much in the way of namespacing. A little bit of memoization takes care of that, however. I use a memoized base object that automatically adds a Views object but nothing else. I’ve had projects that create multiple types of memoized objects from full backbone views/models/collection groups to plain object literals. I find the Views addition takes care of what’s needed for Lifesquare.

var module = function () {
	var modules = {};

// Create a new module reference scaffold or load an existing module.
	return function (name) {
		if (modules[name]) {
			return modules[name];
		}
		return modules[name] = { Views: {} };
	};
}();

var Home = module('home');
Home.Views.SplashImages = Backbone.View.extend({ ... });

No matter where you are in the app’s code you simply call module(‘featureNameGroup’) and you get the fresh object or a reference to the one being used by everyone else. Especially handy when passing modules in to immediately invoked function expressions.

(function(Home) {
	Home.Views.coolNewView = Backbone.View.extend({
		...
		initialize: function() { ... }
		...
	});
}(module('home'));

Add magic to getters/setters

While we all wait for ES6/Harmony and the getters and setters we deserve there’s Backbone.Mutators to do the heavy lifting for you. It gives you hooks for existing or arbitrary model values that can run transformations, mutations or whatever else you like on the get or set of any value.

My favorite use in Lifesquare was accepting a free text date field value, analyzing the string and finding the true date, creating a date mask to match the user input, saving that mask to a field and saving the full date in another field in the database and still retaining the user format in the field. This happens every time a date is entered into a field in Lifesquare. The storage of full date as YYYY-MM-DD HH:II:SS allows for better reporting and sorting on the admin side while the saved user mask allows us to display the date just as entered. This took some work to get right but it’s still not bug free. Free text dates are not a solved problem yet in JavaScript, despite moment.js and date.js.

A simple example is format conversion. Lifesquare is currently targeted toward American users but we store everything in metric so I needed to accept weight in pounds but convert to kilograms.

(function (Patient) {
	"use strict";

	Patient.Model = Backbone.Model.extend({
		mutators: {
			imperialWeight: {
				get: function() {
					var pounds = Math.round(this.weight * 2.20462262)
					return pounds > 0 ? pounds : undefined;
				},

				set: function(key, value, options, set) {
					var matches = value.match(/([0-9\.]+)/);
					if(matches !== null) {
						var pounds = value,
							kilos = pounds * .45359237;

						this.set('weight', kilos, options);
					} else {
						this.set('weight', '', options);
					}
				}
			},
		...
	});
}(module('patient'));

Speaking of matching strings…

With all the input happening it’s inevitable that some validation was required. Nobody likes doing front end validation. Thankfully there’s backbone.validation to make things easier. It works with your model structure to provide hooks for default rules, custom regex tests and even custom validation functions. I use all three of those features extensively and it has been great.

What hasn’t been great is the validation execution path. Maybe I just don’t grok the intended workflow but it’s less than smooth in practice. What I ended up doing was the moment a view accepts a model the .validation object is duplicated as validationRules and the validation object is reset to an empty object literal. Then, as each field is blurred/selected the validation rule for that field is applied to the validation object (from the cloned validationRules) and that’s where the model-wide validation is run. Clunky, for sure, but it works pretty well in practice. Binding the validation rules to the fields, though, is a bummer. I ended up using data- attributes and name attributes. If anyone has integration tips I’m all ears.

Stick to the defaults

When development initially began the server interfaces would not accept requests if data had values outside of what was expected in the model. Given that the Backbone.Mutators were adding those fields to the model during JSON serialization (something for which I am eternally grateful) there were issues. Although the server rules eventually relaxed and just threw away the unnecessary fields I had to write a stripper in the interim. Pretty simple stuff but maybe someone will find it useful. Just plop this into your custom Backbone.Model extension or in any single model as it’s needed and call before/after sync().

//Removes values that shouldn't be sent back during a sync
evictIllegals: function() {
	var self = this,
		allowed = _.keys(this.defaults);
	_.each(self.attributes, function(value, key) {
		if(!!~allowed.indexOf(key) === false) {
			delete self.attributes[key];
		}
	});
}

Wait for the big show

Okay, so now you have some great building blocks. Let’s write some data entry!

Wait. There are multiple groups of data on each page that need to go to different services and all must finish before you can consider the page “submtted”? No problem.

Enter Backbone.Abide

Abide is a small addition to backbone that I wrote during development of Lifesquare. What this dude does is orchestrates the submission of multiple AJAX requests, monitors their completion status and when everything is done will throw an event to anyone who cares to listen. He also will run validation on any models submitted, disable submit buttons and update their text with submitting/finishing/done status messages.

Right now there are hooks to warn the user if leaving a page where values have been modified and monitor the session timeout to throw “hey you’re about to time out and you haven’t submitted anything” events to subscribers but those will be refactored out in future revisions.

Just extend Abide when creating a view and subscribe to the events and you should be good to go.

(function(Home) {
	Home.Views.AwesomeForm = Backbone.Abide.extend({

		initialize: function(options) {
			_.bindAll(this);

			//I like to use "self" because it compresses well
			var self = this;

			self.on('validating', function() {
				//Clear out previous validation warnings
			}).on('validationFailed', function(e) {
				//Tell the user there has been something wrong and identify where
			}).on('done', function(e) {
				//All AJAX done. Navigate the page somehow (pushState, document.location.href, change views, etc.)
			}).on('fail', function(jqXHR, textStatus, errorThrown) {
				//AJAX submission has failed somehow. Show error.
			}).on('sessionExpired', self.renewSession).on('sessionTimeoutWarning', self.warnTimeout);

			$(window).on('beforeunload', function(e) {
				if(self._dirty === true) {
					//Abide provides the _dirty flag if the data has been manipulated by the user. Warn about mods here
				}
			});
		}
}(app.module('home'));

Who is this, really?

One of my favorite features to work on was the image uploading. Users can drop an image on the page (or, if you’re old school, pick one from the file picker) and crop the selection to the desired area. I did some really fun work on the cropper, making it intuitive and allowing re-edits. A picture is work a thousand words, right?

Of course it’s recommended you use an actual picture of your face so EMS professionals can correctly identify you. Unless you’re Worf. Then you use this picture.

The kitchen sink and more

I haven’t even gotten to occurance.js, the natural date parser helper I wrote to accompany moment.js, the autocomplete work with jQuery UI, how to deal with deeply nested backbone views sharing collections and more. All in good time.

What I missed

You can’t get everything right in an app and what I did miss this time around was using an event aggregator, like postal.js. This would have cleaned up a lot of the weird events I push and bubble through backbone and let me decouple things a bit better. In the future I’d like to spend more time getting Postal in but it works for now and like any startup it’s important to ship quickly and refactor on the go.

For now head on over to Lifesquare and check it out. Heck, you should sign up if you live in Marin County, CA. It really is a great idea that could save your life one day.