\_@yarmand coding tales_/

Old school programmer adventures in a world of stratuper.

Javascript Initializers to Your Views With Rails 3.1 and CoffeeScript

| Comments

The Problem

With the asset pipeline included in Rails 3.1, we have a great big javascript file including all at once our code.

But what about code isolation through all our sources files ?

CofeeScript respond to that perfectly with creation of one function object per file compiled. To add a global variable, you simply define it for the window object :

1
2
window.foo= () ->
  console.log('foo')

So all our javascript code is executed when it loads with good code isolation, but what if we want part of the code executed only for one view or all views of a controller ?

call initializers in your layout

I first wrote the gem Epyce to limit code loaded to only current controller and view, but this had 2 main issues :

  • loose benefit of one big js file to be put on a CDN
  • shared code between views must go to controller .js file

Then I rollback to the big application.js file and add initializers registration to my layout.

Like this for erb:

application.html.erb
1
2
3
4
5
6
    <script type='text/javascript'>
      $(document).ready( function() {
        Rails.init('<%="#{controller.controller_name}"%>');
        Rails.init('<%="#{controller.controller_name}_#{controller.action_name}"%>');
      });
    </script>

or if like me you are a haml:

application.html.haml
%script{type: 'text/javascript'}
  $(document).ready( function() { Rails.init("#{controller.controller_name}"); Rails.init("#{controller.controller_name}_#{controller.action_name}");});

Register initializers

Each initializer function can be binded to severals names.

By convention views are named => controllerName_viewName

here is an example of binding a initializer to the Person controller

persons.js.coffee
1
2
Rails.register_init 'persons', () -> 
  console.log('hello from Person controller')

Here is an example of binding several views to one initializer

persons.js.coffee
1
2
Rails.register_init ['persons_new','persons_edit'], () ->
  console.log('editing a Person')

initializers are cumulative

Calling register_init() several times on the same name cumulate initializers

for example:

1
2
3
4
5
Rails.register_init 'mytest', () -> console.log('foo')
Rails.register_init 'mytest', () -> console.log('bar')

# ... later in code ...
Rails.init('mytest')

will produce in console:

foo
bar

The registering code

To make all the previous code work, we need to define the Rails object.

application.js.coffee
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#= require_self
#= require_tree ./application

# From http://yehudakatz.com/2011/08/12/understanding-prototypes-in-javascript/
fromPrototype = (prototype, object) ->
  newObject = Object.create(prototype)
  `for(prop in object)
    {
      if(object.hasOwnProperty(prop))
        newObject[prop] = object[prop];
    }`
  return newObject


window.Rails=fromPrototype Array,
  register_init: (names, fun) ->
    if(typeof(names.forEach) == 'undefined')
      n=names
      names=[n]
    for n in names
      previously_registered=this[n]
      this[n]=() ->
        if(typeof(previously_registered) != 'undefined')
          previously_registered.call()
        fun.call()
  init: (name) ->
    if(typeof(this[name]) != 'undefined')
      this[name]()

Note the usage of #= require_self to ensure your Rails object will be defined before including all our others files


One more thing

When registered, initializers can be called using init() and there name as string or directly on the Rails object.

1
2
3
4
5
6
Rails.register_init 'mytest', () -> console.log('foo')

# ... later in code ...
Rails.init('mytest')
# or directly
Rails.mytest()

Biblio and links

I will finish with a great thanks to Yehuda Katz for his 2 essential posts about Javascript :

links:

Comments