I have a registration form model that takes the users input during registration:
class RegForm
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :company_name, :email, :password
validates_presence_of # ...
end
During this registration process I have multiple models that need to be created, and I am not sure how to properly display error messages and how to bubble the model error messages back to the UI.
if @reg_form.valid?
account = Account.create!(@reg_form)
else
...
Account.create! looks like:
def self.create!(reg_form)
account = Account.create_from_reg!(reg_form)
location = location.create_from_reg!(account, reg_form)
..
..
account.location = location
..
account.save!
account
end
Let's start from the beginning.
We want our registration form object to have the same API as any other ActiveRecord model:
// view.html
<%= form_for(@book) do |f| %>
<% end %>
# controller.rb
def create
@book = Book.new(book_params)
if @book.save
redirect_to @book, notice: 'Book was successfully created.'
else
render :new
end
end
To do that, we create the following object:
class RegForm
include ActiveModel::Model
attr_accessor :company_name, :email, :password
def save
# Save Location and Account here
end
end
Now, by including ActiveModel::Model
, our RegForm
gains a ton of functionality, including showing errors and validating attributes (yes, it's unnecessary to include ActiveModel::Validations
). In this next step we add some validations:
validates :email, :password, presence: true
And we change save
so that it runs those validations:
def save
validate
# Save Location and Account here
end
validate
returns true
if all validations pass and false
otherwise.
validate
also adds errors to the @reg_form
(All ActiveRecord models have an errors
hash which is populated when a validation fails). This means that in the view we can do any of these:
@reg_form.errors.messages
#=> { email: ["can't be blank"], password: ["can't be blank"] }
@reg_form.errors.full_messages
#=> ["Email can't be blank", "Password can't be blank"]
@reg_form.errors[:email]
#=> ["can't be blank"]
@reg_form.errors.full_messages_for(:email)
#=> ["Email can't be blank"]
Meanwhile, our RegistrationsController
should look something like this:
def create
@reg_form = RegForm.new(reg_params)
if @reg_form.save
redirect_to @reg_form, notice: 'Registration was successful'
else
render :new
end
end
We can clearly see that when @reg_form.save
returns false
, the new
view is re-rendered.
Finally, we change save
so that our models save
calls are wrapped inside a transaction:
def save
if valid?
ActiveRecord::Base.transaction do
location = Location.create!(location_params)
account = location.create_account!(account_params)
end
true
end
rescue ActiveRecord::StatementInvalid => e
# Handle database exceptions not covered by validations.
# e.message and e.cause.message can help you figure out what happened
end
Points worthy of note:
create!
is used instead of create
. The transaction is only rolled back if an exception is raised (which methods with a bang usually do).
validate
is just an alias for valid?
. To avoid all that indentation we could instead use a guard at the top of the save
method:
return if invalid?
We can turn a database exception (like an email uniqueness constraint) into an error by doing something like:
rescue ActiveRecord::RecordNotUnique
errors.add(:email, :taken)
end
We can add an error not directly associated with an attribute by using the symbol :base
:
errors.add(:base, 'Company and Email do not match')
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With