You learned about the basics of building forms in HTML and Rails in previous lessons and you can do a whole lot with that knowledge. But there are also cases when crafting a good user experience demands building a form that handles multiple things (e.g. model objects) at once. Users only like clicking the submission button once so you’d better be able to give them the simple experience they demand.
In this section, we’ll take a look at some of the options you have to make a form handle multiple model objects at once. You’ll also learn how to prepopulate a dropdown menu with objects.
By the end of this lesson, you should be able to:
- Build dropdown menus
- Work with model objects
- Make nested forms
- Whitelist nested parameters
- Delete records via form fields
You’ve got a form for creating one of your User objects (say for your Amazon.com clone application) but you also want to make that form create one or more ShippingAddress objects (which a User can have many of). How do you get that one form to create both so your user doesn’t get stuck clicking a bunch of form submits?
This is a multi-part process. It involves your controller, view, models and routes… the whole MVC team! The gist of it is that your form will submit the main object (e.g. the User) as normal but it will sneak in a bunch of attributes for the other object(s) you want to create (e.g. ShippingAddress object(s)). Your model will have to be ready for this. It will create not just the original User object but also the nested objects at the same time.
As you can imagine, it’s important to get the names and parameters properly listed so all this magic can happen behind the scenes.
We’ll do a broad overview of the process here:
1. You will need to prepare the User model so that it knows to create one or more ShippingAddress objects if it receives their attributes when creating a normal User. This is done by adding a method to your User model called
#accepts_nested_attributes_for which accepts the name of an association, e.g:
# app/models/user.rb class User < ActiveRecord::Base has_many :shipping_addresses accepts_nested_attributes_for :shipping_addresses end
- Make sure you’ve allowed your
paramsto include the nested attributes by appropriately including them in your Strong Parameters controller method. See the reading for examples of how to do this.
- Build the form in the view. Use the
#fields_formethod to effectively create a
#form_withinside your existing
There are a couple new aspects to this process. You saw
#fields_for in the Basic Forms lesson but it probably has new meaning to you now. It’s basically how you create a form within a form (which should make sense since it’s actually used behind the scenes by
#form_with). In this example, we might create three “sub-forms” for ShippingAddress objects by using our association, e.g.
<%= form_with model: @user do |f| %> ... <% 3.times do %> <%= f.fields_for @user.shipping_addresses.build do |sub_form| %> ... <%= sub_form.text_field :zip_code %> ... <% end %> <% end %> <%= f.submit %> <% end %>
Note that we could (and should) also have built the new shipping_address objects in the controller instead of the view; it’s just for demonstration purposes here.
#accepts_nested_attributes_for method is fairly straightforward and the docs should be helpful.
The reading will cover more about allowing the nested parameters.
You can also have your form destroy nested forms by first setting the
:allow_destroy option to
true for the
#accepts_nested_attributes_for method, e.g.
accepts_nested_attributes_for :shipping_addresses, :allow_destroy => true. Now, any time you want to destroy a ShippingAddress object from a User’s form, just include the key
_destroy => 1 in the submitted parameters for that ShippingAddress.
If you’ve got a
has_many :through relationship, you’ll likely need to go one additional step further by specifying that each side of your relationship is the inverse of the other. It’s detailed in this blog post from ThoughtBot.
Sometimes, despite all the nice helpers Rails gives you, you just want to do something that’s not standard. You should first wonder whether this is the easiest and most straightforward way to do things. If it passes the smell test, then go ahead and build your form.
It’s often easiest (and good practice while you’re learning) to start with the most basic of HTML forms. If you don’t understand what’s going on in the basic HTML (and remember to include your CSRF token), then you’ll be hopeless trying to use helpers. Once you’ve got a good handle on things, gradually bring in the Rails helpers like
Don’t get discouraged if you get some real head-scratcher moments when building nonstandard forms. It just takes some experience to feel comfortable. And if things are too out of hand, you may need to re-evaluate your approach (what exactly are you hoping to accomplish with your complex form?) and start again.
simple_form is a gem by Platformatec which can really make your life easier (if you aren’t doing anything too crazy). It provides lots of user-friendly features for building forms and is in wide use today.
It’s up to you to check out the documentation and start using it in your own applications as desired.
Sometimes, for a record that already exists, you want to either deselect a dropdown or check none of your checkboxes but you want this to indicate that the associated fields should actually be set to
nil. Usually, though, if you submit the form it will include none of the fields and your back end won’t know that you actually wanted to remove those fields so nothing will happen. How do you get around it?
Try making a hidden field in your form (or nested form) that has the same name as your checkboxes or dropdown but only contains the value
"". Now you’ll get that attribute to show up in your
params hash no matter what and you can handle deleting the records however you’d like appropriate.
Sometimes Rails helper methods will do it for you, but make sure you know what your form is actually submitting (if anything) if you deselect all options!
- Read the Rails Guide on Forms section 5, which covers populating a form with a collection of objects.
- Read the Same Rails Guide on Forms section 10, which covers accepting nested form data.
- Read the Same Rails Guide on Forms section 8, which covers the parameter conventions for nested forms.
- Read this blog post from Peter Rhoades on working with nested forms. The example covers a lot of the things we’ve gone over so far, so follow along. Also note how he does the allowing of nested attributes in Rails 4.
We’ve covered two of the more common use cases for complex forms – pre-populating a form with objects and creating multiple objects with a single form. At this point, even if you’re uncomfortable, you should have all the tools you need to work through creating a form. We’ll get your hands dirty in the project, have no fear.
The best part? This is more or less the most complicated conceptual stuff with learning Rails. Actually, it’s not even really Rails-specific… once you’re comfortable with the HTML that forms require and how the parameters get submitted to your controller, mapping that to the correct Rails conventions or helpers is the easy part. So everything you’ve learned may just be transferrable to every form you’ll ever make.
This section contains helpful links to other content. It isn’t required, so consider it supplemental.
- Simple Form Documentation on GitHub
- Another example of a nested form on SO
- Understanding Rails’ form authenticity tokens
- Why not to hardcode your application’s secret token in production
This section contains questions for you to check your understanding of this lesson. If you’re having trouble answering the questions below on your own, review the material above to find the answer.
- What does the
- When using
#options_for_select, what format does the array need to be in?
- When would you use the
- How can you prevent users from having to submit multiple forms?
- What do you add to the model that allows nested forms to create new objects?
- How do you allow the nested parameters in your controller?
- How can you set up a dropdown or checkbox to delete a record that already exists?