Как отредактировать один встроенный атрибут с помощью Turbo Frame и Trubo Stream с обратной связью по проверке?
Создание редактирования на месте одного атрибута модели с использованием Turbo Frames (без использования драгоценного камня, такого как Best_In_Place, поскольку он требует jQuery и плохо работает с Rails 7). В этой реализации используются ТОЛЬКО турбофреймы.
Для этого я следовал этому руководству: https://nts.strzibny.name/single-attribute-in-place-editing-turbo/ (написано в январе 2022 г.)
Учебник не совсем соответствует Ruby 3.2.0, Rails 7.0.4 и требует настройки одной переменной на странице показа для работы.
К сожалению, в настоящее время в этом учебном методе нет отзывов о проверке, так как реализованная форма turbo_frame не включает его.
Вопрос: как правильно добавить обратную связь по валидации и маршрутизацию ошибок? (желательно решение только для turbo_frames)
Краткое содержание учебника:
- создать новое приложение и построить одну модель: Имя пользователя: строка
- изменения в UsersController (новое действие на контроллере для редактирования одного атрибута и добавление edit_name в список before_action)
before_action :set_user, only: %i[ show edit edit_name update destroy ]
# GET /users/1/edit_name
def edit_name
end
- добавить в route.rb (новый маршрут для редактирования одного конкретного атрибута)
resources :users do
member do
get 'edit_name'
end
end
- создайте view/users/ edit_name.html.erb (новая страница просмотра для поддержки редактирования определенного атрибута (здесь имя)).
<%= turbo_frame_tag "name_#{user.id}" do %>
<%= form_with model: @user, url: user_path(@user) do |form| %>
<%= form.text_field :name %>
<%= form.submit "Save" %>
<% end %>
<% end %>
- дополнения в файле _user.html.erb (ссылка на форму созданного турбо фрейма edit_name.html.erb)
<%= turbo_frame_tag "name_#{user.id}" do %>
Name: <%= link_to @user.name, edit_name_user_path(@user) %>
<% end %>
При запуске сервера приложений я получаю сообщения об ошибках о том, что @user равен nil:Class.
Чтобы учебник заработал, я должен изменить файл _user.html.erb, чтобы использовать локальную переменную для пользователя в ссылке.
- снова отредактировал _user.html.erb (изменив переменную экземпляра @user на локальную переменную user)
<%= turbo_frame_tag "name_#{user.id}" do %>
Name: <%= link_to user.name, edit_name_user_path(user) %>
<% end %>
С этим изменением учебник работает, позволяя редактировать один атрибут на месте с помощью турбокадров! Но обратная связь по проверке модели не реализована.
Ниже я пытаюсь разобраться с валидацией, сначала добавляя валидацию в models/user.rb
class User < ApplicationRecord
validates :name, presence: true
validates :name, comparison: { other_than: "Jason" }
end
ПРЕДЛОЖЕННОЕ РЕШЕНИЕ:
СОЗДАЙТЕ новый файл turbo_stream для всплывающих ошибок редактирования (у него есть ошибка в теге turbo_frame, на который он нацелен, он должен иметь возможность ориентироваться на любой родительский турбофрейм, где было инициировано редактирование одного атрибута)
<%= turbo_stream.replace"name_#{@user.id}" do %>
<%= form_with model: @user, url: user_path(@user) do |form| %>
<% if @user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% @user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<% if @user.errors[:name].any? %>
<%= form.label :name, style: "display: block" %> <%= form.text_field :name %>
<% end %>
<% if @user.errors[:active].any? %>
<%= form.label :active, style: "display: block" %> <%= form.check_box :active %>
<% end %>
<%= form.submit "Save" %>
<% end %>
<% end %>
и отредактируйте метод обновления UsersController.rb для устранения ошибок турбопотока.
# PATCH/PUT /users/1 or /users/1.json
def update
respond_to do |format|
if @user.update(user_params)
format.html { redirect_to user_url(@user), notice: "User was successfully updated." }
format.json { render :show, status: :ok, location: @user }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
format.turbo_stream do
if @user.errors[:name].any?
@user.name = nil #so that it does not repopulate the form with the bad data
if @user.errors[:active].any?
@user.active = nil
end
render :edit_errors, status: :unprocessable_entity
end
end
end
end
Все это работает, за исключением того, что после успешного редактирования формы, созданной после недопустимой записи, он отображает шоу только для этой записи, а не для всех.
Каким будет «сухой» способ сделать все это? (и как мне настроить обновление только одного кадра из турбо-потока, чтобы после успешной проверки обновлялось только одно поле)?
С философской точки зрения, стоит ли это того, чтобы просто использовать jQuery и Gem Best_In_Place??? Похоже, количество изменений накапливается, и код станет уродливым, если будет поддерживать такую функциональность по нескольким атрибутам?
1 ответ
Поскольку первоначальная проблема решена, я просто добавлю несколько других способов сделать это. Самостоятельно сделать это будет немного сложнее, и у вас не будет всех функций, которые может дать вам какой-нибудь драгоценный камень. С другой стороны, это намного меньше кода, и у вас есть полный контроль над всем. Кроме того, если вам просто нужно, чтобы это одно поле было редактируемым, установка драгоценного камня и jquery требует слишком много накладных расходов.
Настраивать:
# rails v7.0.4.2
$ rails new hello_edit_in_place -c tailwind
$ cd hello_edit_in_place
$ bin/rails g scaffold User email first_name last_name --skip-timestamps
$ bin/rails db:migrate
$ bin/rails runner "User.create(email: 'admin@localhost', first_name: 'super', last_name: 'admin')"
$ open http://localhost:3000/users
$ bin/dev
class User < ApplicationRecord
validates :email, presence: true, length: {minimum: 3}
end
Турбо рама
Я просто изменю форму по умолчанию и не буду касаться контроллера в качестве быстрого примера:
# app/views/users/_form.html.erb
# NOTE: this lets you render this partial and pass a local `:attribute` or
# get attribute from url params.
<% if attribute ||= params[:attribute] %>
<%= turbo_frame_tag dom_id(user, attribute) do %>
# NOTE: send `attrtibute` back in case of validation error, so this page
# can be rendered again with params[:attribute] set.
# V
<%= form_with model: user, url: user_path(user, attribute:) do |f| %>
<%= f.text_field attribute %>
# NOTE: show validation errors
<%= safe_join user.errors.full_messages_for(attribute), tag.br %>
<%= f.submit "save" %>
<% end %>
<% end %>
<% else %>
# original form here
<% end %>
# app/views/users/_user.html.erb
# NOTE: there is no need to have the whole set up for each individual
# attribute
<% user.attribute_names.reject{|a| a =~ /^(id|something_else)$/}.each do |attribute| %>
<%= tag.div attribute, class: "mt-4 block mb-1 font-medium" %> # tag.div - so that i can keep rb syntax highlight for stackoverflow
<%= turbo_frame_tag dom_id(user, attribute) do %>
<%= link_to edit_user_path(user, attribute:) do %>
<%= user.public_send(attribute).presence || "—".html_safe %>
<% end %>
<% end %>
<% end %>
Вот и все, каждый атрибут отображается, доступен для редактирования, а электронная почта показывает ошибки проверки. Также потому, что всеturbo_frame_tag
имеют уникальныйid
, все работает с несколькими пользователями на индексной странице.
Турбо поток
Вы также можете использоватьturbo_stream
чтобы иметь больше гибкости и сделать его еще более динамичным, но это немного больше настройки. Кроме того, добавьте возможность редактирования полного имени вместе с полями first_name и last_name :
# config/routes.rb
# NOTE: to not mess with default actions, add new routes
resources :users do
member do
get "edit_attribute/:attribute", action: :edit_attribute, as: :edit_attribute
patch "update_attribute/:attribute", action: :update_attribute, as: :update_attribute
end
end
# app/views/users/_user.html.erb
# Renders user attributes.
# Required locals: user.
<%= render "attribute", user:, attribute: :email %>
<%= render "attribute", user:, attribute: :name %>
# app/views/users/_attribute.html.erb
# Renders editable attribute.
# Required locals: attribute, user.
<%= tag.div id: dom_id(user, attribute) do %>
<%= tag.div attribute, class: "mt-4 block mb-1 font-medium" %>
# NOTE: to make a GET turbo_stream request vvvvvvvvvvvvvvvvvvvvvvvvvv
<%= link_to edit_attribute_user_path(user, attribute:), data: {turbo_stream: true} do %>
# far from perfect, but gotta start somewhere
<% if user.attribute_names.include? attribute.to_s %>
<%= user.public_send(attribute) %>
<% else %>
# if user doesn't have provided attribute, try to render a partial
<%= render attribute.to_s, user: %>
<% end %>
<% end %>
<% end %>
# app/views/users/_name.html.erb
# Renders custom editable attribute value.
# Required locals: user.
<%= user.first_name %>
<%= user.last_name %>
# app/views/users/_edit_attribute.html.erb
# Renders editable attribute form.
# Required locals: attribute, user.
<%= form_with model: user, url: update_attribute_user_path(user, attribute:) do |f| %>
<% if user.attribute_names.include? attribute.to_s %>
<%= f.text_field attribute %>
<% else %>
# NOTE: same as before but with `_fields` suffix,
# so this requires `name_fields` partial.
<%= render "#{attribute}_fields", f: %>
<% end %>
<%= f.submit "save" %>
<% end %>
# app/views/users/_name_fields.html.erb
# Renders custom attribute form fields.
# Requires locals:
# f - form builder.
<%= f.text_field :first_name %>
<%= f.text_field :last_name %>
# app/controllers/users_controller.rb
# GET /users/:id/edit_attribute/:attribute
def edit_attribute
attribute = params[:attribute]
respond_to do |format|
format.turbo_stream do
# render form
render turbo_stream: turbo_stream.update(
helpers.dom_id(user, attribute),
partial: "edit_attribute",
locals: {user:, attribute:}
)
end
end
end
# PATCH /users/:id/update_attribute/:attribute
def update_attribute
attribute = params[:attribute]
attribute_id = helpers.dom_id(user, attribute)
respond_to do |format|
if user.update(user_params)
format.turbo_stream do
# render updated attribute
render turbo_stream: turbo_stream.replace(
attribute_id,
partial: "attribute",
locals: {user:, attribute:}
)
end
else
format.turbo_stream do
# render errors
render turbo_stream: turbo_stream.append(
attribute_id,
html: (
helpers.tag.div id: "#{attribute_id}_errors" do
# FIXME: doesn't render `first_name` `last_name` errors
helpers.safe_join user.errors.full_messages_for(attribute), helpers.tag.br
end
)
)
end
end
end
end
private
def user
@user ||= User.find(params[:id])
end