3

It appears to be normal functionality for a Rails form that has been submitted with invalid data to display error messages and repopulate the form with the invalid data. At least, all my Active Record managed resources work this way.

However, I have a virtual resource/model that is not an Active Record but instead makes use Rails 3 ActiveModel along the lines of Railscast #219.

Everything works as I expect, apart from when submission fails. The re-rendered form is empty. Error messages are displayed as normal.

Which part of Rails is responsible for this functionality and how do I include it in my ActiveModel based resource?

To clarify:

  1. A user completes and submits a form containing two values.
  2. Validation fails because one or more of these values is invalid.
  3. The controller re-renders the 'new' view for the model.
  4. The re-rendered form contains error messages.
  5. The re-rendered form is pre-populated with the two invalid data values.

It is step 5 that I do not understand and which is not happening with my Active Model resource, despite having near identical controller code and identical view code, to my Active Record resources.

The code (as requested)

Controller:

def new
  @contact = Contact.new
end

def create
  @contact = Contact.new(params[:contact])

  if @contact.valid?
    AgentMailer.contact_email(@contact).deliver
    redirect_to "/contact", notice: 'Your request was successful.' 
  else
    #TODO Figure out why data from an invalid submission does not carry over
    render action: "new"
  end
end

View:

<%= form_for(@contact, :html => {:class => "new_contact custom"}) do |f| %>
  <% if @contact.errors.any? %>
    ...
  <% end %>

  ...

  <%= f.label :name %>
  <%= f.text_field :name, options = {placeholder: "Enter name here"} %>     

  ...
<% end %>
  • Can you post your controller and view code to clarify the issue? – Puhlze Jun 10 '13 at 22:10
  • Yes post your controller/view. This should work by default. The solution you post is not the right way to go about this. If it does work it is for the wrong reasons. – drhenner Jun 10 '13 at 22:25

2 Answers2

1

The scaffold command creates form partials that include the error messages you mention. No magic here, just a view that is rendering errors from the model if they exist. You can use the errors ERB code from below as a template for how to add error messages to other views.

rails new test_project
cd test_project
rails generate scaffold users name

Look at app/views/users/_form.html.erb

 <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
Puhlze
  • 2,634
  • 18
  • 16
  • Thanks for the response Puhlze. Perhaps I didn't make myself clear. I have the error explanation section in my form partial and it works well. It is just the field values that do not carry over. Do you know how rails does this? Thanks – Anthony Hepple Jun 10 '13 at 19:57
  • Check out the form_for helper: http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html It creates html forms using values from the provided model. If you're using the form_for helper and values aren't populating, make sure that values are being set on your model correctly in your controller. – Puhlze Jun 10 '13 at 20:25
  • I'm sure that the form_for helper plays a part in carrying over field values but it's the detail of the carrying over that I would like to know. I cannot seem to find it in the api page you linked to or any other for that matter. I have edited the question in an attempt to clarify my question. Thanks for your suggestions. – Anthony Hepple Jun 10 '13 at 21:33
1

By trial and error I found a solution;

For this little bit of Rails magic to work, my model required an initializer of the form:

def initialize(attributes = {})
  attributes.each do |name, value|
    send("#{name}=", value) if respond_to?("#{name}=")
  end
end

I suspect that it is the Validations module of ActiveModel that performs the magic. It would be great if someone could confirm this and point me to some useful documentation.

Update

I believe that the above method is secure because the send method does not circumvent appropriate accessor methods, and robust due to the respond_to check. I'm still not certain which part/module of Rails creates a new model instance when re-rendering a the view .

  • Why not? My controller and view code is trivial and equivalent to my ActiveRecord based resources, with the exception of using @instance.valid? instead of @instance.save? – Anthony Hepple Jun 10 '13 at 22:32
  • You are overriding ActiveRecord for no reason at all. The things that can go wrong are high with no benefit. Rails does give you this for free. – drhenner Jun 11 '13 at 02:03
  • BTW: I hope this doesn't sound negative. I really do just want to help. Post the code and you might get a better solution. – drhenner Jun 11 '13 at 02:04
  • Thanks drhenner, but remember that my model is not an ActiveRecord. Could it not simply be that this initializer is necessary for Rails to provide the functionality that I want? Also, if ActiveRecord models get this for free, what harm can come to an non-ActiveRecord model that has such an initializer? Thanks – Anthony Hepple Jun 11 '13 at 11:14
  • Yup... My bad. I forgot and didn't re-read the post when I made the comments. I just saw the solution and thought "AR models shouldn't..." My bad – drhenner Jun 12 '13 at 16:39
  • So I'm having the same issue with a virtual attribute on my user model. The user is an active record model with the exception of one attribute not being carried over (an invitation token that's part of the url, and subsequently lost when the form is rerendered). Normally the invite token populates a hidden field. How can I retain the token in the field if there's a form error? – Chris Hawkins Jul 18 '13 at 05:12
  • Chris, does the name of your hidden field use the array format? E.g. `name="user[invite]"` In my experience, the key to carrying over all attributes, real or virtual, is ensuring that the instance assigned by the controller, contains **all** attributes. – Anthony Hepple Jul 18 '13 at 09:56