Объекты сложной формы с Rails

Проблема, которую я хотел решить

Я давно использую REST, но иногда он не подходит для моих приложений. Иногда при создании формы вам приходится передавать вложенные атрибуты, и это заканчивается полной катастрофой.

Что такое объекты формы?

Объекты формы позволяют легко управлять операцией, например: user с этими social_networks.

Но зачем формировать объекты?

  • Чистые контроллеры
  • Бизнес-логика остается на своем месте
  • Валидации должны принадлежать объектам форм, а не моделям дополненной реальности.
  • Красиво отображать ошибки в формах

Примечание: Если вы создавали большие приложения Rails, вы заметите, что добавление проверок и правил к вашим моделям ограничит возможность изменения ваших моделей в будущем, поэтому объекты форм являются отличным решением этой проблемы. Теперь у вас могут быть разные правила (проверки) для разных действий в вашем приложении, и ваши модели больше не будут ограничиваться проверками.

Создание объектов сложной формы в Rails

Мы собираемся описать распространенный сценарий…

Обновите активную модель записи с помощью множества ассоциаций

Допустим, у вас есть форма, в которой вы хотите обновить имя пользователя и его социальные сети.

Имея в виду, что наша главная забота — это наши проверки, мы не хотим писать какие-либо проверки в наших моделях, потому что мы уже знаем все неудобства.

В качестве решения мы будем добавлять проверки, которые необходимы для этого требования, т.е. «обновить имя пользователя с его социальными сетями».

app/forms/update_user_with_social_networks_form.rb

class UpdateUserWithSocialNetworksForm
  include ActiveModel::Model
  
  attr_accessor(
    :success, # Sucess is just a boolean
    :user, # A user instance
    :user_name, # The user name
    :social_networks, # The user social networks
  )

  validates :user_name, presence: true
  validate :all_social_networks_valid

  def initialize(attributes={})
    super
    # Setup the default user name
    @user_name ||= user.name
  end

  def save
    ActiveRecord::Base.transaction do
      begin
        # Valid will setup the Form object errors
        if valid?
          persist!
          @success = true
        else
          @success = false
        end
      rescue => e
        self.errors.add(:base, e.message)
        @success = false
      end
    end
  end

  # This method will be used in the form
  # remember that `fields_for :social_networks` will ask for this method 
  def social_networks_attributes=(social_networks_params)
    @social_networks ||= []
    social_networks_params.each do |_i, social_network_params|
      @social_networks.push(SocialNetworkForm.new(social_network_params))
    end
  end

  private

  # Updates the user and its social networks
  def persist!
    user.update!({
      name: user_name, 
      social_networks_attributes: build_social_networks_attributes
    })
  end

  # Builds an array of hashes (social networks attributes)
  def build_social_networks_attributes
    social_networks.map do |social_network|
      social_network.serializable_hash
    end
  end

  # Validates all the social networks
  # using the social network form object,
  # which has its own validations
  # we are going to pass those validations errors
  # to this object errors.
  def all_social_networks_valid
    social_networks.each do |social_network|
      next if social_network.valid?
      social_network.errors.full_messages.each do |full_message|
        self.errors.add(:base, "Social Network error: #{full_message}")
      end
    end
    throw(:abort) if errors.any?
  end
end

app/forms/social_network_form.rb

class SocialNetworkForm
  include ActiveModel::Model
  include ActiveModel::Serialization
  
  attr_accessor(
    :id,
    :url
  )

  validates :url, presence: true

  def attributes
    { 
      'id' => nil,
      'url' => nil
    }
  end
end

app/controllers/update_users_with_social_networks_controller.rb

class UpdateUserWithSocialNetworksController < ApplicationController
  
  before_action :build_form, only: [:update]

  def edit
    @form = UpdateUserWithSocialNetworksForm.new(user: current_user)
    @social_networks = current_user.social_networks
  end

  def update
    @form.save
    if @form.success
      redirect_to root_path, notice: 'User was successfully updated.' 
    else
      render :edit
    end 
  end

  private

  def permitted_params
    params
      .require(:update_user_with_social_networks_form)
      .permit(:user_name, social_networks_attributes: [:id, :url])
  end 

  def build_form
    @form = UpdateUserWithSocialNetworksForm.new({
      user: current_user,
      user_name: permitted_params[:user_name],
      social_networks_attributes: permitted_params[:social_networks_attributes]
    })
  end
end

app/views/update_user_with_social_networks/edit.html.erb

<%= form_for(@form, url: update_user_with_social_networks_path, method: :put) do |f| %>
  <% if @form.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@form.errors.count, "error") %> prohibited this User from being saved:</h2>
      <ul>
        <% @form.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :user_name %>
    <%= f.text_field :user_name %>
  </div>

  <%= f.fields_for :social_networks, @social_networks do |sn_form| %>
    <div class="field">
      <%= sn_form.label :url %>
      <%= sn_form.text_field :url %>
    </div>
  <% end %>

  <div class="actions">
    <%= f.submit "Update" %>
  </div>
<% end %>

config/routes.rb

Rails.application.routes.draw do
  root 'update_user_with_social_networks#edit'
  resource :update_user_with_social_networks, only: [:edit, :update]
end

Заметки

  • Этот объект формы не будет обрабатывать создание ассоциаций
  • Этот объект формы не будет обрабатывать удаление ассоциации (знак уничтожения _destroy)

Основные выводы

Как вы можете видеть, код не очень приятный, чтобы настроить сложные объекты формы, вам нужно немного взломать Rails, как я показал в этом блоге.

Советы и советы

Существует гем, который выводит объекты форм на новый уровень (он обрабатывает вложенные ассоциации и многое другое) Реформа

Заключительные мысли и следующие шаги

Хотя объекты формы являются отличным шаблоном проектирования, они могут стать очень сложными в зависимости от вашей бизнес-логики.
Пример, показанный выше, — это то, что я никогда не буду делать в реальной жизни, поскольку это плохая идея — сохранять один ресурс и несколько ресурсов за один раз, в идеале вы сначала делаете один запрос на обновление одного ресурса, а затем делаете другой запрос на обновление другого ресурса ( с). Итак, это просто демонстрация возможностей объектов формы, которые можно закодировать разными способами для достижения разных целей.

GraphQL имеет огромное преимущество перед REST и объектами форм, поскольку вы можете использовать мутации и добавлять пользовательские проверки к своим действиям. Недостатком является то, что вам понадобится клиент для использования GraphQL API, а Rails не поставляется с ним (пока).

Здесь вы можете найти Github-репозиторий этого сообщения в блоге.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *