Robert Mosolgo

To my knowledge, batman.js is not maintained. For that reason, I don't suggest that you use it for a new project!

Dynamic Navigation View With Batman.js

Creating a dynamic, context-based navigation menu is a breeze with batman.js and batman-rails thanks to Batman’s data-route view binding and the object[key] view filter. Here’s how I did it.

I have Rails/Batman app generated with batman-rails generators. I’m using Slim-assets for my Batman HTML. Source of the app is here.

Models

My app allows users to design crepes, and so far it has two models, Crepes and Ingredients:

/app/assets/javascripts/batman/models/crepe.js.coffee
1
2
3
4
class Creperie.Crepe extends Creperie.ApplicationModel
  # ... stuff
  toString: ->
    "#{@get('name')} ($#{@get('price')})"
/app/assets/javascripts/batman/models/ingredient.js.coffee
1
2
3
4
class Creperie.Ingredient extends Creperie.ApplicationModel
  # ... stuff
  toString: ->
    "#{@get('name')} (#{@get('category')})"

I defined toString on both of the models because when a JS object instance is rendered as text, toString is called automatically.

Layout

My app defines /crepes and /ingredients, and I want each of those pages to show a side-bar nav that lists all items and provides a link to create a new item. So, I added a nav to my Rails layout where I will render my Creperie.ContextNavView. I attach the Batman.View with the data-view view binding:

/app/views/layouts/batman.html.slim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
doctype html
html
  head
    title data-bind='Title | default "Home" | prepend "Creperie | "'
    = stylesheet_link_tag    "application", :media => "all"
    = javascript_include_tag "creperie"
    = csrf_meta_tags
  body
    header
      section
        h1 Creperie!
      nav data-view='NavBarView'

    / here is my context view:
    nav data-view='ContextNavView'

    section#main data-yield='main'

  script type="text/javascript"
    | Creperie.run();

View

Then, I defined a custom Batman.View called ContextNavView.

I defined the prototype’s source attribute to point to the HTML template which I will create next. That way, Batman knows where to find the HTML for this view. This is the same as passing the source to the constructor, eg, new Creperie.ContextNavView(source: 'layouts/context_nav').

I also defined the viewDidAppear hook for this view. You can define hooks for any point in a Batman.View’s lifecycle. I set up an observer on my app’s currentRoute.controller.

/app/assets/javascripts/batman/views/context_nav_view.js.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
class Creperie.ContextNavView extends Creperie.ApplicationView
  source: 'layouts/context_nav' # my HTML template

  viewDidAppear: ->
    Creperie.observe "currentRoute.controller", (newValue, oldValue) ->
      currentController = newValue
      itemClassName = Batman.helpers.singularize(
          Batman.helpers.camelize(currentController)
        ) # camelize and singularize the controller name
      itemClass = Creperie[itemClassName]
      if itemClass?
        @set 'itemClass', itemClass
        @set 'itemRoute', currentController

This is made possible because Batman (v 0.15.0) keeps track of the current route at MyApp.currentRoute (which you can access in your code or in the console as MyApp.get('currentRoute')). Since my controllers are all defined with Rails-style names, I can count on the controller names matching the model that I want to display.

Template

Last, I defined the template which ContextNavView uses as its source:

/app/assets/javascripts/batman/html/layouts/context_nav.html.slim
1
2
3
4
5
6
7
8
9
ul.items
  li.item data-foreach-item='itemClass.all'
    a data-route='routes[itemRoute][item]'
      span data-bind='item'
    a.edit data-route='routes[itemRoute][item].edit' edit
  li.new-item
    a data-route='routes[itemRoute].new'
      span New 
      span data-bind='itemClass.name'

The big win here was sending strings to App.routes with [] in the keypath. That way, I could meta-program my routes – I didn’t have to make them explicit.

So what?

  • Use MyApp.currentRoute to get information about the current page.
  • Subclass Batman.View to provide site-wide navs (or other views).
  • Meta-program your routes (or other parts of the nav) by using [] in your keypaths.