#297 Running JavaScript in Ruby pro
- Download:
- source codeProject Files in Zip (110 KB)
- mp4Full Size H.264 Video (36 MB)
- m4vSmaller H.264 Video (17.1 MB)
- webmFull Size VP8 Video (18.9 MB)
- ogvFull Size Theora Video (40.2 MB)
As web applications become complex you can find yourself duplicating logic on in the Ruby code on the server and JavaScript code on the client. A common situation for this is validations and we have an example of this in the application below.
This page has a form with a field that accepts a membership number. If we enter a random number in this field we’ll see an error message but when we enter a valid membership number the error disappears.
The validation only takes place on the client which presents a problem. If a user who has disabled JavaScript in their browser submits the form the validation will be skipped and for this reason it’s important to validate form input on the server too. The validation logic for the membership number is based on the mod 10 algorithm and is fairly complex.
// Validates using the mod 10 algorithm // Example valid number: 49927398716 function isValidMembershipNumber(number) { var total = 0; for (var i=number.length-1; i >= 0; i--) { var n = +number[i]; if ((i+number.length) % 2 == 0) { n = n*2 > 9 ? n*2 - 9 : n*2; } total += n; }; return total % 10 == 0; }
So, we have a choice: we can either replicate this logic on the server in Ruby or we could execute the JavaScript in our Ruby code on the server. If you have a Rails 3.1 application you may have already been doing this without realising it. The Gemfile of a Rails 3.1 application includes the coffee-rails
gem and this gem executes JavaScript in order to compile the application’s CoffeeScript code using a gem called ExecJS.
ExecJS supports a number of JavaScript runtimes and will choose the best one that’s available on the system it’s running on. It provides a common interface for interacting with and compiling JavaScript code and can be used to execute JavaScript, where it will return a Ruby object, or to compile a JavaScript source file. We’ll use it to read the membership number validation JavaScript file and execute the isValidMembershipNumber
function so that we can validate membership numbers on the server.
Validating Membership Numbers on The Server
We can add validation to the Member model through a custom validation method.
class Member < ActiveRecord::Base validate :check_membership_number def check_membership_number source = File.read(Rails.root.join("app/assets/javascripts/membership_number.js")) context = ExecJS.compile(source) unless context.call("isValidMembershipNumber", membership_number) errors.add :membership_number, "is an invalid number" end end end
This method reads the membership_number.js
file then uses ExecJS to compile it. This returns a Context
that we can call various methods against, one of which is call
which lets us execute JavaScript functions. The first argument we pass to call
is the name of the function we want to execute; any other arguments are passed to this function. We pass in the Member
’s membership_number
. If this function returns false we add an error to the list of the model’s errors.
If we fill in the form with an invalid membership number now and submit the form without JavaScript enabled the form is posted back to the server and the app falls back to the old validation behaviour in Rails and tells us that the number is invalid. If we enter a valid number and name the new member is successfully added.
This works and it means that we’re now able to share logic by writing it in JavaScript for the client then compiling and running the same code on the server. That said, we’re reading and compiling the JavaScript file every time the form is validated and this isn’t particularly efficient. We should be caching the context and we’ll show how that’s done near the end of the episode.
Executing JavaScript Code in Ruby
Next we’ll take this a step further and interact with JavaScript as if it were Ruby. One of the JavaScript runtimes that ExecJS supports is The Ruby Racer and there a many great things we can do with it. We’ll add it to our application now and then use the console to demonstrate what it can do.
To install The Ruby Racer we add it’s gem to the Gemfile
and tell it to require V8. We then run bundle
to install it.
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'mustache' gem 'therubyracer', require: 'v8'
We can now evaluate JavaScript code in the console. First we’ll need to create a V8 context.
ruby-1.9.2-p290 :002 > c = V8::Context.new
If we call eval on this context and pass in some arbitrary JavaScript we’ll get a response in Ruby.
ruby-1.9.2-p290 :003 > c.eval('1 + 2') => 3
We can also define variables for the context in Ruby then use them in JavaScript.
ruby-1.9.2-p290 :004 > c[:num] = 3 => 3 ruby-1.9.2-p290 :005 > c.eval('num * num') => 9
We can even create JavaScript objects and then use them in Ruby code.
ruby-1.9.2-p290 :007 > c.eval('math = {square: function (n) { return n*n; }}') => [object Object] ruby-1.9.2-p290 :008 > c[:math].square 5 => 25
Alternatively we can go the other way and pass in a Ruby lambda and call in in JavaScript code where it will be evaluated.
ruby-1.9.2-p290 :014 > c[:square] = lambda { |n| n*n } => #<Proc:0x00000128f467e8@(irb):14 (lambda)> ruby-1.9.2-p290 :015 > c.eval('square(4)') => 16
A Practical Example
There a number of things we can do with The Ruby Racer to bridge the gap between Ruby and JavaScript. Next we’ll show a practical example of how we can use it to reduce duplication in an application. Shown below is a page from the app we wrote in episode 295. This page shows a list of products and adds more products to the page each time we scroll down towards the bottom.
The application uses a Mustache template to render out the products. The template is used by Ruby to render out the first page of products then by JavaScript to render out the rest along with some JSON data from the Rails application. When we share templates like this we often have shared logic, too, and in this case the logic determines how the price and release date of each item are formatted. We solved this problem in episode 295 by formatting the data on the server before sending it out to the client and while this approach worked for this situation it’s better to work with the raw JSON data in JavaScript and format the data on the client.
If we look at the ProductsController
’s index
action we’ll see that when we return a list of products as JSON we call a product_for_mustache
method on each product to format it for display in the browser.
def index @products = Product.order("name").limit(10) @products = @products.offset((params[:page].to_i-1)*10) if params[:page].present? respond_to do |format| format.html format.json do render json: @products.map { |p| view_context.product_for_mustache(p) } end end end
We’ll change this so that we return a raw list of products for formatting on the client.
format.json do render json: @products end
The formatted JSON response we’ve just removed contains a url
attribute that isn’t available in the raw response. We’ll need to change our Mustache template to use the id
instead and hard-code the rest of the path in.
<div class="product"> <h2><a href="/products/{{id}}">{{name}}</a></h2> <div class="details"> {{price}} {{#released_at}} | Released {{released_at}} {{/released_at}} </div> </div>
We could customize the JSON output so that it returns the URL for each product, but we won’t do that here.
In the index
view we use the product_for_mustache
method again to render the first page of products. We’ll remove this and pass in each product’s JSON representation instead.
<div class="new_member_link"><%= link_to "New Member", new_member_path %></div> <h1>Products</h1> <div id="products" data-json-url="<%= products_url(:format => :json) %>"> <% @products.each do |product| %> <%= render "product", :mustache => product.as_json %> <% end %> </div> <script type="text/html" id="product_template"> <%= render "product" %> </script>
When we reload the page now the price and release date are no longer formatted. Instead we see the raw strings from the JSON representation of each product both for the initial list of products that are rendered by the server and for ones rendered by JavaScript.
We’ll fix the ones that are rendered on the client first. If we look at the JavaScript that renders these products we’ll see that it loops through each product and passes it to the Mustache template to render it out.
render: (products) => for product in products $('#products').append Mustache.to_html($('#product_template').html(), product) $(window).scroll(@check) if products.length > 0
We’ll change this and make an intermediate object that we pass the JSON attributes to. This object will handle the formatting of each product and we’ll pass this object to the Mustache template instead of the JSON data. To do this we’ll rename the product variable to product_attributes
and create a new Product
object from these attributes. We’ll then pass this new object to the template.
render: (products) => for product_attributes in products product = new Product(product_attributes) $('#products').append Mustache.to_html($('#product_template').html(), product) $(window).scroll(@check) if products.length > 0
We’ll need a Product
class and we’ll do that in a new product.js.coffee
file.
class Product constructor: (@attributes) -> id: -> @attributes.id name: -> @attributes.name price: -> "price" released_at: -> "date" @Product = Product
Our new class’s constructor takes the product_attributes
we pass in. This constructor syntax automatically assigns the value passed to an attributes
variable in the class. We then set four properties on the class to match each property. For now we’ve just hard-coded a value for the name
and price
. We’ll add the correct values and formatting later.
The last line in the code above makes the class available outside the file by setting this.Product
to the Product
class. In this context this refers to the browser’s current window.
When we reload the page now the first ten products, the ones rendered on the server, stay the same but any ones added by JavaScript show the placeholder data from our new Product
class. We need to replace this placeholder text with the correctly formatted price and date but before we do that we’ll focus on adding this behaviour to the Ruby code so that all the products look the same and are all formatted by executing that JavaScript.
We need to change the object that we pass to the Mustache template in the index view. We’re currently passing in a JSON representation of a product but instead we want to pass in a product based on our new Product
JavaScript class. We’ll do this in the Product
model in a new for_mustache
method.
class Product < ActiveRecord::Base def for_mustache context = V8::Context.new coffee = File.read(Rails.root.join("app/assets/javascripts/product.js.coffee")) javascript = CoffeeScript.compile(coffee) context.eval(javascript) context.eval("new Product(#{to_json})") end end
Our Product
class is written in CoffeeScript so we need to compile it before we can load it into the context
. We do this by calling CoffeeScript.compile
(this is provided by the CoffeeScript gem which is already loaded in Rails 3.1). We load the Product
class by calling eval
on the resulting JavaScript then return the new product by creating a new instance of this class and passing in a JSON representation of the product, mimicking what we do on the client.
The for_mustache
method returns a Ruby object, but as this just wraps a JavaScript object it will behave the same way when we pass it to the Mustache template on the server. We’ll modify the view now to use our new method.
<%= render "product", :mustache => product.for_mustache %>
Now when we reload the page we see the placeholder data for all of the products as the Product
JavaScript class is used on both the server and client to render the products.
Next we’ll need to modify the placeholder data in the Product
class with the code to format the price and release date.
class Product constructor: (@attributes) -> id: -> @attributes.id name: -> @attributes.name price: -> digits = String(Math.round(@attributes.price * 100)).split("").reverse() formatted = [digits.shift(), digits.shift(), "."] for digit, index in digits formatted.push(digit) if (index+1) % 3 == 0 && index+1 < digits.length formatted.push(",") "$" + formatted.reverse().join("") released_at: -> months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] date = new Date(@attributes.released_at) day = date.getDate() month = months[date.getMonth()] year = date.getFullYear() "#{month} #{day}, #{year}" @Product = Product
When we reload the page now the price and date are formatted correctly.
Caching For Better Performance
Our new functionality is pretty much complete, but what about performance? Our for_mustache
method isn’t currently very efficient as for every product it renders it has to read the CoffeeScript file, compile it to JavaScript and then parse it. It would be much better if we could cache the context
so that the method doesn’t have to go through all this every time.
A quick way to do this is to store the context in a thread variable.
class Product < ActiveRecord::Base def for_mustache Thread.current[:product_v8] ||= V8::Context.new.tap do |context| coffee = File.read(Rails.root.join("app/assets/javascripts/product.js.coffee")) javascript = CoffeeScript.compile(coffee) context.eval(javascript) end Thread.current[:product_v8].eval("new Product(#{to_json})") end end
We only set this variable if it’s not set already. We can then tap
the context and put the code that fetches and parses the CoffeeScript code in a block so that it’s only parsed once.
One thing we need to watch for when we’re caching an entire context is memory leaks. The thread variable will never be garbage collected and if we do something complex in the JavaScript that it runs then it will use more and more memory that will never be released. The code we’ve written doesn’t seem to leak so it seems that instantiating a class this way seems to work fine, but it’s something to be aware of. If you’d rather play it safe then you could just catch the JavaScript string using something like Memcached and evaluate that in a new context each time.