In the previous part, I talked about controllers and routing. Now let's talk about the form. Quite often, it is required to implement forms that do not correspond to any model. Or add a validation that only makes sense in a particular business process.
I will tell about 2 types of forms: form-objects and types.
Object forms are used to process and validate user input when data is needed for an operation. For example, user login or data filtering.
Types are used if you want to expand the behavior of the model. For example, in your project, users can register both through vkontakte, and through the usual form. Filling email is mandatory for regular users, but not for vk users. This behavior is easily solved using types.
In RoR projects, forms are rigidly tied to models. Rendering a complex form without a model object is almost impossible and not convenient. Therefore, form objects are extended using ActiveModel :: Model . Thus, forms are models without persistence support (they are not saved in the database). Accordingly, we will get seamless integration with the form builder, validation, localization.
For convenience, form objects also use gem virtus . It assumes type casting, sets default values. For example, if a date comes from a form in a string representation, then virtus automatically converts it to a date.
# # app/forms/base_form.rb class BaseForm include Virtus.model(strict: true) include ActiveModel::Model end # app/forms/user/statistics_filter_form.rb class User::StatisticsFilterForm < BaseForm attribute :start_date, ActiveSupport::TimeWithZone, default: ->(*) { DateTime.current.beginning_of_month } attribute :end_date, ActiveSupport::TimeWithZone, default: ->(model, _) { model.start_date.next_month } end # app/controllers/web/users/statistics_controller.rb class Web::Users::StatisticsController < Web::Users::ApplicationController def show # permits, .. active_record @filter_form = User::StatisticsFilterForm.new params[:user_statistics_filter_form] @statistics = UserStatisticsQuery.perform resource_user, @filter_form.start_date, @filter_form.end_date end end = simple_form_for @filter_form, method: :get, url: {} do |f| = f.input :start_date, as: :datetime_picker = f.input :end_date, as: :datetime_picker = f.button :submit
Consider the situation more difficult. We have a login form with two fields: email and password. Fields are required. Also, if the user is not found or the password did not match, the corresponding error should be displayed.
# app/forms/session_form.rb class SessionForm < BaseForm attribute :email attribute :password validates :email, email: true validates :password, presence: true # , validate do errors.add(:base, :wrong_email_or_password) unless user.try(:authenticate, password) end def user @user ||= User.find_by email: email end end # app/controllers/web/sessions_controller.rb class Web::SessionsController < Web::ApplicationController def new @session_form = SessionForm.new end def create @session_form = SessionForm.new session_form_params # if @session_form.valid? sign_in @session_form.user redirect_to root_path else render :new end end private def session_form_params params.require(:session_form).permit(:email, :password) end end
In this example, the form assumes all concerns about validating the input data, the controller does not contain unnecessary logic, and the model only verifies the password.
With this approach, it is very easy to implement additional functionality: display the "remember me" checkbox, block users from the black list.
If I'm not mistaken, Types came from symfony. Type is the heir to the model, which impersonates itself as a parent and adds new functionality.
Consider this task: users of the application can invite users only a rank lower than themselves. Also, the inviter does not need to know the password of the invitee. The list of user roles that can be assigned to a new user is determined by the policy. In the part about ACL I will tell you more about this.
module BaseType extend ActiveSupport::Concern class_methods do def model_name superclass.model_name end end end class InviteType < User include BaseType after_initialize :generate_password, if: :new_record? validates :role, inclusion: { in: :available_roles } validates :inviter, presence: true # def policy InvitePolicy.new(inviter, self) end def available_roles policy.available_roles end def available_role_options User.role.options.select{ |option| option.last.in? available_roles } end private def generate_password self.password = SecureRandom.urlsafe_base64(6) end end
InviteType checks for the presence of the invitee, generates a password and restricts the list of available roles.
I will focus more on BaseType. It overrides the model_name method so that the type is perceived as a parent object. You should not override the name method, since ruby because of this blows the roof. There is a subtlety when working with STI: you need to additionally override the method sti_name.
Having object-forms and types it is convenient to transform the data coming from the form. For example, there are 2 fields in a form: hours spent, minutes spent, and the model stores the elapsed time in seconds.
class CommentType < Comment include BaseType # some code def elapsed_time_hours TimeConverter.convert_to_time(elapsed_time.to_i)[:hours] end def elapsed_time_hours=(v) update_elapsed_time v.to_i, elapsed_time_minutes end def elapsed_time_minutes TimeConverter.convert_to_time(elapsed_time.to_i)[:minutes] end def elapsed_time_minutes=(v) update_elapsed_time elapsed_time_hours, v.to_i end private def update_elapsed_time(hours, minutes) self.elapsed_time = TimeConverter.convert_to_seconds(hours: hours, minutes: minutes) end end
PS The article was written over a year ago and just lay in the archive. During this time, I shared a project on the basis of which this series of articles was written. I would do some things differently; nevertheless, there are interesting and relevant decisions in it.
Source: https://habr.com/ru/post/321034/
All Articles