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
- Railscast of Rayan Bates «Passing data to javascript»
- Wiki of gon gem