Loccasions: Going Client-Side with Leaflet, Backbone, and Jasmine

Share this article

We’ve finally arrived at the moment of the map. For the last several articles in the Loccasions series, I have promised things like “in the next post we will deal with the map” and “I will lower taxes,” and I have not delivered. In this post, I will fulfill at least one of those promises.

Adding the map to this application is almost completely a client-side proposition. As such, this post (and one or two following it) will be a metric ton of javascript and a thimble’s worth of Ruby.

Libraries, Frameworks, and Maps, OH MY!

As I mentioned when setting up this application, I use Backbone as the framework for the javascript and Jasmine as my client-side testing framework. My justifications for this choice is that I like both frameworks… a lot.

I do not, however, use a gem to generate Backbone files or magically hook up Backbone to the server. The plan is to write the Backbone classes from scratch, adding in the config to get them talking to the server as needed. I have absolutely nothing against using a gem for Backbone and rails, other than there seems to be confusion about which one does what (If you go to the rails-backbone repo, it tells you to use gem "backbone-rails", but if you go to the backbone-rails repo, it tells you to use gem "rails-backbone". I was in an infinite loop looking at both of them, saved only my wife cutting the power to my office.).

Also, I won’t cover the basics of Backbone, as that has been done. If you don’t understand Backbone, spend some time learning the basics, which should more than prepare you for this article.

For Jasmine, I use the jasmine gem because it’s backed by Pivotal and they rock. However, for Loccasions I’ll employ a specific branch of the gem, which is capable of running the specs and javascript source files through the asset pipeline. We are, after all, using Rails 3.1.

Web-based maps are all javascript, all the time these days. Choosing the right js-map framework was a bit daunting, especially since I’ve spent much of my career using one (ESRI’s ArcGIS Server, if you’re interested. You’re not.)

  • Google Maps is out, due to the possibility of Google pulling a Crazy Ivan about licensing.
  • Yahoo is out, because frankly, because it’s dead.
  • I briefly looked at OpenLayers and may yet use it, since it’s true open source, which I like.
  • I also tried to use Mapstraction, which abstracts away the provider and let’s you change providers on the fly. However, when I tried using OpenLayers with Mapstraction, I couldn’t figure out how to change the theme, so I moved on.
  • Finally, I settled on Leaflet, mainly because it looks great and I like the api so far.

The last bit is a bleeding-and-we’re-taking-blood-thinners addition to the application. I’ve used Backbone a few times and you usually need templates to present your model and collection data in the HTML. I really didn’t want to bring in another templating language (in addition to haml) if I could avoid it, so I found haml-coffee. This gem allows you to write your templates using HAML, and then makes them available in your javascript on the client. We will run through at least one scenario that shows this clearly in the next post.

Setup

Whew! Now, let’s get the application setup with all our new client-side yummies. First, download all the code we’ll need:

  • Leaflet put this in vendor/assets/javascripts/leaflet/leaflet.jsLeaflet also has a CSS stylesheet and images. Put these (css files and images directory) in vendor/assets/stylesheets/leaflet
  • Backbone put this in vendor/assets/javascripts/Backbone/Backbone-min.js
  • Underscore put this in vendor/assets/javascripts/Backbone/underscore-min.js

I also created a vendor/javsascripts/vendor.js file that will load these files. The asset pipeline will do this automatically, but, um, I am a control freak.

// vendor/assets/javascripts/vendor.js
    //= require leaflet/leaflet
    //= require Backbone/underscore-min
    //= require Backbone/Backbone-min

Similarly, I created a vendor/stylesheets/vendor.css file for the Leaflet CSS:

/*
     * This is the vendor.css in our vendor/assets/stylesheets dir
     *=require leaflet/leaflet
     *
    */

Finally, add a couple of gems to our Gemfile:

group :assets do
  ...
  gem 'haml-coffee'
end

group :test, :development do
  ...
  gem 'jasmine', :git => 'git://github.com/pivotal/jasmine-gem.git', :branch => '1.2.rc1'
end

The aforementioned (I luuuv using that word) haml-coffee and jasmine (remember, we’re using a branch) gems. Oh, and I am sure you remembered to bundle install, right?

Now, it’s time to setup jasmine, so run rails g jasmine:install to setup Jasmine support in the application. Jasmine comes with its own server, which is launched by typing rake jasmine. Being super-savvy users of Foreman (remember the last post) we’ll add it to our Procfile:

web: rails s
db: mongod --dbpath=/Users/ggoodrich/db/data
test: guard
jasmine: bundle exec rake jasmine

Next time you foreman start, Jasmine will be running. The Jasmine server runs at http://localhost:8888, but it won’t be very interesting until we get some specs added.

Spec files are added to Jasmine in the spec/javascript/support/jasmine.yml file. Because we are using a branch that supports the asset pipeline, our jasmine.yml file is a bit different than the one used in the current release. Here is a gist of the one I’ve setup for Loccasions. One change I made was to pull in our coffeescript files (this is what the branch gives us) and manually add our vendor javascripts (jQuery, Underscore, Backbone, and Leaflet). The other change is the last line of the file, identifying app/assets as a path to be served by the asset pipeline.

