Blissfully Reactive Bootstrap3 Tooltips with Meteor Templates

It’s been a long time since I wrote something here. I thought I would break the silence with a new post about Meteor.

Meteor makes it pretty easy to start prototyping a new project very quickly.
When you want your ol’ familiar Bootstrap for CSS, you just meteor add bootstrap and there it is, including the stylish $el.tooltip() component.

So while building templates in Meteor, I thought I had tooltips figured out. I’d write them something like this:

my_awesome_template.html


my_awesome_template.coffee

Template.my_awesome_template.rendered = ->
  @$('[title]').tooltip()

Template.my_awesome_template.tooltip_label = ->
  "It has #{Likes.find({}).count()} like(s)!"

Which looked great, but I quickly came to realize that when my Likes Collection changed, the bootstrap tooltip wasn’t getting refreshed, even though the underlying title attribute was. I tried using several event binding techniques like observe to try and refresh the tooltip whenever the collection changed, but those strategies weren’t panning out. I wasn’t interested in squirreling away a lot of tooltip code into my subscription callbacks, either.

It turns out, I was way over-complicating this. You see, I already have a function that was executing every time the label changed, it was just a tiny bit too fast. I wanted to refresh the tooltip AFTER the tooltip_label function ran, so this is what I did:

my_awesome_template.coffee

Template.my_awesome_template.tooltip_label = ->
  setTimeout (->
    $('#link').tooltip('fixTitle')
    ), 0
  "It has #{Likes.find().count()} like(s)!"

By calling setTimeout with a zero delay, I queued up the code to refresh the tooltip immediately after the function returns. A simple no cruft way to include reactive Bootstrap Tooltips in your Meteor Templates.

If you prefer to be educated through interactive media, here is a link to an example on Meteor Pad.

Updated August 10, 2014:
Turns out you can use the Meteor.defer function to do the same thing as setTimeout in an even cleaner way. Now my CoffeeScript looks like this:

my_awesome_template.coffee

Template.my_awesome_template.tooltip_label = ->
  Meteor.defer -> $('#link').tooltip('fixTitle')
  "It has #{Likes.find().count()} like(s)!"

Is your Javascript waiting on multiple Backbone fetches?

I’ve been working with Backbone for approximately a month now. I’m a fan of using a structured coding practice to untangle your objects and the DOM.
In Backbone, you get the latest state of your model by fetching it from the server. You can set a callback on the fetch function directly or by binding to the collection’s reset event.

Tracking fetched collections manually

I needed to fetching more than one collection at the same time and call a callback when all of the fetches were finished. My first attempt wasn’t very good. I’ll give you a largely trivialized sample of what I had going on.

This is an example of a view written in CoffeeScript. I want to fetch 3 different collections, render them using Handlebar templates, and call a function when all 3 collections are rendered:

initialize: =>
  @apples = new MyModels.Apples
  @oranges = new MyModels.Oranges
  @bananas = new MyModels.Bananas
  @bindTo(@apples, 'reset', @renderApples)
  @bindTo(@oranges, 'reset', @renderOranges)
  @bindTo(@bananas, 'reset', @renderBananas)

render: =>
  @apples.fetch()
  @oranges.fetch()
  @bananas.fetch()

This actually isn’t bad so far. We create our models and bind an event so that when they are fetched, their respective render functions will be called. The real mess came later in the same file.

renderBananas: =>
  @$el.html(HandlebarsTemplates["bananas"](
    bananas:@bananas.toJSON()))
  Backbone.ModelBinding.bind(this)

  @bananasLoaded = true
  @finishLoading()

finishLoading: =>
  return unless @applesLoaded
  return unless @orangesLoaded
  return unless @bananasLoaded

  alert("All collections loaded!")

For brevity, I left off renderApples and renderOranges. Those functions are identical to renderBananas with their respective fruits substituted in. This solution isn’t very scalable, as we need to create more state variables to track any new collections that get added in the future and make sure we account for those variables when we finish loading.
It would be better if we could define a callback that would wait for all of my fetch calls to finish. It would be even cooler if these things chained together.

Making promises

What I needed to be using are jQuery Deferred Objects. See, Backbone server calls (like fetch and save) are just $.ajax() calls under the hood, and since jQuery 1.5, $.ajax() calls implement an immutable version of Deferred Object called a Promise. As it happens, everything in that code sample is already defer-able.
I’ll modify the view to make use of Promises.

initialize: =>
  @apples = new MyModels.Apples
  @oranges = new MyModels.Oranges
  @bananas = new MyModels.Bananas
  # Dropped the bind calls, we won't be needing them!

fetchCollections: =>
  # Combines these individual promises into one.
  $.when(
    @apples.fetch(),
    @oranges.fetch(),
    @bananas.fetch()
  ).promise()

renderCollections: =>
  $.when(
    @renderApples(),
    @renderOranges(),
    @renderBananas()
  ).promise()

renderBananas: =>
  d = @$el.html(HandlebarsTemplates["bananas"](
    bananas:@bananas.toJSON()))
  Backbone.ModelBinding.bind(this)
  d.promise()

finishLoading: =>
  alert("All collections loaded!")

With all of this in place, the render function shown below is remarkably straightforward, and communicates the behavior of the view beautifully!

render: =>
  @fetchCollections()
    .then(@renderCollections)
    .then(@finishLoading)

This function is so much better than before; it tells me everything that’s going to happen in a clean and concise way.
An additional enhancement I’ve snuck in was to return promises for my .html() calls in the renderBananas function. Now I know that my HTML templates will be rendered before finishLoading gets called. This will protect me from making a mistake like attempting to focus() an element that maybe hasn’t been rendered yet.