Help me figure out how to display form validation errors using gem Reform . I could not think of anything else but how to use notice as the output of validation errors.

The problem is that if the form does not pass validation - the current state of the form is not saved (example in the picture). That is, if the form contains 20 fields and the user made a mistake in one thing - he will have to refill the form again. And this is not correct.

The documentation on this issue could not find anything.

example of a classic validation error output

Form-object:

 class AddressForm < Reform::Form include Reform::Form::ActiveModel include Reform::Form::ActiveModel::FormBuilderMethods model :address property :first_name property :last_name property :address property :city property :country_id property :zipcode property :phone extend ActiveModel::ModelValidations copy_validations_from Address end 

Controller:

 class Users::ProfilesController < ApplicationController before_action :authenticate_user! before_action :set_user before_action :create_form, only: [:update, :edit] def edit end def update form = AddressForm.new(address_type) if form.validate(params_type) form.save redirect_to edit_profile_path else render :edit, notice: form.errors.full_messages end end def address_type return Address.find(@user.billing_address) if params[:billing] return Address.find(@user.shipping_address) if params[:shipping] end def params_type return params[:billing] if params[:billing] return params[:shipping] if params[:shipping] end private def set_user @user = current_user end def profile_params params.require(:user). permit(:email, :password, :password_confirmation, :current_password) end def create_form @address_form = AddressPresenter.new(@user) end end 

Presenters

 class AddressPresenter include ActiveModel::Model attr_reader :billing_form, :shipping_form def initialize(user) @user = user @billing_form = AddressForm.new(address('billing')) @shipping_form = AddressForm.new(address('shipping')) end private def address(type) address_id = eval("@user.#{type}_address_id") address_id ? Address.find(address_id) : Address.new end end 

View

 .row .col-md-6 = form_for @address_form.billing_form, as: :billing, url: {action: 'update'} do |f| %h3= t('checkout.billing_address') %hr.style3 = render 'static/error_messages', target: @address_form.billing_form = render 'address/form', f: f = f.submit t('page.save_button'), class: 'btn btn-primary' .col-md-6 = form_for @address_form.shipping_form, as: :shipping, url: {action: 'update'} do |f| %h3= t('checkout.billing_address') %hr.style3 = render 'static/error_messages', target: @address_form.shipping_form = render 'address/form', f: f = f.submit t('page.save_button'), class: 'btn btn-primary' 

Update

I do not understand what the problem is, but if you do everything strictly along the docks, remove the presenters and for one form, everything works — both validation and change of form.

Modified controller code:

 class Users::ProfilesController < ApplicationController before_action :authenticate_user! before_action :set_user def billing_address @user.billing_address || Address.new end def edit # сюда приходят предзаполненные данные @billing_address ||= AddressForm.new(billing_address) end def update form = AddressForm.new(billing_address) if form.validate(params[:billing]) form.save redirect_to action: :edit else render :edit end end private def set_user @user = current_user end end 

View

 = form_for @billing_address, as: :billing, url: {action: 'update'} do |f| %h3= t('checkout.billing_address') %hr.style3 = render 'static/error_messages', target: @billing_address = render 'address/form', f: f = f.submit t('page.save_button'), class: 'btn btn-primary' 
  • one
    Well, right, in the new presenter there are no getters for getting forms. Either add attr_reader :billing_form, :shipping_form , or delete the constructor and the class inherit from Struct.new(:billing_form, :shipping_form) . - anoam
  • @anoam Thank you! If you can have one more moment please tell me - after I add getters to the presenter, the form is rendered, but when the pre-filled fields change, the data is not overwritten. Debug showed that in the update method in the form form = choose_form old data is written. Although new screen debugs come to params - Alexandr Dmitrenko
  • one
    corrected your answer. So must complain. And yet, I have now carefully looked at the full code of the controller - I advise you to bring the work with addresses into a separate one. And so much of a controller is able. - anoam
  • @anoam unfortunately does not fail. Do not tell me in which direction to look for a problem? Perhaps something is wrong with the presenter? - Alexandr Dmitrenko
  • @anoam As far as I understand from debugging, the problem is not validation, but the fact that in the update method the old data comes in the form (pre-filled) - Alexandr Dmitrenko