Lastly, I want to leverage Sinon for mocking and stubbing when needed in my javascript tests. There is a nice plugin to Jasmine for Sinon here. Download both those files into our spec/javascripts/helpers/ directory. You’ll need to add the spec/javascripts/helpers/jquery.js file (you can copy it from the jQuery site or from the jquery-rails gem) because Jasmine won’t load jQuery yet (they’re working on it….).

I would suggest reading through this series about Jasmine and Sinon to get familiar with how this all fits together. You’ll no doubt notice its influence on Locccasions.

Whew….easy as, um, really hard pie.

Client-side Directory Structures, and the Women Who Love Them

For every two Backbone introductory articles, there is one on structuring your Backbone code. The structure of the files is a development-only concern, since the asset pipeline will smush all the javascript into one application.js file. After reading through some of the articles, I’ve come up with this:

Our, um, Backbone

In a nutshell, the collections, models, views, and templates all get their own directory. The lib directory is for code that is not part of the Backbone structure.

As I mentioned, the asset pipeline in Rails 3.1 will load all this stuff for you, but I want to control the order of how things are loaded. As such, we need to make a change to your app/assets/javascript/application.js file, like so:

//= require jquery
//= require jquery_ujs
//= require vendor
//= require ./app
//= require ./router
//= require_tree ./lib
//= require_tree ./models
//= require_tree ./collections
//= require_tree ./views
//= require_tree ./templates

Setup Complete, Now What?

With all of our client-side whatzits in place, we can look at the design of our Events page from the perspective of Backbone. This post will flush out the Events#index page, as shown in the following screenshot.

What You See

One way to approach this page with Backbone is to slice the page into views, like so:

What Backbone Sees

This is the approach taken, and should be enough for us to start writing tests.

Gentleman, Right Now on Stage 3, Put Your Hands Together for JAAASSSMMIIIIINE

Let’s start with the map view. Here is our first Jasmine test

// spec/javascripts/views/mapView_spec.js

describe("MapView", function() {
  describe("initialize", function() {

    beforeEach(function() {
      loadFixtures("map.html");
    });

    it("should use the #map element by default", sinon.test(function() {

    }));

    it("should create a map", sinon.test(function() {

    }));

  });

});

You can see that Jasmine looks a lot like any BDD framework syntax. There are “describe” blocks (which can be nested) and “it” blocks to test specific behavior. Also, our beforeEach allows us to load a fixture file. In this case, our fixture file is adding a div#map to the page (really, it’s just <div id='map'></div>) which will hold our map for the tests. Client-side testing is often dependent on the structure of the markup, so being able to load fixture files (which Jasmine will unload after the test) is really nice.

Our map views tests are simple. First, make sure that the view uses the #map DOM element. Second, make sure it creates a “map”.

As quick side note, you may have noticed the it functions are wrapped with sinon.test(). This creates a “sandbox” for the test. If the MapView has any dependencies (and it does), we’ll be stubbing/mocking them as needed. The sandbox makes it easy to restore any stubbed or mocked objects when a test completes.

What is a map, though? I mentioned before that we will use Leaflet, but I am not sure that we won’t change map providers. As such, we should hide the map behind an abstraction in the view. Also, I don’t really need to bootstrap an actual Leaflet map for my tests. Therefore, I have created the concept of a “MapProvider”, which I pass to the view on initialize. We can use Sinon to mock/stub the provider, keeping out tests light.

The MapProvider interface/protocol is very simple right now:

App.MapProviders.Leaflet = ->
  # Create new map
  createMap: (elementId) ->

  addBaseMap: ()->

  addLayerToMap: (layer) ->

  setViewForMap: (options) ->

Just four functions, named to make their purpose very obvious. The implementation of the Leaflet provider is here if you’re interested.

Let’s complete our 2 map view tests:

it("should use the #map element by default", sinon.test(function() {
    // Arrange
    var mp = new App.MapProviders.Leaflet();
    var mapSpy = this.stub(mp, "createMap");
    var setViewSpy = this.stub(mp,"setViewForMap");

    //Act
    var view = new App.MapView({
      mapProvider: mp
    });

    //Assert
    expect(view.el.id).toEqual("map");
    mapSpy.restore();
    setViewSpy.restore();
    </code></pre>

  }));

  it("should create a map", sinon.test(function() {
    //Arrange
    var mp = new App.MapProviders.Leaflet();
    var mapProviderMock = this.mock(mp);
    mapProviderMock.expects("createMap").withArgs("map").once();
    mapProviderMock.expects("setViewForMap").once();

    //Act
    var view = new App.MapView({
      mapProvider: mp
    });

    //Assert
    mapProviderMock.verify();

  }));

