Rails form object with reform-rails with collections not working or validating
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 %>
field_with_errors not being applied on reform form object
The easiest solution is to use simple_form_for gem which will create the entire component (label, input and inline error message):
<%= simple_form_for signup_form do |form| %>
<%= form.input :name %>
Otherwise you need to add the inline error messages yourself, for example:
<%= form.object.errors[:name].join(', ') %>
Form Objects in Rails
Complete Answer
Models:
#app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
#app/models/email.rb
class Email < ApplicationRecord
belongs_to :user
end
Controller:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
end
def new
@user_form = UserForm.new
@user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
@user_form = UserForm.new(user_form_params)
if @user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
Form Objects:
#app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :emails
validates :name, presence: true
validate :all_emails_valid
def emails_attributes=(attributes)
@emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
@emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email_form|
Email.new(email_text: email_form.email_text)
end
user.emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
end
Views:
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 :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 %>
Rails: Form Object not finding I18n on activemodel (namespacing issue)
Well I found out the answer by digging into the source code. The issue I was having that my form object
was in a Users::PasswordAndLoginUpdatingForm
namespace.
The trick is to use activemodel:
as your namespace, but, if you have further namespaces in your models, they're separated with /
s.
So, the correct yml looks like this:
en:
activemodel:
errors:
models:
users/password_and_login_updating_form:
attributes:
requested_email_address:
invalid: "foo"
Rails Form Object with Virtus: has_many association
I would just set the emails_attributes from user_form_params in the user_form.rb as a setter method. That way you don't have to customize the form fields.
Complete Answer:
Models:
#app/modeles/user.rb
class User < ApplicationRecord
has_many :user_emails
end
#app/modeles/user_email.rb
class UserEmail < ApplicationRecord
# contains the attribute: #email
belongs_to :user
end
Form Objects:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
include Virtus.model
attribute :name, String
validates :name, presence: true
validate :all_emails_valid
attr_accessor :emails
def emails_attributes=(attributes)
@emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
@emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email|
UserEmail.new(email: email.email_text)
end
user.user_emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
# app/forms/email_form.rb
# "Embedded Value" Form Object. Utilized within the user_form object.
class EmailForm
include ActiveModel::Model
include Virtus.model
attribute :email_text, String
validates :email_text, presence: true
end
Controller:
# app/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
end
def new
@user_form = UserForm.new
@user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
@user_form = UserForm.new(user_form_params)
if @user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
Views:
#app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: @user_form %>
#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 :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 %>
Unexpected naming of fields using Reform Form Object with Composition
You can call ::model only once! Once you call it with the correct model (or any name), the rendering will name the fields to whatever ::model you specify. The point about Reform is to hide internals about your model names!
Related Topics
Markdown to Plain Text in Ruby
Can't Find Gem Railties (>= 0.A) with Executable Rails (Gem::Gemnotfoundexception)
Shortening Socket Timeout Using Timeout::Timeout(N) Does Not Seem to Work for Me
Ruby on Rails How to Deal with Nan
Nanoc Changing the Base Path When Deploying Page in Github
How to Convert 1 to "First", 2 to "Second", and So On, in Ruby
The Program 'Rails' Is Currently Not Installed
Ruby: Too Many Open Files @ Rb_Sysopen
Rails Cancan and State MAChine - Authorizing States
+= Operator Appears to Modify Frozen String
Project Euler #3 in Ruby Solution Times Out
Scraping an Angularjs Application
Why Does 'Puts(Nil or 4)' Fail in Ruby
Linking Two Models in a Multi-Model Form
Rails Reload Dynamic Routes on Multiple Instances/Servers
Respond_With Redirect with Notice Flash Message Not Working
How Would I Go About Converting This Time String to Epoch Time in Ruby