2

I've spent a lot of time digging into sprockets' and tit's source code, trying to figure out how to pass variables / bindings to the Erb evaluation context. Here's what I'm trying to do: I need to serve a JS file whose contents change on a per-request basis. The portions that change depend on data stored in the DB, hence the need to route requests through the Rails app and the need to pass variables / bindings. On top of that the JS file uses the require directives to insert other JS files, hence the need to use sprockets.

Here's the code snippet that isn't working:

Controller file:

def ever_changing_js
  @foobars = Foobar.all
  MyApp::Application.assets.instance_eval do
    def foobars
      @foobars
    end
  end

  render :text => MyApp::Application.assets.find_asset('ever_changing.js').to_s, :content_type => "application/javascript"
end

ever_changing.js:

//= require file1.js
//= require file2.js

// Some code that uses @foobars

How can I get this done? Any help would be appreciated.

Saurabh Nanda
  • 6,373
  • 5
  • 31
  • 60

2 Answers2

0

JavaScript files should be completely static; Sprockets is not meant to do what you are trying to do.

Any data that changes on a per-request basis should be written to a <script> tag at the bottom of the template you are rendering.

app/assets/javascripts/user.js

(function(exports) {
  function User(name) {
    this.name = name;
  }

  User.prototype.speak() {
    console.log(this.name + ' says, "Hello!"');
  };

  exports.User = User;
})(this);

app/views/users/show.html.erb

...

  <%= javascript_include_tag('user') %>
  <script>
    (function() {
      var user = new User(<%= @user.name %>);

      $('#speak-button').click(function() {
        user.speak();
      });
    })();
  </script>
</html>

If you can give more context around your specific use case, I can give a more specific example.

Ross Allen
  • 43,772
  • 14
  • 97
  • 95
  • Thanks for your answer ssorallen. Unfortunately, in my case there is no associated template file. Let me explain. The JS in question, can be used by various users of the application to render a widget on their website. (Think of a live chat widget that people install on their websites). Now each user may have different settings + data that need to be referenced into the JS file. I want to wrap the static part of the JS and the dynamic part of the JS into one file and serve it. Does this make sense? – Saurabh Nanda Nov 04 '12 at 03:15
  • JavaScript APIs like Google Maps serve the same JS to every client and ask that the client instantiate the objects and APIs they want. – Ross Allen Nov 04 '12 at 04:13
  • If you *really* want a dynamic JavaScript file, name your template like `.js.erb`, and Rails will return the proper MIME type for JavaScript for the request. It should live in `app/views` instead of `app/assets`. – Ross Allen Nov 04 '12 at 04:17
  • I can't ask the client to instantiate objects the way they like because that would become too technical. Ideally the client would use a web-UI to change the settings and would include just one JS file in his/her website. – Saurabh Nanda Nov 04 '12 at 09:08
  • If I use a `.js.erb` file can I use `//= require` directives on the top? I need the final JS file to be a concatenated version of 3-4 other JS files + the dynamic bits. – Saurabh Nanda Nov 04 '12 at 09:09
0

I am trying to accomplish the same thing you are. I see a couple problems with your controller code snippet. Rather than doing an instance_eval on the Sprockets::Environment, you should class_eval the context_class, as shown in the Sprockets::Context documentation.

MyApp::Application.assets.context_class.class_eval do
  def foobars
    @foobars
  end
end

Then foobars will be available to your ERb template.

As a sidenote, you can do

render js: MyApp::Application.assets.find_asset('ever_changing.js').to_s

instead of setting the content type yourself.

Adam Stegman
  • 1,013
  • 10
  • 14