Passing data to javascript using gon and jbuilder in Rails

Sometimes it is required to pass some data from server-side code to client-side javascript code.

There is a gon gem for that and it has pretty good manual. Nevertheless, I had some issues using this gem, so I decided to share my experience.

Preparation

Let's create sample rails application:

rails new sample

Add gon, select2-rails, acts-as-taggable-on gems to Gemfile. We will use that in our example. It won't hurt to remove turbolinks gem to avoid some side effects. And we should just run bundle install --without production

Let's remove //= require turbolinks from app/assets/javascripts/application.js

We should create controllers, models and views for posts, so we could work with them:

bin/rails g scaffold posts title:string content:text

And, of course, database migrations:

bin/rails generate acts_as_taggable_on:migration
bin/rake db:migrate

We will use acts_as_taggable for Post model:

class Post < ActiveRecord::Base
  acts_as_taggable
end

Our sandbox is ready now.

Editing tags with select2 and page-specific assets

Let's add tags field to an edit form:

# app/views/posts/_form.html.erb
# ...
  <div class="field">
    <%= f.label :tag_list %><br>
    <%= f.text_field :tag_list %>
  </div>
# ...

We should add tags to the list of permitted attributes in post_params method of our PostsController

# app/controllers/posts_controller.rb
# ...
    def post_params
      params.require(:post).permit(:title, :content, :tag_list)
    end
# ...

Tags editing already works now. Of course, it would be pretty handy to have a dropdown with a list of existing tags. So, let's make it look well by using select2 and we will use gon in conjunction with jbuilder (comes with rails 4 by default) to get the list of existing tags.

Enable select2:

# app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require select2
//= require_tree ./application
# app/assets/stylesheets/application.css
/*
 *= require_self
 *= require select2
 *= require_tree ./application
 */

Note, that I created a separate subfolder application for application assets. Thanks to it, we will be able to separate asset groups, e.g. place admin assets to admin folder.

We have an interesting thing here. Tags list is required just for one view file and select2 should be initialized only in app/views/posts/_form.html.erb. So, how shall we do it? Where should we put client-side javascript? Pretty a lot of people are interested in such questions and there are several possible solutions:

  • inline js directly in view file,
  • execute js only if container has specific id or class name,
  • execute js, only if there is some particular element on the page.

There are pros and cons for those approaches:

  • inline js looks ugly;
  • js compiled into a signle file will load faster because of caching;
  • js in a single file will execute some of checks on every page.

I think, I can propose prettier approach and here it is.
First of all, let's create app/assets/javascripts/specific folder and place page-specific assets there. In our case it'll look like that (note, how do we get the list of tags with gon):

# app/assets/javascripts/specific/posts/_form.js.coffee
$ ->
  $('#post_tag_list').select2
    tags: if gon? then gon.tags else []
    tokenSeparators: [","]
    width: '200'

And it's pretty easy to include this script in our view file:

# app/views/posts/_form.html.erb
# put it in the end of file
<%= javascript_include_tag 'specific/posts/_form' %>

We should configure assets precompilation to make it work in production:

# config/environments/production.rb
# ...
  config.assets.precompile += %w(specific/*)

It seems awkward to me that this is not included into assets pipeline by default.
I see some advantages of that approach:

  • files are in a separate folder and well structured;
  • code is executed only on the page where it is required;
  • thus, there won't be tons of checks for some particular id/class/element presence on other pages.

And there is only one small disadvantage: the page will perform several js requests when you first open it. I think, we can easily accept it when page-specific javascript relates to a single page and won't be executed on other dozens or hundreds of pages. Especially, if it's an admin area which is used by only one user.

Passing list of tags with gon and jbuilder

So, everything should work now. But wait! Where does the list of tags come from? The answer is: gon and jbuilder.

Let's add the following lines to the controller:

# app/controllers/posts_controller.rb
  before_action :init_gon, only: [:new, :edit, :create, :update]
# ...
  # Initialize gon to pass data to javascript

  def init_gon
    @tags = ActsAsTaggableOn::Tag.all.collect { |tag| tag.name }
    gon.jbuilder template: 'app/views/posts/_form.json'
  end

Note, that we pass the list of tags not only for new and edit actions, but for create and update actions also. In case of validation error these pages do not redirect to, but just render new and edit views. That means, that we need tags list there.

We should create jbuilder template which renders json required for us

# app/views/posts/_form.json.jbuilder
json.tags @tags

And include gon in the main layout before all other js includes:

# app/views/layouts/application.html.erb
    <%= include_gon %>

It's ready!

Additional resources