Один из способов справиться с этим - использовать виртуальную модель, которая обрабатывает параметры привязки к форме и из формы:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
end
Затем можно настроить форму:
<%= form_with(model: (@search_query || SearchQuery.new), url: '/experiences', method: :get) %>
<div>
<%= f.label :team_size %>
<%= f.number_field :team_size %>
</div>
# ..
<% end %>
И затем вы можете просто привязать параметры к модели с помощью ActionController::Parameters#permit
, как если бы вы делали это с обычной моделью ActiveRecord:
class ExperiencesController
before_action :set_search_query, only: :index, if: ->{ params[:search_query].present? }
# ...
def index
@experiences = if @search_query
@search_query.build_scope(policy_scope(Experience))
else
policy_scope(Experience)
end.geocoded
end
private
def set_search_query
@search_query = SearchQuery.new(search_query_params)
end
def search_query_params
params.fetch(:search_query).permit(:team_size, :duration, :price)
end
end
Эта петля сделает форму с состоянием точно такой же, как ваши обычные формы CRUD. На самом деле мы не реализовали #build_scope
да, поэтому давайте сделаем так:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
def build_scope(base_scope)
compacted_attributes = attributes.reject { value.nil? || value.empty? }
compacted_attributes.each_with_object(base_scope) do |(k,v), base|
if base.respond_to? "filter_by_#{k}"
# lets you customize the logic with a scope
base.send("filter_by_#{k}", v) # the scope is responsible for ordering
else
# convention over configuration!
base.where(Hash[k,v]).order(Hash[k,:desc])
end
end
end
end
Поскольку это использует соглашение о конфигурации, вы можете избавиться от этих бессмысленных областей в вашей модели.