These two test show different types of testing. The first test ensures that the MapView does the right thing, provided it’s dependency does the right thing. We don’t really care what the MapProvider does for this test, because it’s not pertinent to the outcome of the test. So, we stub those methods out, which stops the calls from getting to the Leaflet API while making sure they don’t throw an error.

The second test is an example of “Expectation Based Unit Testing”. Here, we DO care how the MapView interacts with its dependency. We expect it to call certain methods, and we ask our mock object to verify that those methods were, indeed, called.

Reloading our Jasmine test page (http://localhost:8888, remember?) we see:

Fail

We can fix that.

I’m the Map[View]!

Any Dora fans out there? No? coughs Right, me neither. Here’s the MapView implementation:

###
app/assets/javascripts/views/mapView.js
###

class App.MapView extends Backbone.View
  el: "div#map",
  initialize: ->
    @mapProvider = this.options.mapProvider
    @initialCenter = this.options.initialCenter || { latitude: 51.505, longitude: 0.09 }
    @render()
  setInitialView: ->
    @mapProvider.setViewForMap
      latitude: @initialCenter.latitude,
      longitude: @initialCenter.longitude
      zoomLevel: 13
  render: ->
    @mapProvider.createMap(@el.id)
    @setInitialView()

Once you add this file, reloading the Jasmine test page looks like:

Pass

Do You Know the Way to Map, Jose?

OK, so, we have a MapView. How do we get our page to use it? I mean, I don’t see a map when I go to my ‘/events’ page in Loccasions. This is where Backbone’s “Router” (or the Artist-Formally-Known-as-Controller) comes into play. In this case, we want to route the “root route” or “/” to a place where it knows to create our MapView. I’ve written a couple of specs to fulfill this requirement:

// spec/javascripts/appRouter_spec.js

describe("AppRouter", function() {
  describe("index", function() {
    beforeEach(function() {
      loadFixtures("map.html");
      this.mapViewStub = sinon.spy(App.MapView.prototype, "initialize");
      window.bootstrapEvents = [];
    });
    afterEach(function(){
      this.mapViewStub.restore();
    });

    it("should create a map view", function() {
      this.router = new App.Router();
      this.router.index();
      expect(this.mapViewStub).toHaveBeenCalled();
    });

  });
  describe("/", function () {
    it("should respond to empty hash with index", function() {
      this.router = new App.Router();
      this.routeSpy = sinon.spy();
      try {
        Backbone.history.start({silent:true, pushState:true});
      } catch(e) {}
      this.router.navigate("elsewhere");
      this.router.bind("route:index", this.routeSpy);
      this.router.navigate("", true);
      expect(this.routeSpy).toHaveBeenCalledOnce();
      expect(this.routeSpy).toHaveBeenCalledWith();
    });
  });
});

In this case, we use a third Sinon stubby/mocky thingy called a “spy”. A spy is like a mock, but you can’t set a return value on it. A spy exists just to record if it was called and what arguments were used to call it. That works well for our first test, where we are making sure the initialize method on our MapView is called. The second test uses a spy to make sure that, when the user goes to the “root route”, the AppRouter#index function is called.

Reloading our Jasmine page gives us the appropriate failures, so let’s write our AppRouter class:

###
app/assets/javascripts/router.js.coffee<
###

class App.Router extends Backbone.Router
  routes:
    "" : "index"
  index: ->
    if $('#map').length > 0
      @mapView = new App.MapView(
        mapProvider: new App.MapProviders.Leaflet()
      )

Done. The specs pass.

Start Me Up

The last bit we need to put in place is to bootstrap our Backbone application code. In other words, we need to tell Backbone to get the Router in place, create all the views, and whatever else it needs to do. I created a little coffescript file for this:

###
app/assets/javascripts/app.js.coffee
###

window.App =
  start: ->
    new App.Router()
    Backbone.history.start(
      pushState: true, root: "/events"
    )

$(App.start)

This file defines my application namespace (App) and creates our start function. the start function simply creates our router and tells Backbone to start handling the routing. If you want to understand exactly what Backbone.history.start is doing, look here.

Update

Mr. Henry (from the comments below) pointed out the following:

  • Add the div #map to your events/index.html.haml
  • Add #map { height: 350px } to the app/assets/stylesheets/events.css.scss

Here’s the page with the map:

What You See

We still have a lot of work to do on this page. However, it’s a good stopping point, and I want to play with the map for a bit.

My Blogger Went All Over the Place and All I Got Was This Lousy Map

I know, that was a ton of work just to get a map. However, we also now have:

  • A structure for our client-side code.
  • A way to drive out the client-side code with tests (TDD/BDD/ETC)
  • A MAP!

We’ll finish out the Events index page in the next post and then highlight how the Occasions page is different. After that, v0.0.1 of Loccasions should just about be done.

Glenn GoodrichGlenn Goodrich
View Author

Glenn works for Skookum Digital Works by day and manages the SitePoint Ruby channel at night. He likes to pretend he has a secret identity, but can't come up with a good superhero name. He's settling for "Roob", for now.

loccasions
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week