Pass variables to javascript with gon

If you haven’t noticed yet that there is a good solution passing Rails variables from controllers to javascript I will be happy to make you fun with gon. In this post I will show how to use it with controller’s filters in a clean, dry and the best way. This approach will help you to avoid growing controllers in a big monsters.
Using filters in contollers
If you don’t know what is contoller filters and how use them, checkout oficial documentation. In our case we have to know that there are posibilities to apply filters for one action and this one form of using:
class ApplicationController < ActionController::Base
before_filter LoginFilter
end
class LoginFilter
def self.filter(controller)
unless controller.send(:logged_in?)
controller.flash[:error] = "You must be logged in"
controller.redirect_to controller.new_login_url
end
end
end
Using Gon
Say, we have to pass location model as a json object from server side to client side (from rails controller to javascript). With gon we can reduce it to:
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
gon.locations = @product.locations.as_json(:only => [:latitude, :longitude], :methods => [:address])
end
end
Everything is ok, but if we have to pass a lot of variables to javascript we will get a fat controller. Also it’s not easy to test controllers. So my solution will be to use before_filter
here. Here is a example of fat controller’s action:
def show
gon.locations = @product.locations.as_json(:only => [:latitude, :longitude], :methods => [:address])
gon.current_user = current_user.as_json(:only => [:email, :id])
gon.redirect_path = users_path
gon.global_variables = {
:subdomain => @product.subdomain,
:statistic => {
:count_hints => @product.count_hints,
:count_logins => current_user.count_logins
}
}
gon.image_uploader = {
:create_path => product_path(@product),
:image_tempale => render_to_string(:partial => 'image')
}
# etc. and etc.
end
This code looks too bad and I don’t know how you fill when you see it, but my brain explodes.
Useing Gon in a DRY way
So go down to business and make something with this peace of ugly code. Let’s create LocationGonFilter
class and move it to app/filters
folder. You should create this folder if you don’t have it.
class LocationGonFilter
def self.filter(controller)
return unless controller.respond_to?(:gon)
gon = controller.gon
if resource = controller.send(:resource)
if resource.respond_to?(:locations)
gon.locations ||= []
gon.locations |= resource.locations.as_json(:only => [:latitude, :longitude], :methods => [:address])
end
end
end
end
After this we can refactor our controller:
class ProductsController < ApplicationController
before_filter LocationGonFilter, :only => [:show]
def show
end
def resource
@product ||= Product.find(params[:id])
end
end
Test for this filter will be look like this:
require 'spec_helper'
describe LocationGonFilter do
let(:controller) { double(:some_controller) }
let(:gon) { double(:gon) }
let(:resource) { double(:resource) }
let(:locations) { [create(:location), create(:location, :address2 => 'suite 2')] }
let(:json_locations) { locations.as_json(:only => [:latitude, :longitude], :methods => [:address]) }
context :without_gon do
it { expect { LocationGonFilter.filter(controller) }.to_not raise_error }
end
context :with_gon do
before { controller.stub(:gon).and_return(gon) }
before { gon.stub(:locations).and_return([]) }
before { controller.stub(:resource).and_return(resource) }
context :filter do
it { expect { LocationGonFilter.filter(controller) }.to_not change(gon, :locations) }
it do
resource.stub(:locations).and_return(locations)
gon.stub(:locations=) do |args|
gon.stub(:locations).and_return(args)
end
LocationGonFilter.filter(controller)
gon.locations.should eq(json_locations)
end
end
end
end
I’m using FactoryGirl here to create locations
PS. I believe that every class should be responsible for only one thing (this is a Single responsibility principle (SRP)). In my post I’ve explained how to achieve it for controller layer. Filters are separated layer and there we should filter parameters, controllers should inly listen requests, pass action to bottom layer (model) and then response on request, they should not contain complidated logic