How do you write DRY, modular coffeescript with Sprockets in Rails 3.1?
Asked Answered
W

3

15

I'm in the early stages of trying to write some sensible Javascript. I want to namespace basically everything under the name of my application to avoid globals as much as possible, but still give me a way to access functions declared around the place. However, I don't want to be super verbose in my function definitions.

My ideal CoffeeScript would be something like this:

class @MyApp
  @myClassMethod = ->
    console.log 'This is MyApp.myClassMethod()'

  class @Module1
    @moduleMethod = ->
      console.log 'This is MyApp.Module1.moduleMethod()'

You get the picture. This way I avoid having to write MyApp.Module.submoduleMethod = -> every time I want to define a namespaced function properly - using @ and defining things within my class definition keeps things nice and short.

This is all going well until I want to split my functionality up into multiple CoffeeScript files. Then what I really want is something like this:

// application.js
class @MyApp
  //= require 'module1'
  //= require 'module2'

// module1.js
class @Module1
  @moduleMethod = ->
    console.log 'This is STILL MyApp.Module1.moduleMethod()'

It doesn't seem like Sprockets can do this.

Is there a sensible way to require my CoffeeScript files in the right place in my container files? Or another way to approach writing modular code that is divided into separate files using CoffeeScript, Sprockets and Rails 3.1?

Wildfire answered 25/7, 2011 at 12:15 Comment(1)
I think this question needs further investigation - the answers below aren't good enough, esp. since the creator of coffeescript has removed the "easy modules" page b/c the technique is poor.Blanc
M
3

I have a module solution that I use in my code.

I define my modules like below

@module "foo", ->
    @module "bar", ->
        class @Amazing
            toString: "ain't it"

Amazing is available as

foo.bar.Amazing

implementation of the @module helper is

window.module = (name, fn)->
  if not @[name]?
    this[name] = {}
  if not @[name].module?
    @[name].module = window.module
  fn.apply(this[name], [])

It's written up on the coffeescript website here.

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

Machicolation answered 26/7, 2011 at 6:47 Comment(5)
Thanks for the link. Any idea how to extend this so that it works nicely across multiple files?Wildfire
It does work across multiple files. This is the whole point of it. What do you mean by nicely?Machicolation
I meant without having to define all of the parent modules in each file. This may well be less annoying than I first thought though.Wildfire
Where do you put the @module "foo", -> code? Can you put it in application.js? I thought it would need a .coffee extension...Blanc
@bradgonesurfing: The creator of coffeescript removed the link - he apparently thinks that it's not so good... so is there a proper way to organize JS files? For instance, I can't define constants or "instance variables" to be used throughout a "class" using the method that used to be given at the broken link...Blanc
E
4

Simply keep module1.js as-is and make application.js look something like this:

//= require 'module1'

class @MyApp
  ...

  @Module1 = Module1

This will work because you've made Module1 a global (declaring class @Module1 is equivalent to writing @Module1 = class Module1, and @ points to window in that context), and within the class @MyApp body, @ points to the class itself.

If you want Module1 to only be a property of the global MyApp class after it's attached, you could add the line

delete window.Module1
Elongate answered 25/7, 2011 at 14:24 Comment(3)
Good idea! There would still be a problem if my module names clashed with names in other scripts on the page, right? I'm being picky because I'm writing code that probably will be injected into other pages using a bookmarklet.Wildfire
Ah, in that case I'd just define one global and then attach everything else to it. To do that, flip your dependency order around: In application.js, require module1.js, and in module1.js, require myapp.js, so that the global MyApp is defined first. You can then write class MyApp.Module...Elongate
My answer above handles the name collision problem. Using class is nice but it's not like in ruby where you can monkey patch. It will wipe out any previously define classes. #6816457 gets around thisMachicolation
M
3

I have a module solution that I use in my code.

I define my modules like below

@module "foo", ->
    @module "bar", ->
        class @Amazing
            toString: "ain't it"

Amazing is available as

foo.bar.Amazing

implementation of the @module helper is

window.module = (name, fn)->
  if not @[name]?
    this[name] = {}
  if not @[name].module?
    @[name].module = window.module
  fn.apply(this[name], [])

It's written up on the coffeescript website here.

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

Machicolation answered 26/7, 2011 at 6:47 Comment(5)
Thanks for the link. Any idea how to extend this so that it works nicely across multiple files?Wildfire
It does work across multiple files. This is the whole point of it. What do you mean by nicely?Machicolation
I meant without having to define all of the parent modules in each file. This may well be less annoying than I first thought though.Wildfire
Where do you put the @module "foo", -> code? Can you put it in application.js? I thought it would need a .coffee extension...Blanc
@bradgonesurfing: The creator of coffeescript removed the link - he apparently thinks that it's not so good... so is there a proper way to organize JS files? For instance, I can't define constants or "instance variables" to be used throughout a "class" using the method that used to be given at the broken link...Blanc
B
1

Here's the modular pattern I use for managing coffeescript with sprockets (works with Rails 4 as well):

  # utils.js.coffee

  class Utils
    constructor: ->

    foo: ->
      alert('bar!!!')

    # private methods should be prefixed with an underscore
    _privateFoo: ->
      alert('private methods should not be exposed')

  instance = new Utils()

  # only expose the methods you need to.
  # because this is outside of the class,
  # you can use coffee's sugar to define on window

  @utils = foo: instance.foo

  # otherscript.js.coffee 

  //= require utils
  class OtherScript
    constructor: ->
      @utils.foo()         # alerts bar!!!
      @utils._privateFoo() # undefined method error

One disadavantage of this approach is that you are exposing your objects on window. Adding a module loader or adopting some of the new es syntax around modules could be a nice alternative depending on your needs.

Bandaranaike answered 8/9, 2013 at 19:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.