Передача данных в javascript при помощи gon и jbuilder в Rails

Порой возникают задачи в которых нужно передать данные из сервеного кода в javascript.

Я уже писал о варианте решения данной задачи на PHP для фреймворка Yii, а сейчас пришла очередь Ruby on Rails. Для этих целей уже существует вполне удобный гем gon и у него даже есть неплохая документация. Тем не менее при использовании данного гема возникают некоторые вопросы, на которые нет подробных ответов и инструкций, поэтому я решил поделиться своим опытом на примере простого приложения.

Подготовка

Итак. создаем рельсовое приложение:

rails new sample

Добавляем в Gemfile гемы gon, select2-rails, acts-as-taggable-on, на примере которых мы и будем отрабатывать передачу параметров из серверного ruby-кода в клиентский javascript. Не помешает также удалить известный гем turbolinks для устранения побочных эффектов. Для установки гемов в проект запускаем bundle install --without production

Удалим //= require turbolinks из app/assets/javascripts/application.js

Теперь создадим контроллеры, модели и вьюхи для постов, чтобы нам было с чем работать:

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

Ну и кончечно накатим миграции базы данных

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

Подключаем acts_as_taggable к модели Post:

class Post < ActiveRecord::Base
  acts_as_taggable
end

И наша песочница для экспериментов готова.

Редактирование тегов через select2 и page-specific assets

Добавляем поле для тегов в форму редактирования поста:

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

В контроллере добавляем теги в список разрешенных атрибутов в методе post_params

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

Уже сейчас у нас работает редактирование списка тегов. И конечно же было бы удобно, если бы подсказкой отображался список существующих тегов. Сделаем это красиво, используя select2, а для получения списка существующих тегов нам как раз поможет гем gon в связке с гемом jbuilder (который идет с rails 4 по умолчанию).

Итак, подключаем 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
 */

Заметьте, что под ассеты приложения я создал отдельную подпапку application. Благодаря этому можно будет разделить группы ассетов, например, отделить ассеты админки в папке admin.

Теперь у нас возникает интересный момент. По идее список тегов нужен для 1 конкретной вьюхи и select2 нужно инициализировать этим списком только для файла app/views/posts/_form.html.erb. И как же нам поступить? Куда закинуть код клиентского скрипта? Вспомним, кстати, что все ассеты сжимаются в 1 файл. Подобные вопросы интересуют многих людей и существуют разные варианты решения задачи:

  • инлайновый js прямо во вьюхе,
  • выполнение специфичного кода по наличию id или класса у контейнера (например, тега body),
  • выполнение кода, который ищет элемент на странице и только тогда запускается.

Существуют «за» и «против» этих подходов:

  • инлайновый js не красив, засоряет код;
  • javascript скомпилированный в 1 файл грузится быстрее;
  • код в общем js-файле запускает проверки на каждой странице.

Я хочу предложить, как мне кажется, наиболее изящный подход и вот в чем он заключается.
Во-первых создаем папку app/assets/javascripts/specific и будем дублировать в ней структуру вьюх для подключения странично-специфичных скриптов (page-specific javascript). В нашем случае это будет выглядеть так (кстати, заметьте как читается список тегов через gon):

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

При этом в самой вьюхе мы легко и понятно подключаем данный скрипт:

# app/views/posts/_form.html.erb
# в конце файла
<%= javascript_include_tag 'specific/posts/_form' %>

А чтобы это у нас действительно заработало на production'е - добавляем все файлы из папки specific в путь компиляции:

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

Странно, что такого механизма дополнительно не предусмотрено в assets pipeline изначально, на ряду с существующими возможностями.
Как мне кажется такой подход имеет ряд преимуществ:

  • файлы лежат в отдельной папке и четко структурированы;
  • код подключается и выполняется только на той странице, где нужно;
  • соответственно при подгрузке на других страницах не будет кучи проверок наличия того или иного элемента или класса.

Впрочем присутствует и один недостаток: при первом заходе на страницу будет делаться несколько запросов к js-файлам. Однако это легко оправдывается, когда специфичный js-код выполняется лишь на одной странице из десятков или сотен, особенно, если она находится в админке и используется лишь одним пользователем.

Передача списка тегов при помощи gon и jbuilder

Что ж, теперь все должно работать. Но погодите, откуда же взяться списку тегов?
Это очень просто реализуется связкой gon + jbuilder.

В контроллере добавляем 

# 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

Стоит отметить, что список тегов передается не только в страницы new и edit, а также в create и update по той причине, что в случае ошибки валидации они не перенаправляют вас, а просто отображают вьюшки для экшенов new и edit, а значит в них список тегов тоже нужен.

Ну и создаем шаблон jbuilder'а который собственно выводит нужный нам json списка тегов

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

А в главный лейаут перед всеми скриптами подключаем gon:

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

Готово!

Дополнительная литература