М. 9 Различные формы Ajax в Rails - Evanto/qna GitHub Wiki
Теория
В прошлый раз мы релизовывали Ajax при помощи встроенного механизма Rails. Сегодня посмотрим, как Ajax работает внутри Rails, как это связано с JS, и как в Rails реализовать другие виды Ajax-форм. Как помним, Ajax-формы бывают разного вида в зависимости от того, что возвращает сервер:
- стандартный Ajax (возвращает асинх. JS + XML)
- асинх. JS + JS
- асинх. JS + HTML
- асинх. JS + JSON
В прошлый раз мы использовали тип Ajax AJAJ - асинх. JS + JS (т.е. наш сервер возвращал исполняемый JS – кусок JS-кода, который тут же исполняется). Сегодня сделаем 2 других типа форм:
- асинх. JS + HTML
- асинх. JS + JSON
Но сначала разберемся, как изнутри работает взаимодействие Rails и JS.
Как работает Ajax и JS внутри Rails-приложения
Чтобы в Rails работала схема Unobtrusive JavaScript (ненавязчивый JS), необходим гем jquery-rails. Кроме jQuery, он подключает дополнительные расширения - обработчики дополнительных событий в JS и jQuery, связывающие действия формы, которую рендерит Rails, с JS-кодом. Посмотрим, как в Rails формируется форма, работающая через Ajax (на примере нашей формы создания ответа к вопросу):
= form_for [@question, @answer], remote: true, data: { type: :json } do |f|
p
= f.label :body, 'Your answer'
.answer-errors
= f.text_area :body
p
= f.fields_for :attachments do |a|
= a.label :file
= a.file_field :file
p= f.submit 'Create'
Мы дали хелперу формы form_for
опцию remote: true
, после чего запросы пошли через Ajax. Почему?
Посмотрим в веб-инспекторе, из чего состоит эта форма (начало кода):
<form accept-charset="UTF-8" action="/questions/1/answers" class="new_answer" data-remote="true" ...
Видим в начале тег формы form
, в середине - атрибут data-remote="true"
. Как только мы даем хелперу формы опцию remote: true
, в html-коде формы возникает тег data-remote="true"
. Можно подумать, что вроде нет правила, что при добавлении такого атрибута форма автоматически должна начать отправлять данные через JS (и это не работает так в других фреймворках или с отключенным rails jquery). А дело в том, что через remote: true
подключается специальный обработчик, находящийся в другом геме - jquery-ujs. Гем jquery-ujs хранится как зависимость к гему rails jquery, в репозитории rails, он прост и хорошо документирован.
remote: true
работать через Ajax и в JS
Как гем jquery-ujs заставляет формы с В jquery-ujs и лежит весь код по взаимодействию rails-приложения с JS/Ajax. Разберем подробнее, что там происходит, глядя в исходник гема – rails.js.
В rails.js и находится весь js-код по взаимодействию (биндингу) html-форм (и других элементов, генерируемых рельсами) с JS и вызовом/обработкой Ajax-запросов. Полистаем этот файл. Со строки 25 начинается перечисление используемых data-селекторов:
$.rails = rails = {
// Link elements bound by jquery-ujs
linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]',
Нам интересны те из них, что содержат слова [data-remote]
, как у нас в форме: например select[data-remote]
, button[data-remote]
.
Почему появление data-remote="true"
заставляет форму работать через Ajax? Потому, что в файле rails.js гема jquery-ujs есть селектор, который берет форму, и, если у нее есть атрибут data-remote, начинает отправлять ее асинхронно, через Ajax. Посмотрим, как это работает. В строке 35 есть formSubmitSelector
с атрибутом form
(пока без remote
):
// Form elements bound by jquery-ujs
formSubmitSelector: 'form',
remote
выбирается дальше, в коде, который через условие вешает на формы обработчик, делающий запросы через Ajax. Ниже есть переменная remote
, которая определяется, если атрибут remote
определен у формы (форме прописано remote: true
). Наличие у формы опции remote: true
проверяется в условии, начинающемся со строки 498 if (remote)
. И ниже, в том же условии, в строке 512 rails.handleRemote(form);
, на формы с remote: true
крепится handleRemote
(обработчик события).
Если форма имеет опцию remote: true
, гем jquery-ujs автоматически крепит к ней обработчик события handleRemote.
Обработчик HandleRemote
готовит данные (все опции) будущего ajax-запроса для отправки, затем выполняет ajax-запрос с ними. Он начинается в строке 93. Этот довольно большой и универсальный метод, т.к. надо учесть много вариантов (он работает для кнопок, ссылок, форм, и многого другого), и, в зависимости от того, POST это запрос или GET, по-разному формирует запрос из параметров.
Так же в его обязанности входит файлинг (вызов) других событий: когда мы выполняем в Rails ajax-запрос (любого вида), возникает несколько событий, на которые мы можем повесить обработчики для выполнения какого-то кода. Например, есть ajax before
, ajax before send
(перед отправкой формы или перед выполнением ajax-запроса), ajax success
(после успешного выполнения ajax-запроса), ajax error
(после неуспешного выполнения ajax-запроса) и т.д. - на каждое из этих событий мы можем повесить свой обработчик (как - посмотрим чуть позже).
Мы разобрались, почему добавление опции remote
заставляет форму отправляться через Ajax, а не напрямую: потому что у нас есть специальный код, отслеживающий формы с наличием этой опции и вешающий на них обработчик события, который меняет стандартное выполнение запроса на ajax (отправку формы с html на ajax).
Но как Ajax-запрос работает с тем, что у нас в контроллере (format html, format js)? Посмотрим в веб-инспекторе (вкладка Network, фильтр XHR, который показывает только ajax-запросы ), как работает отправка нашей формы. Заполнив форму ответа и нажав кнопку, увидим:
- POST
- 200
- Type - text/javascript
Как Rails понимают, что надо отработать именно js.erb? Как раз по типу запроса (Type), а тип запроса передается в заголовках запроса - Request Headers:
Request Headers
Accept: */*:q=0.5, text/javascript, application/javascript, application/ecmascript, application/x-ecmascript
Именно в заголовке Request Headers передаются типы ответов, которые может принять клиент. Здесь перечислены т.н. маймтайпы (MIME Types): text/javascript
, application/javascript
, application/ecmascript
, application/x-ecmascript
. Почему их несколько? Потому что сервер может ответить разными маймтайпами (типами ответа). Эти тайпы означают одно и то же - например, что будет отдан JS - но само значение может быть отдано сервером по-разному: как text/javascript
, application/javascript
, application/ecmascript
, application/x-ecmascript
, etc.
MIME stands for "Multipurpose Internet Mail Extensions. It's a way of identifying files on the Internet according to their nature and format. For example, using the "Content-type" header value defined in a HTTP response, the browser can open the file with the proper extension/plugin. MIME Types List
The Complete List of MIME Types
В запросе перечисляются все возможные маймтайпы для JS чтобы не зависеть от того, с каким маймтайпом сервер его отдаст (быть в состоянии принять любой). Кстати, строки в accept у нас появляются тоже благодаря опции remote: true
, можем даже посмотреть, где это происходит в исходнике rails-ujs: строки 147-149:
if (settings.dataType === undefined) {
xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
}
Если в настройках dataType
не определен, то установка заголовков запроса содержится в settings.accepts.script
, там же лежат и маймтайпы (text/javascript, application/javascript, application/ecmascript, application/x-ecmascript).
Вкладка Headers
*/*
означает любой ответ.
*/*
означает, что при указанном remote: true
наш клиент не только просто запрашивает JS, а говорит: “я предпочитаю JS, но приму любой ответ.” */*
это как раз и есть любой ответ. Другой вопрос, что мы можем не уметь его обрабатывать. Но, тем не менее, не будет ошибки, если сервер вернет какой-то другой (не JS) ответ. */*
как раз и нужен, чтобы избежать ошибок при отдаче сервером других типов ответов - чтобы если сервер вернул другой ответ , мы могли это увидеть, отследить, среагировать.
Рядом со звездочками есть интересная вещь: q=0.5
:
Accept: */*;q=0.5
- принимать все ответы, вес не-JS ответов 0.5, вес JS-ответов 1, т.к. им предпочтение. Это так называемый вес, или приоритет. У указанных 4х типов вес по умолчанию 1 (т.к. JS - предпочтительный тип), а у любого другого типа ответа вес 0.5 (менее предпочтителен, но тоже принимается).
Таким образом получается, что когда мы в контроллере - например, в create что-то прописываем, то наш create.js.erb файл будет отвечать на это потому, что у нас:
- есть запрос с опцией
remote: true
- есть эта вьюха -
create.js.erb
в соответствующей папке
Если мы сейчас попробуем запросить html, то будет ошибка, что у нас нет такой вьюхи. Но если мы во вьюхе create.js.erb добавим respond_to
для html, то немного изменится поведедение: в веб-инспекторе тип запроса (Type) будет text/javascript.
Кстати, мы можем управлять запрашиваемыми типами, указывая после remote: true какой тип данных мы запрашиваем:
= form_for [@question, @answer], remote: true, data { type: :html } do |f|
В этом случае у нас изменится запрос. В панели Network характеристики ответа останутся те же.
Но если посмотрим в Headers на Accept, то тут он поменяется на text/html:
Accept: text/html, */*; q=0.01
- предпочитаем text/html, но возьмем и любой другой ответ, только с гораздо меньшим приоритетом (весом): 0.01. То есть если в пришедших данных будет что-то похожее на html, то в первую очередь отработает именно этот ответ.
Тут мы запрашиваем html, но возвращается нам все равно js, т.к. наш контроллер умеет отвечать только с помощью create.js.erb
, и отрабатывает именно этот файл.. Если бы мы могли принимать только html (в Accept был бы только html), у нас была бы ошибка, что нет такого ответа. Но у нас в заголовке Accept прописана возможность принимать любой ответ (/), поэтому сервер отрабатывает тем способом, каким получается (js.erb).
Другой вариант – не прописывать ничего дополнительного у remote: true
, и тогда предпочтительным ответом будет JS. Но прописать при этом в create контроллера ответов умение отвечать только на html (например, пустым ответом):
def create
@question = Question.find(params[:question_id])
@answer = @question.answers.create(answer_params)
respond_to do |format|
format.html { render nothing: true }
end
end
В этом случае мы получим следующее.
Тип ответа в запросе поменялся на text/html (несмотря на то, что мы запросили js):
Мы сделали запрос, ответ пришел, но типа html, хотя мы запросили text/javascript (POST, 200, text/html). Hесмотря на то, что в Accept мы запросили text/js и другие варианты js-ответа:
Accept: */*;q=0.5, text/javascript, application/javascript, application/ecmascript, application/-ecmascript
Записи в контроллере относительно формата приоритетнее, чем то, какие есть вьюхи
Это потому, что когда мы пишем в контроллере что-то насчёт формата, это имеет приоритет над тем, какие вьюхи у нас есть. Если прописываем контроллеру какой-то рендер, то он приоритетен и все пойдет так, как прописано в контроллере. А если в контроллере ничего не пропишем, будет отвечать та вьюха, которая есть для данного экшена (create.js.erb). Пропишем в контроллере, на какие данные должен отвечать сервер:
def create
@question = Question.find(params[:question_id])
@answer = @question.answers.create(answer_params)
respond_to do |format|
format.html { render nothing: true }
format.js
end
end
Что у нас должно рендериться при таком контроллере?
- Для html (если запрошен html) - ничего
- Для js - т.к. мы не указали что рендерить, будет рендериться create.js
- Для json - пока ничего, его добавим позже
В консоли увидим POST, 200, text/javascript,Accept */*;q=0.5
- т.к. мы в контроллере запросили js (format.js
), отработал js.
Таким образом мы и управляем типом данных, которые нам нужны.
Весь наш контроллер:
class AnswersController < ApplicationController
def create
@question = Question.find(params[:question_id])
@answer = @question.answers.build(answer_params)
respond_to do |format|
if @answer.save
format.html { render partial: 'questions/answers', layout: false }
format.json { render json: @answer }
else
format.html { render text: @answer.errors.full_messages.join("\n"), status: :unprocessable_entity }
format.json { render json: @answer.errors.full_messages, status: :unprocessable_entity }
end
end
end
def update
@answer = Answer.find(params[:id])
@answer.update(answer_params)
@question = @answer.question
end
private
def answer_params
params.require(:answer).permit(:body, attachments_attributes: [:file])
end
end
Реализуем тип запроса “асинхронный js и html”
Теперь реализуем тип запроса “асинхронный js и html”: пусть сервер возвращает html. Есть 2 варианта:
- указать явно html у
remote: true
в questions/show.html.slim:= form_for [@question, @answer], remote: true, data: { type: :html } do |f|
- убрать из контроллера строку про js, оставив только html, и тогда сервер будет отвечать только на html (убираем format.js):
#answers_controller.rb:
respond_to do |format|
format.html { render nothing: true }
end
Можно сделать и то, и другое. Тогда форма будет запрашивать html и получать ответ от сервера в html. При этом нам надо повторить функционал файла create.js.erb - например, создав вьюху create.html.slim. Что делал файл create.js.erb? Рендерил в answers паршл questions/answers. Мы не можем выполнить такой же код в контроллере (отдать в нем тот же js через html - в этом случае js будет экранирован и не будет выполняться - отдастся как строки, а не как исполняемый js). Поэтому отрендерим тут не ничего, а паршл:
#answers_controller.rb:
respond_to do |format|
format.html { render partail: 'question/answers', layout: false }
end
Важный момент: надо дописать layout: false
, чтобы вернулся html только из этого паршла, а все футеры, хедеры, меню и другие паршлы не рендерелись.
Обработчики события, естественно, вешаются не глобальные, а для какого-то конкретного ajax-запроса от какого-то конкретного элемента. В нашем случае нам надо повесить обработчик события на форму. Посмотрим инспектором, как нызвается эта форма: class="new_answer" data-remote="true"
. Чтобы повесить обработчик не на любой элемент с классом new_answer
, а именно на эту форму, укажем, что это элемент form
с классом new_answer
: $(‘form.new_answer’).
Теперь нам надо повесить на нее обработчики событий. В первую очередь, успешного выполнения запроса. Таким событием является ajax: success
. Для прикрепления в элементу события можно использовать функцию bind
, которая принимает 2 параметра: название события, на которое вешаем обработчик и функция, которая будет выполняться при выполнении этого события.
e
- объект события
Data
в котором могут храниться какие-то данные (но у нас они будут храниться не в нем, а в xhr)
Status
Xhr
- ответ от сервера, в котором будут храниться данные
Проверим в браузере, что получилось: запостим ответ через форму: POST, 200, text/html. Запрос был успешным, вернулся html - в ответе пришли данные (html). Мы этот код можем посмотреть в соседней вкладке Preview, там будет список ответов.
В этом списке добавился наш новый ответ, но в браузере он не появился в списке ответов при нажатии кнопки create. Если обновить страницу, новый ответ появляется, а автоматически при создании – нет. Это потому, что нет обработки этих данных. Данные-то пришли, но нет кода, который бы с ними что-то делал (добавлял, изменял и т.д.) Поэтому клиент ничего с ними и не делает. Когда был create.js.erb, в нем хранился исполняемый код, который обрабатывал данные (рендерил паршл в .answers). Сейчас этот файл не отрабатывает, а другого исполняемого кода у нас нет. Теперь данные только приходят, и все, и в html данные нет смысла слать, т.к. они не выполняются как js. Что делать? Надо добавить в js обработчики события на исполнения этих ajax-запросов:
#answers.js.coffee
$('form.new_answer').bind 'ajax:success', (e, data, status, xhr) →
Дальше надо сделать то же, что у нас было в create.js.erb, т.е. вызвать функцию .html
для класса .answers
. Ответ у нас уже приходит, осталось поместить его в .answers
. Чтобы получить тело этого приходящего нам ответа из xhr
, в котором приходят данные, используем ResponseText
. Получается:
#answers.js.coffee
$('form.new_answer').bind 'ajax:success', (e, data, status, xhr) →
$('.answers').html(xhr.responseText)
Проверим в браузере, создав новый ответ. Теперь автоматическая отправка ответа в список работает.
Разберемся с выводом ошибок. Сейчас они не выводятся. Если отправить пустой ответ, будет статус 200, а в превью - список ответов.
Но нам надо сообщить юзеру, что ответ не может быть пустым. Для этого доработаем answers_controller, в котором мы еще не делали проверки, позволяющей по-разному отвечать в случае успеха и неуспеха при создании ответа. Во-первых, сделаем build, чтобы ответ не сохранялся сразу и мы могли делать save отдельно и написать условие, чтобы по-разному реагировать на то, что он вернет и выдать разные данные.
def create
@question = Question.find(params[:question_id])
@answer = @question.answers.build(answer_params)
И в блоке respond_to добавим условие с save.
В случае возврата html мы должны вывести ошибки. В файле .js мы перебирали ошибки и выводили их, тут мы не можем так сделать т.к. из контроллера не можем вернуть никакого исполняемого js. Поэтому тут поступим по-другому: вернем текст, представляющий из себя список ошибок, разделенных символом новой строки. Вот наш массив ошибок, как нам его разделить новой строкой?
format.html { render text: @answer.errors.full.messages }
Для этого используем метод .join
, который превращает элементы массива в строку и разделяет их элементом, который мы укажем (укажем "\n"
– символ новой строки - обязательно в двойных кавычках, потому что такие эскейп-последовательности работают только в них):
format.html { render text: @answer.errors.full.messages.join("\n") }
Так у нас будет возвращаться список всех сообщений об ошибке, разделенный символами новой строки.
Важный момент: если мы теперь нажмем create с пустым ответом, сообщение об ошибке появится, но при этом пропадут все ответы. Почему? Несмотря на то, что возвращается сообщение об ошибке (его видно в превью - ‘body can’t be blank’), статус у нас 200 (успех). Надо сделать так, чтобы при ошибках отрабатывал не ajax:success
, а другой обработчик, т.е. написать условие в контроллере.
Ответы исчезли, т.к. у нас есть только обработчик на успех (ajax:success
), а на случай ошибки нет ни обработчика, ни условия. Сейчас наш код только заменяет .answers html-ем из паршла ответов в случае success. Добавим обработчик на случай ошибки. Это можно сделать, добавив код статуса с ошибкой:
#answer_controller.rb
status: :unprocessable_entity
Это код 422 – если у вас ошибки валидации, он подходит лучше всего:
Теперь у нас при создании нового ответа будет этот статус 422 (POST, 422, text/html), а во вкладке Response – ошибка 'Body can't be blank' (но на странице эта ошибка не появляется, т.к. обработчик есть только на успех, а тут статус 422).
Добавим обработчик 2 - на ошибки: тут же, в answers.js.coffee
, добавим второй bind
(на том же уровне, что $ 1-го бинда – так в coffee вешается 2-й обработчик события на тот же элемент (можем так несколько вешать). Аргументы второго бинда – событие ajax:error
и те же параметры, только в другой последовательности: xhr
– ответ, status
– код ошибки, error
– текст ошибки (unprocessable entity). И так же, как мы делали в create.js
, в .answer-erros
добавляем ошибки.
В create.js.erb у нас было:
$('.answer-errors').html('<%= j message %>')
А в coffee будет:
$('.answer-errors').html(xhr.responseText)
Итого (coffee):
$('form.new_answer').bind 'ajax:success', (e, data, status, xhr) →
$('.answers').html(xhr, responseText)
.bind 'ajax:error', (e, xhr, status, error)
$('.answer-errors').html(xhr.responseText)
Проверяем в браузере: теперь ошибки выводятся, статус 422, и все работает, как надо – и при валидном, и при невалидном ответе. Правда, нет очистки поля. Но такое решение, как мы сейчас сделали – запрос и обработка html-я встречается довольно редко. Иногда оно бывает удобно (получить кусок html и обработать его в js), но в данном случае у нас от такого решения больше минусов, чем плюсов. В случае с create.js.erb в нем лежал весь исполняемый код, это было удобно.
Возврат JSON
Гораздо чаще используется вариант, когда возвращается JSON (AJAJSON).
Сделав вариант с html, мы провели подготовку по обработке json-а. Схема взаимодействия та же, и json обрабатывается в обработчиках, похожих на тот, что мы только что сделали в coffee. В show.html.slim запросим данные вместо html через JSON:
= form_for [@question, @answer], remote: true, data: { type: :json } do |f|
Теперь нам надо указать, что мы вообще json понимаем. Это можно сделать по-разному (например, добавить в контроллере format.json
). Но тут есть отличие: указывая это, мы должны рендерить не паршлы, не html, не js, а данные. И в этом случае мы должны вернуть не список всех ответов, а только вновь созданный объект (это логично: мы в экшене create мы создаем один-единственный объект @answer
, и мы должны его вернуть). Это аналогично тому, как если бы у нас не было ajax – в этом случае мы бы сделали редирект на только что созданный объект. Но тут у нас редиректы работать не будут, и мы вместо редиректа должны сделать возврат данных этого объекта в json-формате, чтобы потом их дальше обработать. Делаем это так: чтобы вернуть json, пишем render json: @answer
(передаем объект нового ответа - @answer):
format.json { render json: @answer }
Rails автоматом вызовут метод to_json
, который преобразует объект @answer
в строку в формате json. Для второго условия, где возврат ошибок, тоже добавляем рендер json-а – т.к. это json, мы можем возвращать не объединенные строки, а массив ошибок full_messages
:
format.json { render json: @answer.errors.full_messages, status: :unprocessable_entity }
Проверяем ошибки в браузере: POST, 422, application/json, но ошибки вывелись как массив, над полем формы появилось: ["Body can't be blank"].
А если создаем новый ответ, то html заменяется json-ом нашего объекта (т.е. это наш объект в формате json). Работает не совсем правильно. Это потому, что в coffee мы берем чистый ответ от сервера и никак его не обрабатываем (сервер возвращает json, мы его и выводим просто как текст): $('.answer-errors').html(xhr.responseText)
.
Чтобы правильно обработать этот json, надо его сначала распарсить, и потом как-то применить. Создадим объект answer из того объекта, который возвращается сервером в случае успеха. Делаем это через функцию parse.JSON
, которая предоставляется jQuery: в ней берем тело ответа (мы знаем, что это json, и его распарсиваем, и после этого у нас в answer есть js-объект, к которому мы можем обращаться). Ближайший аналог такого объекта - хэш руби.
$('form.new_answer').bind 'ajax:success', (e, data, status, xhr) ->
answer = $.parseJSON(xhr.responseText)
Но т.к. в отличие от вариантов, когда мы возвращали js и html, тут возвращается не список вопросов, а только 1 ответ, и мы должны не просто его заменить а добавить к списку ответов с помощью append
, и добавляем мы не просто answer
, а answer.body
:
$('form.new_answer').bind 'ajax:success', (e, data, status, xhr) ->
answer = $.parseJSON(xhr.responseText)
$('.answers').append('<p>' + answer.body + '</p>')
Мы можем написать answer.body т.к. в json-е распарсенного объекта у нас есть в том числе body. Проверяем в браузере - все работает, ответ добавляется. Теперь разберемся с выводом сообщений об ошибках, которые у нас показывались как массив. С массивом ошибок надо поступить точно так же: сначала распарсить, а потом итеративно вывести. Распарсиваем ответ:
errors = $parseJSON(xhr.responseText)
Перебираем все элементы массива с помощью итератора jQuery .each
:
Итерируем массив errors
, параметры – index
, value
(index
нам не нужен, но он должен присутствовать), и апендаем value
:
errors = $.parseJSON(xhr.responseText)
$.each errors, (index, value) ->
$('.answer-errors').append(value)
Итого:
$ ->
$('.edit-answer-link').click (e) ->
e.preventDefault();
$(this).hide();
answer_id = $(this).data('answerId')
$('form#edit-answer-' + answer_id).show()
$('form.new_answer').bind 'ajax:success', (e, data, status, xhr) ->
answer = $.parseJSON(xhr.responseText)
$('.answers').append('<p>' + answer.body + '</p>')
.bind 'ajax:error', (e, xhr, status, error) ->
errors = $.parseJSON(xhr.responseText)
$.each errors, (index, value) ->
$('.answer-errors').append(value)
Все, теперь ошибки выводятся как надо.
Послесловие:
При работе через js и создание файлов js.erb нам не приходится писать обработчики событий, как мы это делали сейчас для работы с json. И тут мы просто добавляем тело ответа к списку ответов: $('.answers').append('<p>' + answer.body + '</p>')
Но у нас были еще и формы для создания и редактирования ответов, и чтобы повторить это поведение, нам понадобится добавить не просто тег , но и другие теги форм. Либо сделать так, чтобы не выводились сразу все формы, а выводились динамически при нажатии на кнопку edit, добавляясь к нужному элементу.
Т.е при работе через json могут возникать какие-то сложности и решения. Получается, что рельсовый способ из коробки – самый простой, легкий, и он соблюдает структуру распределения вьюх, контроллеров и т.д. так как она принята в Rails , поэтому он довольно популярный.
C точки зрения взаимодействия сервера и клиента, с точки зрения ajax-запросов конечно лучше возвращать json. Но при работе с json надо быть готовым к тому, что придется выносить обработчики событий в js-файлы и, возможно, придется больше писать и это будет менее удобно. В любом случае, надо уметь работать через json (html не так часто возвращают через ajax, поэтому он менее актуален).
И такой момент - когда работаете через json, и js-кода становится довольно много, то js-код по обработке (код обработчиков), если он не разбит по файлам, быстро становится неудобоваримым – все в каше в нескольких файлах, легко запутаться. Когда код обработки ajax-ответов начал становится сложным, грязным и запутанным, это стало одной из причин появления современных js-фреймворков – желание структурировать сложный js-код по обработке ajax. Они полезны, когда на сайте много ajax-запросов. Но не на всех сайтах много ajax и не везде они нужны. Так что в большинстве случаев вам хватит работы с возвратом js или json. Но если у вас например отзывчивый интерфейс, который достигается за счет множественных ajax-запросов, вам помогут js-MVC-фреймворки (react, angular, etc.)
А суть того, как работают с этим Rails, вам уже понятна: они запрашивают json, вы им отвечаете. Плюс позже мы пройдем написание REST API, и когда на клиенте у нас ипользуются MVC-js-фреймворки, Rails используются просто как сервер API, то есть там нет стандартных видов, идет возврат json, а все что касается обработки этого json ложится на фреймворк.
Полезные ссылки
5.3.2. Accept
JSON and AJAX Tutorial: With Real Examples - awesome tutorial