3

In my view I want:

:coffeescript
  Gmap('#canvas').getAddressBounds request.term

which is defined in maps.js.coffee as

Gmap = (mapId) ->
  getAddressBounds: (address) ->
    data = []
    $(mapId).gmap3
      action: 'getAddress'
      address: address
      callback: (results) ->
        return unless results
        data = $.map results, (item) ->
          bounds: item.geometry.bounds
    data

This doesn't work though. First, there's a scope issue. The Gmap function is not visible to the script in the view. If I add the code directly to the view, Gmap is visible, but data always returns as [].

Gavin
  • 4,273
  • 3
  • 27
  • 39
  • The scope issue you describe has become the most common CoffeeScript question on StackOverflow; see http://stackoverflow.com/questions/6089992/cant-find-variable-error-with-rails-3-1-and-coffeescript and several others. – Trevor Burnham Jul 26 '11 at 14:45

2 Answers2

5

What's going on is that you're treating asynchronous code as if it's synchronous. Some debug output might help you visualize this:

Gmap = (mapId) ->
  getAddressBounds: (address) ->
    data = []
    console.log '1: Calling gmap3'
    $(mapId).gmap3
      action: 'getAddress'
      address: address
      callback: (results) ->
        console.log '3: Callback called'
        return unless results
        data = $.map results, (item) ->
          bounds: item.geometry.bounds
    console.log '2: Returning data'
    data

When you pass a callback, it could get called at any time. If it were called during the gmap3 function, then data would indeed be set before being returned. But the reason gmap3 uses a callback to return its result, rather than just returning, is that the function is asynchronous—in particular, it calls the callback when the server responds to your query. The way that JavaScript does events, that means your callback is guaranteed not to be called until after all code has finished executing.

There's no way to wrap an asynchronous function in a synchronous one in JavaScript (or CoffeeScript); even running an infinite loop until your callback is called wouldn't work, because, again, the JS runtime doesn't handle events like server responses (or even user input events) until all code has finished executing. So all you can do is change your function, too, to use a callback:

Gmap = (mapId) ->
  getAddressBounds: (address, cb) ->
    $(mapId).gmap3
      action: 'getAddress'
      address: address
      callback: (results) ->
        return unless results
        cb $.map results, (item) ->
          bounds: item.geometry.bounds

Then call it like so:

 Gmap('#canvas').getAddressBounds request.term, (data) -> console.log data

I talk a little bit more about the JS event model in my CoffeeScript book. Also, John Resig's How JavaScript Timers Work is a must-read. Asynchronicity takes some getting used to, but the benefits over multithreading are spectacular.

Trevor Burnham
  • 76,828
  • 33
  • 160
  • 196
2

change Gmap = ... to window.Gmap = ....

will solve the problem. The reason is coffeescript wraps everything in an anonymous function. If you want a more feature full module system have a look here

https://github.com/jashkenas/coffee-script/wiki/Easy-modules-with-coffeescript

bradgonesurfing
  • 30,949
  • 17
  • 114
  • 217