Find an article

Start typing to search the archive.

← All articles

Pass variables to JavaScript with gon

This post describes how to enjoy using the gon gem. Gon lets you pass Ruby variables from the Rails server to JavaScript, but it can turn your controllers into huge monsters. I offer an interesting approach to avoid this.

If you haven’t noticed yet that there is a good solution for passing Rails variables from controllers to JavaScript, I will be happy to introduce you to gon. In this post, I will show how to use it with controller filters in a clean, DRY way. This approach will help you avoid turning controllers into big monsters.

Using filters in controllers

If you don’t know what controller filters are or how to use them, check out the official documentation. In our case, we have to know that filters can be applied to one action. Here is one way to use them:

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 a location model as a JSON object from the server side to the client side (from a 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. Controllers aren’t easy to test either. So my solution is to use before_filter here. Here is an example of a fat controller 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 feel when you see it, but my brain explodes.

Using Gon in a DRY way

So let’s get down to business and do something with this piece of ugly code. Let’s create a LocationGonFilter class and move it to the 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

A test for this filter will 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 the Single Responsibility Principle (SRP)). In my post, I’ve explained how to achieve this for the controller layer. Filters are a separate layer, where we should filter parameters. Controllers should only listen for requests, pass the action to the lower layer (the model) and then respond to the request. They should not contain complicated logic.