Skip to content

Creating a nested model web form in Rails using Cocoon

September 29, 2015

I’ve recently been working on learning Ruby on Rails for the past few weeks and it’s been a lot of fun.  However, a few days ago I ran into a huge roadblock which was how to make a web form for a nested model.  After several days of rooting through StackOverflow and even searching page 2 of google, I found one tutorial which finally led to a great deal of success.  If you want to check out the original post head on over to https://hackhands.com/building-has_many-model-relationship-form-cocoon/ and check it out.  Below is a very similar guide which is pretty much just updated for Rails 4 and also includes the migration and controller code.

Now, the primary goal is to have a single form in which we can create data for multiple models which are nested within eachother.  For the purposes of this project we are going to be making a form that will manage user access on a set of projects.  We will have many projects and any number of users can work on a set of projects.  There will be three models we will create, Project, User, and the model which will connect those two will be Contributor.

So, first we will setup a new project and add the Gem we will need: https://github.com/nathanvda/cocoon.  Cocoon allows use to more easily create forms that handle nested models. Add:

gem 'cocoon'

to Gemfile in the root of your project. Now in terminal run:

bundle install

This will install cocoon into your rails project, but you will still have to modify one more file to finish this off.  Open application.js in your app/assets/javascripts directory and add the line:

//= require cocoon

I’ve added it right above the line that says require_tree.  Now lets make our models. in terminal run these three commands to build the 3 model files and 3 migration files we will need:

rails generate model Project title:string description:text
rails generate model Contributor access:string
rails generate model User name:string

Now, lets edit the migration files to link the tables properly in our database. First go to the directory db/migrate and open  create_contributors.rb.  We need to add the belongs_to relationship in this file which will link all of our tables together. Add:

t.belongs_to :project, index: true
t.belongs_to :user, index: true

That is the bare minimum we need to link these two. I know there is a way set this up with the terminal commands but I feel that editing the files manual may help some people with potential issues. Now in terminal, run the commands:

rake db:create
rake db:migrate

to setup your database using the migration files.

Next we need to edit the models.  Go to app/models and open project.rb. Here we need to add a few important lines of code. Add the lines:

has_many :contributors
has_many :users, :through => :contributors

accepts_nested_attributes_for :contributors, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :users

The first line allows many contributors to link to Project. The second line allows you to access users from projects.  The third and forth line has the model property accepts_nested_attributes_for which allows project to accept attributes for its related models.

Now, lets edit contributor.rb.  Add these three lines:

belongs_to :project
belongs_to :user

accepts_nested_attributes_for :user, :reject_if => :all_blank

The first two lines have the belongs_to model property.  This will allow Contributor to link to one project and one user.  Be sure to use the singular names for project and user.

The last thing we need to do to fix the models is to edit the user.rb file. This one should now be pretty straight forward.  It uses the has_many model property to link to both models but it goes through contributors to reach projects. Add these two lines:

has_many :contributors
has_many :projects, through: :contributors

Now we need to create a controller for Projects.  In terminal run:

rails generate controller Projects

Open projects_controller.rb in app/controllers. We will only be working on creating a new model, any other actions should be pretty simple to get working on your own.

def new
@project = Project.new
end

def create
@project = Project.create(project_params)
redirect_to @project
end

private
def project_params
params.require(:project).permit(:title, :description, contributors_attributes: [:access, user_attributes: [:name]])
end

The thing that you will want to focus on here is our project_params method.  Here, you will see that it takes a long set of values and the structure should look very similar to how our Project model would see all of its data.  You have the title, and description, but then you have contributors_attributes which is a hash that contains access and another hash called users_attributes that contains name. Note: the contributors_attributes and user_attributes names are important. I believe that cocoon will use these names to help link the attributes to their respective tables. Also, you should see that in the two names contributors is plural while user is singular. A Project has multiple contributors but a contributor only has one user.

So what you have is the project which contains a set of data, one part of data is a reference to contributors which has a set of data, and one part is the users.

Now the last thing we have to do is write the views.  Go to the directory app/views/projects and create a file named new.html.erb. Add this code, we’ll go over the interesting stuff.

<%= form_for @project do |f| %>
<p>
<%= f.label :title, "Project Title" %><br />
<%= f.text_field :title %>
</p>
<p>
<%= f.label :description, "A breif description of this project" %><br />
<%= f.text_area :description, rows: 5 %>
</p>

<fieldset id="project-users">
<ol>
<%= f.fields_for :contributors do |contributor| %>
<%= render 'contributor_fields', f: contributor %>
<% end %>
</ol>
<%= link_to_add_association 'add user', f, :contributors, 'data-association-insertion-node' => "#project-users ol", 'data-association-insertion-method' => "append", :wrap_object => Proc.new {|contributor| contributor.build_user; contributor } %>
</fieldset>

<%= f.submit %>
<% end %>

Alright, you should know the basics for rails now. We want to examine the code for the link_to_add_association tag.  ‘add user’ sets the text for a link that will allow our render tag above to appear.  Take a look at the part ‘#project-users ol’ that is related to the fieldset id value and the ol tag. If you change the html for this you’ll want to change that selector to match.

Now we need to make a partion that the render line of code will use. Add a file named _contributor_fields.html.erb and add the code:

<%= f.fields_for :user do |contributor_user| %>
<%= f.label :name, "Name:" %>
<%= contributor_user.text_field :name %>
<% end %>

<%= f.label :access, "access:" %>
<%= f.text_field :access %>

<%= link_to_remove_association "remove", f %><br />

All of this should be pretty self explanatory at this point.  Run your rails server at head to localhost:3000/projects/new and test out the form. You should be able to hit the add user button and have a new form popup. When you hit the submit button you’ll receive an error, this is only because we have not yet setup a show action or a view for it but everything should have worked. Feel free to add that or check the database through the dbconsole.  Go to terminal and type in these commands.

rails dbconsole
.headers on
.mode column
SELECT * FROM projects;
SELECT * FROM contributors;
SELECT * FROM users;

With the output you should see the data you entered and ids linking all data correctly.  Hopefully this helps some people out there.  With how useful nested models and forms are, I’m very surprised there isn’t an easier way to do this in rails yet.

Advertisements

From → Uncategorized

Leave a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: