Rails form object with reform-rails with collections not working or validating
Asked Answered
C

2

6

I am using the reform-rails gem In order to utilize a form object in my rails project.

I realize a form object is probably overkill for the example code I use below, but it is for demonstration purposes.

In the form I am creating a user, and associated to that user record are two user_emails.

# models/user.rb
class User < ApplicationRecord
  has_many :user_emails
end

# models/user_email.rb
class UserEmail < ApplicationRecord
  belongs_to :user
end

Notice that I am not using accepts_nested_attributes_for :user_emails within the User model. It appears to me that one of the main points of form objects is that it helps you get away from using accepts_nested_attributes_for, so that is why I am attempting to do this without it. I got that idea from this video which talks about refactoring fat models. I have the link pointing to the section of the video on form objects, and he expresses how much he dislikes accepts_nested_attributes_for.

I then proceed to create my user_form:

# app/forms/user_form.rb
class UserForm < Reform::Form
  property :name
  validates :name, presence: true

  collection :user_emails do
    property :email_text
    validates :email_text, presence: true
  end
end

So the user_form object wraps a user record and then a couple of user_email records associated to that user record. There are form-level validations on the user and on the user_email records this form wraps:

  • the user#name must have a value
  • each user_email#email_text must have a value

If the form is valid: then it should create one user record and then a couple of associated user_email records. If the form is not valid: then it should re-render the form with error messages.

I will show what I have in the controller thus far. For brevity: only displaying the new action and the create action:

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def new
    user = User.new
    user.user_emails.build
    user.user_emails.build
    @user_form = UserForm.new(user)
  end

  def create
    @user_form = UserForm.new(User.new(user_params))
    if @user_form.valid?
      @user_form.save
      redirect_to users_path, notice: 'User was successfully created.'
    else
      render :new
    end
  end

  private
    def user_params
      params.require(:user).permit(:name, user_emails_attributes: [:_destroy, :id, :email_text])
    end
end

Lastly: the form itself:

# app/views/users/_form.html.erb
<h1>New User</h1>
<%= render 'form', user_form: @user_form %>
<%= link_to 'Back', users_path %>

# app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
  <% if user_form.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(user_form.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% user_form.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>
  <% f.fields_for :user_emails do |email_form| %>
    <div class="field">
      <%= email_form.label :email_text %>
      <%= email_form.text_field :email_text %>
    </div>
  <% end  %>

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

As a test: here is the form with inputted values:

enter image description here

Now I proceed to submit. What should happen is there should be a validation error because a value for that second email must be present. However, when submitted here are the logs:

Parameters: {"utf8"=>"✓", "authenticity_token"=>”123abc==", "user"=>{"name"=>"neil", "user_emails_attributes"=>{"0"=>{"email_text"=>"email_test1"}, "1"=>{"email_text"=>""}}}, "commit"=>"Create User"}

ActiveModel::UnknownAttributeError (unknown attribute 'user_emails_attributes' for User.):

So there is some issue with my form object.

How can I get this form object to work? Is it possible to use reform_rails and get this form object to work without using accepts_nested_attributes? Ultimately: I just want to get the form objet to work.

Some resource I have already explored in addition to the reform-rails docs:

My first attempt to make a form object was with the virtus gem, but I could not seem to get that one working either. I did post a stackoverflow question for that implementation as well.

Carder answered 17/3, 2017 at 20:12 Comment(1)
You might want to check how Reform handles instantiation and validation of collection objects from parameters in the case that they've never been persisted. accepts_nested_attributes_for has options around rejecting blanks, for example.Killingsworth
P
8

Complete Answer:

Models:

# app/models/user.rb
class User < ApplicationRecord
  has_many :user_emails
end

# app/models/user_email.rb
class UserEmail < ApplicationRecord
  belongs_to :user
end

Form Object:

# app/forms/user_form.rb
# if using the latest version of reform (2.2.4): you can now call validates on property 
class UserForm < Reform::Form
  property :name, validates: {presence: true}

  collection :user_emails do
    property :email_text, validates: {presence: true}
  end
end

Controller:

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :user_form, only: [:new, :create]

  def new 
  end

  # validate method actually comes from reform this will persist your params to the Class objects
  # you added to the UserForm object. 
  # this will also return a boolean true or false based on if your UserForm is valid. 
  # you can pass either params[:user][:user_emails] or params[:user][user_email_attributes]. 
  # Reform is smart enough to pick up on both.
  # I'm not sure you need to use strong parameters but you can. 

  def create    
    if @user_form.validate(user_params)
      @user_form.save
      redirect_to users_path, notice: 'User was successfully created.'
    else
      render :new
    end
  end

  private

  # call this method in a hook so you don't have to repeat
  def user_form
    user = User.new(user_emails: [UserEmail.new, UserEmail.new])
    @user_form ||= UserForm.new(user)
  end 

  # no need to add :id in user_emails_attributes
  def user_params
    params.require(:user).permit(:name, user_emails_attributes: [:_destroy, :email_text])
   end
 end

The Form:

# app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: @user_form %>
<%= link_to 'Back', users_path %>

#app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
  <% if user_form.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(user_form.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% user_form.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>
  <%= f.fields_for :user_emails do |email_form| %>
    <div class="field">
      <%= email_form.label :email_text %>
      <%= email_form.text_field :email_text %>
    </div>
  <% end  %>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>
Pout answered 21/3, 2017 at 23:15 Comment(3)
Wow! Thanks so much! Please approve my minor edits which include all the pieces (for completion sake) and I will gladly mark your answer as the accepted answer!Carder
I will be working through your answer on the virtus implementation question too. If you are feeling up to it: feel free to checkout my raw rails 5 implementation question on form objects. Unfortunately I'm pretty much out of reputation points so I can't offer much of a bounty on that one.Carder
you have saved my life :DWavelength
M
1

I finally got it working!!!

First, I couldn't get it saving the collection on Rails 5. I created a 4.2.6 and it works our of the box. I suggest you to create an issue on the github repository page of the Reform gem.

So, this is the working code :

models/user.rb

class User < ActiveRecord::Base
  has_many :user_emails
end

models/user_email.rb

class UserEmail < ActiveRecord::Base
  belongs_to :user
end

user_form.rb

class UserForm < Reform::Form

  property :name
  validates :name, presence: true

  collection :user_emails, populate_if_empty: UserEmail do
    property :email_text
    validates :email_text, presence: true
  end
end

The populate_if_empty is important when validation occurs.

And the controller create method:

def create
  @user_form = UserForm.new(User.new)
  if @user_form.validate(user_params)
    @user_form.save
    redirect_to users_path, notice: 'User was successfully created.'
  else
    render :new
  end
end

This will validates your User model AS WELL AS any nested associations.

There you have it! Dry models, validations and saving of the model and associations.

I hope this helps!

Montmartre answered 22/3, 2017 at 4:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.