3 answers 3

It's simple. Form (s), in fact, retain their values. The problem is that the rendering uses OTHER objecte forms. In the update action builds its own form-obekt:

 form = AddressForm.new(address_type) 

But when rendering, form-objects are used, which are created independently, inside the presenter. Naturally, they know nothing about the data entered by the user. The option "in the forehead" is to use the forms of the presenter, but it will not be entirely correct in terms of architecture. I recommend simply rewriting the controller and presenter so that they interact with the same object forms. For example:

Controller:

 helper_method :presenter def update form = choose_form # upd!! if form.validate(address_params) form.save redirect_to action: :edit else render action: :edit end end # ... private def choose_form billing? ? billing_form : shipping_form end def billing? params[:billing].present? end def billing_form # UPD2 @billing_form ||= AddressForm .new( Address.find(@user.billing_address) #кстати, подозреваю что find здесь лишний ) end def shipping_form # UPD2 @shipping_form ||= AddressForm .new( Address.find(@user.shipping_address) ) end def presenter @presenter ||= AddressPresenter.new(billing_form, shipping_form) end # upd!! def address_params params.permit(billing? ? :billing : :shipping).permit! #вообще правильно пермитить только нужные параметры, но форм-обжект всё равно лишнее не пропустит. end 

Presenter:

 def initialize(billing_form, shipping_form) @billing_form = billing_form @shipping_form = shipping_form end 

View:

 = form_for presenter.billing_form, #... 

Well, the forms should not be cleared, and you can use f.error_messages and other form-builder buns in them.

Why I advise exactly this option, and not something like prensenter.shipping_form.validate... : 1. The presenter is freed from dependencies. Eval, which in itself is smell, leaves, and here it is also made “do not sew up the sleeve”. 2. create_form of create_form on a callback. Presenter will be created lazily.

And yet, a little offtopic: In the presenter, there is an extra ActiveModel::Model . It seems he is not needed anywhere. Anyway, it can be simplified to the usual Struct .

  • thanks for the example. Address.find really turned out to be superfluous. I did everything as you wrote but came across another problem - now the view is not rendered. Error link - Alexandr Dmitrenko
  • one
    AddressPresenter it possible to see the actual AddressPresenter code?) - anoam
  • thank! Code added. - Alexandr Dmitrenko

This is an idiom in Rails, but its description is really not very useful.

The trick relies on the fact that the same objects are filled in the edit and update for drawing the same view: edit . It works with everything that is compatible with ActiveModel , probably with Reform forms too.

You are expected to:

  • in the edit action inside @address_form.billing_form ( form_for argument) the form will be empty (or with default values
  • in the update action, in case of an error , the form inside @address_form.billing_form will be filled in with what came from the user (even if the saving ended in errors)

The last rule in your code is not executed: the form with errors is only in the local form in the controller. You need to make sure that after the failed update in @address_form.billing_form , what you have in the form now lies.

    If you need to remove error messages under each field from the form, you do not need to use a specialized gem for this. This feature is provided by regular Ruby on Rails tools. To do this, in the view, when displaying the form, you need to refer to the errors method of the current model and select among the errors those that relate to the current field

     = f.text_field :slug, class: 'form-control' - if f.object.errors[:slug] .field_error - f.object.errors[:slug].each do |msg| span= msg 

    If we move the processing section to a separate partition

     - if object.errors[field] .field_error - object.errors[field].each do |msg| span= msg 

    You can enable error handling in one line.

     = f.text_field :name, class: 'form-control' = render 'admin/shared/field_errors', object: f.object, field: :name = f.text_field :slug, class: 'form-control' = render 'admin/shared/field_errors', object: f.object, field: :slug