Порой возникают задачи в которых нужно передать данные из сервеного кода в 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 %>
Готово!
Дополнительная литература
- Рейлскаст Райана Бейтса «Passing data to javascript»
- Wiki гема gon