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

Glenn Goodrich
Ruby Editor
This entry is part 11 of 15 in the series Loccasions

Loccasions

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.

Loccasions

<< Loccasions: Hiring a Foreman, Inheriting Resources, & OccasionsLoccasions: Getting to Occasions >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Thomas Davis

    Love your approach to testing, much like mine! Will link back here when I have done my backbone testing tutorial.

    Also thanks for links in to two of my sites(http://thomasdavis.github.com and http://backbonetutorials.com)

    • http://www.ruprict.net/ Glenn Goodrich

      Thanks Thomas…I use http://backbonetutorials.com a ton, so I should definitely be thanking YOU. Keep up the great work.

  • http://blog.firsthand.ca Nicholas Henry

    The link to Backbone is broken, you may wish to use this one:
    https://raw.github.com/documentcloud/backbone/master/backbone.js

    • http://www.ruprict.net/ Glenn Goodrich

      Weird…it works on one click, and not the other. Freaky. Thanks for pointing that out.

  • http://blog.firsthand.ca Nicholas Henry
    • http://www.ruprict.net/ Glenn Goodrich

      Ahhh….life on the edge. I’ll have to look into how/if this changes the app. Cheers.

  • http://blog.firsthand.ca Nicholas Henry

    This is the correct link to the Jasmine/Sinon series:
    http://tinnedfruit.com/2011/03/03/testing-backbone-apps-with-jasmine-sinon.html

    (slug is all lowercase otherwise you get a 404)

    • http://www.ruprict.net/ Glenn Goodrich

      Fixed, thanks.

  • http://blog.firsthand.ca Nicholas Henry

    Typo: ‘keeping out tests light’ should be ‘keeping our tests light’

  • http://blog.firsthand.ca Nicholas Henry

    I see that for some vendored javascripts you have added the minified version, for example underscore-min.js. Would it make sense to use the non-minified version as the asset pipeline is going to do this for you anyway? I guess my concern is minifying a previously minified version worries me. Absolutely no experience or evidence for my concerns however :-) I guess it keeps the style of files consistent though.

  • http://blog.firsthand.ca Nicholas Henry

    In the MapView test on line 14, some random HTML tags appear: .

  • http://blog.firsthand.ca Nicholas Henry
    • http://www.ruprict.net/ Glenn Goodrich

      Fixed

  • http://blog.firsthand.ca Nicholas Henry

    For those following at home, once you have finished this post, there’s a couple of things you will need to do to for the map display.

    * Add the div #map to your events/index.html.haml
    * Add #map { height: 350px }

    You need the div#map element in the DOM for the MapView to display. If you don’t add the height, the map will be in the DOM, but with a 0px height.

    • http://www.ruprict.net/ Glenn Goodrich

      Added to post…cheers.

  • http://blog.firsthand.ca Nicholas Henry

    Awesome post, Glenn. Here’s a couple of extras I tried that you might be interested in:

    * I added https://github.com/bradphelan/jasminerice which allowed me to right my specs in CoffeeScript and my fixtures in Haml. Switch back and forth between Javascript and CoffeeScript and HTML/Haml was jarring. This solved the problem. You don’t need to run a separate server for your specs, with this gem, just access it from: http://localhost:3000/jasmine. I really liked writing my specs in CoffeeScript, they are definitely easier to read. You can checkout it in my repo: https://github.com/nicholasjhenry/loccasions/blob/master/spec/javascripts/views/map_view_spec.js.coffee

    * Also https://github.com/netzpirat/guard-jasmine, along with phantomjs (brew install phantomjs) provides for headless testing of your JS/CoffeeScript. Super cool!

    • http://www.ruprict.net/ Glenn Goodrich

      Great info, Nicholas.

      I am still on the fence with coffeescript. I can see both sides of the argument. You’re right about one thing, though, having them both in the project is probably NOT the way to go.

  • Pedro Mendes

    Thanks for a really informative post.

    I’m fairly inexperienced with backbone.js but I’ve been studying it in order to start developing large webmapping applications integrated with jquery ui and OpenLayers. We’ve already been using both OpenLayers for the webmapping capabilities and jQuery UI as the widget provider in some of our apps.

    After some documentation and a few tutorials I can say I’ve a minimum understanding of the backbone.js concepts and patterns but I’m not quite picturing how and where could I integrate the implementation of map controls and events (OpenLayers.Controls) within backbone modular approach…

    Any thoughts on this?

    Thanks!

    • http://www.ruprict.net/ Glenn Goodrich

      Hey Pedro,

      I am hoping to cover that in the next post in the series when I react to map events within the Backbone structure. Stay tuned, and we can revisit your question after that post (should be out in the next 2 weeks)

      Glenn

      • Pedro Mendes

        Great!

        If I got the time before the next post to investigate these issues myself I’ll provide some feedback here.

  • sandy

    hi!!
    i just stuck in the begin of this part!!
    when i ran jasmine all i have back is

    Uncaught TypeError: Cannot set property ‘Leaflet’ of undefined in the console and all myy spec failled!!

    i try with:

    App.MapProviders.Leaflet = ->
    # Create new map
    createMap: (elementId) ->
    addBaseMap: ()->
    addLayerToMap: (layer) ->
    setViewForMap: (options) ->

    and i try with the gisht??

    i think you should have describe more some steps here !! totaly lost!!
    that tutoriel is a great material i want to finish the serie some help please!!!

  • bemonkey

    So sad defenely a great tutorial but this part is really losing me for the second time!!
    jasmine telling me : loadFixtures is not defined and
    Cannot read property ‘Leaflet’ of undefined

    so sad gonna give it a try nex year!!lol