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: