Как написать Front-end на Рельсах почти без JavaScript
Всем привет! Меня зовут Дмитрий, я Full Stack инженер, и вот уже 5 лет успешно использую Ruby. В продуктовой студии Railsware, где я работаю, к нам часто обращаются за разработкой MVP для различных продуктов. Инструменты и подходы для создания пользовательского интерфейса от команды Rails прекрасно подходят для этой задачи. Сегодня я немного расскажу о них и покажу, как все работает.
Вопреки слухам в девелоперских комьюнити, Rails не становится устаревшей технологией, не собирается умирать, и остается отличным инструментом для разработки вашего нового проекта. Одна из причин заключается в том, что в Rails есть достаточно инструментов, чтобы покрыть базовый функционал типичного веб-приложения. Вам не нужно думать о том, как обрабатывать НТТР-запросы, что использовать для сохранения и получения данных из базы, как отрисовать HTML, который пользователи увидят в своих браузерах, и даже как «вдохнуть жизнь» в пользовательский интерфейс.
Инструменты из библиотеки
Rails UJS
Давным давно, когда я только пытался сверстать свою первую HTML-страничку, у Rails уже был крутой инструмент jquery-ujs (unobtrusive javascript), который теперь называется rails-ujs. Он отлично работает с рельсовым бэкендом, когда вам нужно добавить парочку AJAX запросов малой ценой.
Предлагаю попробовать сделать что-то вроде этого:
app/controllers/money_controller.rb
class MoneyController < ApplicationController def show @money = GetAllMoney.call end def destroy SpendAllMoney.call end end
views/money/show.html.erb
<div class="money"> <h3>Your money</h3> <span id="money-amount"><%= @money %></span> <span>$</span> <%= link_to 'Spend all money', money_path, method: 'delete', remote: true, data: { confirm: 'Do you want to spend all money?' }, class: 'spend-money-button' %> </div>
views/money/destroy.js
document.querySelector('#money-amount').innerHTML = 0
Итак, вы сделали запрос AJAX, используя всего несколько HTML-атрибутов и один JS-файл с одной строчкой кода. Довольно круто, правда?
Turbolinks
Еще один старожил в мире Rails — Turbolinks. Эта библиотека не находится в стадии активной разработки, но о ее преемнике мы поговорим немного позже. Если в двух словах, то Turbolinks приносит вам SPA опыт почти без клиентского кода. Если подробно, то эта библиотека:
- загружает содержимое новых страниц с помощью JS и заменяет его на странице без перезагрузки браузера;
- кэширует страницы, чтобы повторные посещения казались мгновенными;
- позволяет сохранять элементы на странице неизменными во время навигации.
Первые две фичи просто работают из коробки, а последняя должна быть явно определена в вашем коде. Сейчас я вам покажу небольшой и надуманный пример того, что мы можем достичь с ее помощью. Предположим, что где-то на странице есть счетчик уведомлений:
app/helpers/application_helper.rb
module ApplicationHelper def notifications_count sleep 3 # emulate some calculations 10 end def articles Article.last(5) end end
app/views/layouts/application.html.erb
<!DOCTYPE html> <html> <head> <title>Turbolinks</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <div class="container"> <nav class="navigation"> <ul> <%- articles.each do |article| %> <li> <%= link_to article.title, article_path(article.id) %> </li> <% end %> </ul> <div class="notifications"> <div class="notifications-badge"> <%= notifications_count %> </div> </div> </nav> <section class="content"> <%= yield %> </section> </div> </body> </html>
Подсчет количества уведомлений может занять некоторое время, но это цена, которую вы платите за поддержку актуальности данных.
Позже, возможно, вы также захотите обновлять количество уведомлений, подписавшись на них в режиме реального времени. У Rails даже есть встроенный Action Cable для этого.
Поскольку такая работа проделана на фронтенде, вам не нужно подсчитывать общее количество страниц между переходами, обработанными Turbolinks. Конечно, вся проблема может быть решена с помощью простого кэширования, но знаете... есть только две сложные вещи в CS... инвалидация кэша... и мы все равно говорим о Turbolinks :)
Таким образом, можно просто не выполнять код, если страница запрашивалась Turbolinks, и запретить Turbolinks обновлять часть страницы. Вот как это выглядит:
app/helpers/application_helper.rb
module ApplicationHelper def notifications_count + return nil if request.headers['Turbolinks-Referrer'].present? + sleep 3 # emulate some calculations 10 end def articles Article.last(5) end end
app/views/layouts/application.html.erb
<div class="notifications"> - <div class="notifications-badge" id="notifications-badge"> + <div class="notifications-badge" id="notifications-badge" data-turbolinks-permanent> <%= notifications_count %> </div> </div>
Чего же нам не хватает?
Старенькие фичи Rails делают свою работу хорошо, и многие приложения успешно строили на них сложные юзерские интерфейсы, не используя сложный JS фреймворк. Несмотря на это, нам все еще не хватает фич, которые сделают наши приложения более удобными для обслуживания и упростят разработку интерфейса.
Новые инструменты от команды Rails
В начале 2021 года DHH объявил о появлении альтернативного подхода Hotwire, нового способа Rails для построения пользовательских интерфейсов. Несмотря на то, что Hotwire является собирательным названием для семейства библиотек, эта семья довольно мала. По состоянию на октябрь 2021 года было всего две библиотеки: Turbo и Stimulus.
Они обе разработаны командой Rails и могут без проблем интегрироваться в ваш величественный монолит. Я расскажу больше о Turbo, так как эта библиотека относительно новая и заменит уже существующую Turbolinks.
Turbo
Если вы думали, что Turbolinks потеряли свою часть «links», потому что это теперь больше, чем навигация, вы на 100% правы. Библиотека Turbo разделена на несколько частей, где каждая служит единой цели — доставить в ваше приложение HTML, отрисованный на сервере, с разницей в том, когда и как это делается:
- Turbo Drive — тот старый добрый Turbolinks, с которым мы знакомы.
- Turbo Frames — «отдельные» фреймы, которые могут быть загружены асинхронно и обновляться, когда сервер возвращает фрейм с тем же id.
- Turbo Streams — другой тип фреймов, который обновляется в результате HTTP-запроса или с помощью сервера через Websocket.
- Turbo Native — обертка вашего «турбированного» веб-приложения, которая интегрирует его в мобильное приложение.
Итак, теперь обо всем по порядку:
Turbo Drive
Как я уже говорил, Turbo Drive просто заменяет Turbolinks и берет на себя навигацию между страницами. Поскольку почти ничего не изменилось, миграция довольно проста.
Вам нужно просто добавить пакет npm
yarn add @hotwired/turbo
Заменить Turbolinks на Turbo в вашем javascript коде
app/javascript/packs/application.js
import Rails from "@rails/ujs" - import Turbolinks from "turbolinks" + import * as Turbo from "@hotwired/turbo" Rails.start() - Turbolinks.start()
Зменить data-turbolinks
... атрибуты с data-turbo
...
app/views/layouts/application.html.erb
- <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> - <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbo-track': 'reload' %> + <%= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %>
Важный момент, на который нужно обратить внимание — заполнение формы. Turbo Drive берет на себя и это. Прежде всего, он ожидает, что redirect после отправки формы будет со статусом 303, чтобы позволить Fetch API автоматически следовать за редиректом. Это правильный статус НТТР для неиндемпотентных (умное слово для описания методов не GET или HEAD 🤓) запросов, если вы хотите, чтобы переадресация осуществлялась с помощью метода GET. В противном случае правильно перенаправлены будут лишь POST запросы, поскольку они также предусматривают статусы 301 и 302. Так что вам следует явно указать код статуса для редиректа. И вот как это сделать
app/controllers/any_controller.rb
- redirect_to money_path + redirect_to money_path, status: :see_other
Так или иначе в рельсовых формах все равно используется метод POST и добавляется <input type="hidden" name="_method" value="patch">
, чтобы определить, какое действие контроллера использовать. Это означает, что ваши формы все еще будут работать, а о необходимости правильного кода статуса уже велись бурные дискуссии.
Стоит обратить внимание на то, что Turbo не поддерживает параметр local: true
, который вы могли использовать для отключения JS-контроля над формой. Если это ваш случай, нужно внести еще одно небольшое изменение:
app/views/_any_form.html.erb
- <%= form_with(url: money_path, local: true) do |f| %> + <%= form_with(url: money_path, data: { turbo: false }) do |f| %>
Turbo Frames
Наконец мы подобрались к чему-то новенькому в рельсе. Turbo Frame — это простой инструмент для создания контейнера с контентом, который может загружаться и обновляться отдельно. Так же, как и в геме render_async или .ejs
от Rails, но с меньшим количеством кода.
Давайте посмотрим пример того, как мы можем разложить нашу страницу на несколько частей, которые асинхронно подгружаются только тогда, когда пользователь их видит. Представьте страницу продукта с общей информацией, свойствами продукта и отзывами клиентов. Нет гарантии, что пользователь посетит каждый из этих разделов, поэтому мы можем загрузить их только если он действительно добрался до этой части страницы.
Я не буду останавливаться на настройке моделей, маршрутов, установке Bootstrap и добавлению CSS. Полагаю, вас мало интересуют такие базовые вещи.
Вот так выглядит наш products/show.html.erb
:
app/views/products/show.html.erb
<div class="product"> <ul class="nav nav-tabs" id="product-tab" role="tablist"> <li class="nav-item" role="presentation"> <button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="true">General</button> </li> <li class="nav-item" role="presentation"> <button class="nav-link" id="properties-tab" data-bs-toggle="tab" data-bs-target="#properties" type="button" role="tab" aria-controls="properties" aria-selected="false">Properties</button> </li> <li class="nav-item" role="presentation"> <button class="nav-link" id="reviews-tab" data-bs-toggle="tab" data-bs-target="#reviews" type="button" role="tab" aria-controls="reviews" aria-selected="false">Reviews</button> </li> </ul> <div class="tab-content"> <div class="tab-pane active p-3" id="general" role="tabpanel" aria-labelledby="general-tab"> <turbo-frame id="<%= dom_id(@product, 'general') %>" loading="lazy" src="<%= general_product_path %>"> <%= render('common/spinner') %> </turbo-frame> </div> <div class="tab-pane p-3" id="properties" role="tabpanel" aria-labelledby="properties-tab"> <turbo-frame id="<%= dom_id(@product, 'properties') %>" loading="lazy" src="<%= properties_product_path %>"> <%= render('common/spinner') %> </turbo-frame> </div> <div class="tab-pane p-3" id="reviews" role="tabpanel" aria-labelledby="reviews-tab"> <turbo-frame id="<%= dom_id(@product, 'reviews') %>" loading="lazy" src="<%= reviews_product_path %>"> <%= render('common/spinner') %> </turbo-frame> </div> </div> </div>
Это обычные табы из Bootstrap. Но интересные вещи происходят в элементах .tab-page
. Мы добавили тег turbo-frame
, который является нашим контейнером для загрузки и обновления. У каждого фрейма должен быть свой собственный атрибут идентификатора id
, а хелпер dom_id
будет хорошим инструментом, чтобы освободить нас от необходимости думать над именами. Для асинхронной загрузки фрейма мы должны добавить атрибут src
, и ответ из этого пути должен вернуть фрейм с таким же идентификатором (id).
Поскольку мы хотим загрузить только видимую часть, добавляем loading="lazy"
, и фрейм будет загружаться только тогда, когда этот элемент появится на странице. Обратите внимание, что не важно, как этот элемент стал видимым. Пользователь может просто проскролить страницу к этому тегу и его содержимое загрузится. Стили родительского элемента могут измениться с display: none
на display: block
, приложение может вставить этот тег на страницу с помощью JavaScript, или вы даже можете рекурсивно рендерить один фрейм из другого (но не забудьте как-нибудь выйти из рекурсии).
Спиннер в примере — это просто div с CSS анимацией. Вам ничего не нужно с этим делать. Он просто будет вращаться, пока содержимое фрейма не загрузится и появится на странице.
app/views/common/_spinner.html.erb
<div class="text-center mt-5"> <div class="spinner-grow text-secondary" role="status"> <span class="visually-hidden">Loading...</span> </div> </div>
В качестве альтернативы можно использовать атрибут busy
, который добавляется во фрейм при загрузке, и добавить свой CSS, чтобы показать состояние загрузки.
Наш контроллер довольно простой:
app/controllers/products_controller.rb
class ProductsController < ApplicationController def show @product = Product.find(params[:id]) end def general @product = Product.find(params[:id]) render partial: 'products/general' end def properties @product = Product.find(params[:id]) render partial: 'products/properties' end def reviews @product = Product.find(params[:id]) render partial: 'products/reviews' end end
app/views/products/_general.html.erb
<turbo-frame id="<%= dom_id(@product, 'general') %>" loading="lazy" src="<%= general_product_path(product_id: @product) %>"> <div class="product--general"> <h1> <%= @product.title %> </h1> <div class="row mt-4"> <div class="col"> <div class="product--image"> <%= image_tag @product.image %> </div> </div> <div class="col"> <h3> <%= @product.price %> </h3> <%= @product.content %> </div> </div> </div> </turbo-frame>
app/views/products/_properties.html.erb
<turbo-frame id="<%= dom_id(@product, 'properties') %>"> <h1> <%= @product.title %> properties </h1> <dl class="row mt-4"> <%- @product.properties.each do |name, value| %> <dt class="col-sm-3"><%= name.to_s.titleize %></dt> <dd class="col-sm-9"><%= value %></dd> <% end %> </dl> </turbo-frame>
app/views/products/_review.html.erb
<turbo-frame id="<%= dom_id(@product, 'reviews') %>"> <%- @product.reviews.each do |review| %> <div class="card mb-3"> <div class="card-body"> <div class="card-title"> <%= review.author %> </div> <div class="card-text"> <%= review.content %> </div> </div> </div> <% end %> </turbo-frame>
Тут мы рендерим отдельные фрагменты, но это может быть и вся страница с макетом. Главное — отрендерить тег turbo-frame
с тем же идентификатором, что и у тега, куда контент будет вставлен.
В целом, это все, что вам нужно, чтобы получить «лениво загруженную страницу». К сожалению, я не смог найти удобный способ обработки ошибок при работе с turbo frames, но набросал решение, которое может вам помочь:
app/controllers/any_controller.rb
def general @product = Product.find(params[:id]) raise StandardError, 'Some error' render partial: 'products/general', locals: { in_cart: product_in_cart?(@product) } rescue StandardError render partial: 'common/turbo_error', locals: { id: dom_id(@product, 'general'), error_message: 'Oops. Something went wrong' } end
app/views/common/_turbo_error.html.erb
<turbo-frame id="<%= id %>"> <%= error_message %> </turbo-frame>
Ещё одна замечательная вещь, которую мы можем сделать с turbo frames, это заменять части страницы в ответ на заполнение формы. Идея очень похожая. Действие контроллера должно вернуть тег turbo-frame
и Turbo заменит его на странице. Давайте расширим предыдущий пример, чтобы получить возможность добавлять и удалять товары в корзине:
app/controllers/products_controller.rb
class ProductsController < ApplicationController include ActionView::RecordIdentifier def show @product = Product.find(params[:id]) end def general @product = Product.find(params[:id]) - render partial: 'products/general' + render partial: 'products/general', locals: { in_cart: product_in_cart?(@product) } end def properties @product = Product.find(params[:id]) render partial: 'products/properties' end def reviews @product = Product.find(params[:id]) render partial: 'products/reviews' end + def add_to_cart + @product = Product.find(params[:id]) + + session[:cart] = (session[:cart] || []) << @product.id + + render partial: 'products/general', locals: { in_cart: product_in_cart?(@product) } + end + + def remove_from_cart + @product = Product.find(params[:id]) + + session[:cart] = (session[:cart] || []).reject { |id| @product.id == id } + + render partial: 'products/general', locals: { in_cart: product_in_cart?(@product) } + end + + private + + def product_in_cart?(product) + return false unless product && session[:cart] + + session[:cart].include?(product.id) + end end
app/views/products/_general.html.erb
<turbo-frame id="<%= dom_id(@product, 'general') %>" loading="lazy" src="<%= general_product_path(product_id: @product) %>"> <div class="product--general"> <h1> <%= @product.title %> </h1> <div class="row mt-4"> <div class="col"> <div class="product--image"> <%= image_tag @product.image %> </div> </div> <div class="col"> <h3> <%= @product.price %> </h3> + <%- if in_cart %> + <%= form_with(url: remove_from_cart_product_path) do |f| %> + <%= f.submit 'Remove from cart', class: 'my-3 btn btn-danger' %> + <%- end %> + <%- else %> + <%= form_with(url: add_to_cart_product_path) do |f| %> + <%= f.submit 'Add to cart', class: 'my-3 btn btn-success' %> + <%- end %> + <% end %> <%= @product.content %> </div> </div> </div> </turbo-frame>
Вы видите, что теперь у контроллера есть действия для добавления и удаления товаров. Оба этих метода просто рендерят фрагмент general
и он волшебным образом обновляется на странице. Это очень похоже на то, что обычно делается в шаблонах .js.erb
. Тем не менее, turbo — более предпочтительный вариант, чтобы избежать дополнительного JS кода, который, к тому же, лежит в папке views.
Turbo Streams
Turbo дал нам еще один интересный инструмент для изменения HTML на странице — Turbo Streams. Он дает больше возможностей для обновления интерфейса DOM, и вы не ограничены заменой только одного фрейма, как это происходит с Turbo frames. Этa манипуляция с DOM называются action
и она должна выполняться на элементах targets
, полученных из какого-либо селектора. Turbo streams дает вам 7 действий к выполнению:
- append — добавить html в начало цели.
- prepend — добавить html в конец цели.
- replace — заменить всю цель на html.
- update — обновить html внутри цели.
- remove — удалить всю цель.
- before — добавить HTML после цели.
- after — добавить HTML перед целью
Вы обычно слышите о Turbo Streams, когда речь заходит об обновлениях в режиме реального времени и создании еще одного приложения для чата. Но мы можем начать с примера попроще и посмотреть, как Turbo Streams помогает отобразить заполнение формы в пользовательском интерфейсе. Давайте продолжим с предыдущим примером: добавим возможность разместить новый отзыв и показать общее количество отзывов. В начале я просто добавлю количество отзывов и форму для добавления нового отзыва:
app/views/products/show.html.erb
<ul class="nav nav-tabs" id="product-tab" role="tablist"> <li class="nav-item" role="presentation"> <button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="true">General</button> </li> <li class="nav-item" role="presentation"> <button class="nav-link" id="properties-tab" data-bs-toggle="tab" data-bs-target="#properties" type="button" role="tab" aria-controls="properties" aria-selected="false">Properties</button> </li> <li class="nav-item" role="presentation"> - <button class="nav-link" id="reviews-tab" data-bs-toggle="tab" data-bs-target="#reviews" type="button" role="tab" aria-controls="reviews" aria-selected="false">Reviews</button> + <button + class="nav-link" + id="reviews-tab" + data-bs-toggle="tab" + data-bs-target="#reviews" + type="button" + role="tab" + aria-controls="reviews" + aria-selected="false" + > + Reviews + <span id=<%= dom_id(@product, 'reviews_count') %>> + <%= render(partial: 'products/reviews/count_badge', locals: { count: @product.reviews.count }) %> + </span> + </button> </li> </ul>
app/views/products/_reviews.html.erb
<turbo-frame id="<%= dom_id(@product, 'reviews') %>"> <%= render(partial: 'products/reviews/form', locals: { product: @product }) %> <div id="<%= dom_id(@product, 'reviews_list') %>"> <%- @product.reviews.each do |review| %> <%= render(partial: 'products/reviews/card', locals: { review: review }) %> <% end %> </div> </turbo-frame>
app/views/products/reviews/_count_badge.html.erb
<span class="badge bg-primary"> <%= count %> </span>
app/views/products/reviews/_form.html.erb
<%= form_with(url: add_review_product_path(id: product.id), class: 'mb-4', id: dom_id(product, 'reviews_form')) do |f| %> <%= f.text_area :review, class: "form-control mb-1" %> <%= f.submit 'Add a review', class: 'btn btn-primary' %> <% end %>
app/views/products/reviews/_card.html.erb
<div class="card mb-3"> <div class="card-body"> <div class="card-title"> <%= review.author %> </div> <div class="card-text"> <%= review.content %> </div> </div> </div>
И вот как это будет выглядеть:
Теперь мы можем добавить немного интерактивности, используя Turbo Streams:
app/controllers/products_controller.rb
+ def add_review + @product = Product.find(params[:id]) + @review = @product.add_review(author: 'You', content: params[:review]) + end
app/views/products/add_review.turbo_stream.erb
<%= turbo_stream.update(dom_id(@product, 'reviews_count')) do %> <%= render(partial: 'products/reviews/count_badge', locals: { count: @product.reviews.count }) %> <% end %> <%= turbo_stream.replace(dom_id(@product, 'reviews_form')) do %> <%= render(partial: 'products/reviews/form', locals: { product: @product }) %> <% end %> <%= turbo_stream.prepend(dom_id(@product, 'reviews_list')) do %> <%= render(partial: 'products/reviews/card', locals: { review: @review }) %> <% end %>
Самый интересный файл вот здесь _add_review.turbo_stream.erb
. Формат turbo_stream
может быть новым для вас, если вы впервые сталкиваетесь с Turbo Streams. Turbo требует, чтобы HTTP-ответ имел контент-тип text/vnd.turbo-stream.html
, поэтому вы должны либо передать content_type
: «text/vnd.turbo-stream.html
» в метод render
в действии контроллера, либо добавить расширения .turbo_stream.erb
для вашего шаблона. Второй вариант мне кажется более практичным. Главный субъект в _add_review.turbo_stream.erb
— это хэлпер turbo_stream
. Мы используем его для вызова ранее упомянутых действий. А если точнее, он генерирует XML теги, которые описывают, какие манипуляции DOM должны быть сделаны. Этот файл делает три вещи:
- Обновляет счетчик отзывов — обновляет содержимое тега с идентификатором
dom_id(@product, 'reviews_count')
- Сбрасывает форму обзора — заменяет весь тег на id
dom_id(@product, 'reviews_count')
- Показывает новый обзор на странице — добавляет контент в начало тега с id
dom_id(@product, 'reviews_list')
Вот и все, что вам нужно для создания интерактивного веб-приложения. Без единой строки JS кода! И этого будет достаточно для большинства приложений.
С Turbo Streams вы также можете изменять содержимое страницы с помощью WebSocket. Это не потребует много действий с нашей стороны. Предлагаю вернуться к нашему примеру и обновить отзывы во всех открытых браузерах, когда будет добавлен новый отзыв.
Перед тем, как мы начнем, вы должны добавить в свой Gemfile гем «turbo-rails» и запустить эту команду Bundle exec rails turbo:install
Он установит @hotwired/turbo-rails
и заменит адаптер Action Cable с async (по умолчанию) на redis. Теперь мы готовы к работе в режиме реального времени.
Первое, что нам нужно сделать, это подписаться на обновления продукта. Это очень просто благодаря хелперу turbo_stream_from
. Вот как это выглядит:
app/views/products/show.html.erb
<div class="product"> + <%= turbo_stream_from @product %> <ul class="nav nav-tabs" id="product-tab" role="tablist">
И теперь, вместо возврата тегов turbo-frame
, которые показывают, какие действия должны быть выполнены на пользовательском интерфейсе, мы отправим эти действия всем слушателям (всем открытым страницам продукта)
app/controllers/products_controller.rb
def add_review @product = Product.find(params[:id]) @review = @product.add_review(author: 'You', content: params[:review]) + Turbo::StreamsChannel.broadcast_update_to( + @product, + target: ActionView::RecordIdentifier.dom_id(@product, 'reviews_count'), + partial: 'products/reviews/count_badge', + locals: { count: @product.reviews.count } + ) + + Turbo::StreamsChannel.broadcast_prepend_to( + @product, + target: ActionView::RecordIdentifier.dom_id(@product, 'reviews_list'), + partial: 'products/reviews/card', + locals: { review: @review } + ) end
И чтобы некоторые действия не выполнились дважды, удалим их из НТТР-ответа
app/views/products/add_review.turbo_stream.erb
- <%= turbo_stream.update(dom_id(@product, 'reviews_count')) do %> - <%= render(partial: 'products/reviews/count_badge', locals: { count: @product.reviews.count }) %> - <% end %> <%= turbo_stream.replace(dom_id(@product, 'reviews_form')) do %> <%= render(partial: 'products/reviews/form', locals: { product: @product }) %> <% end %> - <%= turbo_stream.prepend(dom_id(@product, 'reviews_list')) do %> - <%= render(partial: 'products/reviews/card', locals: { review: @review }) %> - <% end %>
Обновление формы все еще останется в HTTP-ответе, так как форма должна быть очищена после отправки, и мы не хотим очищать ее для всех пользователей.
Это все, что нужно, если вы хотите добавить в Rails-приложение немного коммуникации в режиме реального времени. Магия рельсов во всей ее красе!
Выводы
Как итог, мы видим, что команда Rails проделала большую работу, чтобы свести к минимуму взаимодействие со всем этим большим и страшным миром JS, оставив фреймворк отличным инструментом для создания современных веб-приложений. С помощью Turbo вы сможете без дополнительных усилий сделать ваше приложение максимально похожим на SPA при этом оставив бОльшую часть логики и разметки на бэкенде. Конечно, реальный мир может (и, скорее всего, будет) требовать больше, чем может дать Turbo. Но и тут команда Rails разработала Stimulus и request.js, чтобы сделать вашу жизнь легче, когда вам все-таки придется писать JS код в Rails приложении. Но это уже совсем другая история ;)
27 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів