Объекты сложной формы с 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-репозиторий этого сообщения в блоге.