Как отредактировать один встроенный атрибут с помощью 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 || "&mdash;".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
Другие вопросы по тегам