In this blog, I'll show how to create complex forms in Rails 4,
where one can "add fields on the fly" by using JavaScript.
This issue on how to "add fields on the fly" inspired me
when I was learning how to create complex forms by using Ruby on Rails.
While learning, I went through the Rails Guide which covers
complex forms
creation in detailed way, apart from
showing how to
"add fields on the fly".
"Adding fields on the fly" is not covered fully,
as it's not directly supported by Rails, because
it involves the usage of JavaScript. Well, I was
intrigued by this issue, and I wanted to know how
to mix JavaScript with complex forms creation.
Following Peter Rhoades'
excellent article's steps, I decided to approach
this issue from the very start, step-by-step: First, I'll create the rails project,
and then create the Person and Address models used in this sample.
After that I'll update _form
partial
that is used for Person and Address creation.
Finally, some JavaScript is added to the project to allow "adding fields on the fly", which
also makes a more interactive look to the page.
About the models, the Person model can have many Addresses, and the Address model belongs to one Person model. By the way, those models are identical to the ones that are presented in the Rails Guide; only the JavaScript part is new. If you want to see JavaScript right away, you can advance directly to the second section of this blog. The source code for this blog is available at GitHub.
Preparation
Okay, lets start by creating a new rails project on command line:
rails new nested_forms
Then install the gems in the project bundle:
cd nested_forms
bundle install
Then generate scaffold for Person, and generate model for Address:
rails generate scaffold Person name
rails generate model Address kind street person_id:integer
Finally migrate those new models into database
bundle exec rake db:migrate
Then update your Person model by adding association:
has_many :addresses
.
Add also line accepts_nested_attributes_for :addresses
to allow Address creation in the same controller where the Person is created.
Ruby on Rails API says:
"Nested attributes allow you to save attributes on associated records through the parent."
And like mentioned in the Rails Guide:
"This creates an addresses_attributes=
method on Person that allows you to create,
update and (optionally) destroy addresses."
# file: app/models/person.rb
class Person < ActiveRecord::Base
has_many :addresses
accepts_nested_attributes_for :addresses
end
Next add belongs_to :person
association to
the generated Address model:
# file: app/models/address.rb
class Address < ActiveRecord::Base
belongs_to :person
end
Then update People controller's strong parameters in method person_params
.
This allows Address creation at the same time when Person is created.
Also update the new
action to so
that it builds two Addresses for new Person.
# file: app/controllers/people.rb
class PeopleController < ApplicationController
def new
@person = Person.new
2.times { @person.addresses.build }
end
....some code left out....
def person_params
params.require(:person).permit(:name,
:addresses_attributes => [:id, :kind, :street])
end
end
class PeopleController < ApplicationController before_action :set_person, only: [:show, :edit, :update, :destroy] # GET /people # GET /people.json def index @people = Person.all end # GET /people/1 # GET /people/1.json def show @addresses = @person.addresses end # GET /people/new def new @person = Person.new 2.times { @person.addresses.build} end # GET /people/1/edit def edit end # POST /people # POST /people.json def create @person = Person.new(person_params) respond_to do |format| if @person.save format.html { redirect_to @person, notice: 'Person was successfully created.' } format.json { render action: 'show', status: :created, location: @person } else format.html { render action: 'new' } format.json { render json: @person.errors, status: :unprocessable_entity } end end end # PATCH/PUT /people/1 # PATCH/PUT /people/1.json def update respond_to do |format| if @person.update(person_params) format.html { redirect_to @person, notice: 'Person was successfully updated.' } format.json { head :no_content } else format.html { render action: 'edit' } format.json { render json: @person.errors, status: :unprocessable_entity } end end end # DELETE /people/1 # DELETE /people/1.json def destroy @person.destroy respond_to do |format| format.html { redirect_to people_url } format.json { head :no_content } end end private # Use callbacks to share common setup or constraints between actions. def set_person @person = Person.find(params[:id]) end # Never trust parameters from the scary internet, only allow the white list through. def person_params params.require(:person).permit(:name, :addresses_attributes => [:id, :kind, :street]) end end
Finally add fields_for
, label
and text_field
form helpers and some text for adding Address to the _form
partial:
# file: app/views/people/_form.html.erb
Addresses:
<ul>
<%= f.fields_for :addresses do |addresses_form| %>
<li>
<%= addresses_form.label :kind %>
<%= addresses_form.text_field :kind %>
<%= addresses_form.label :street %>
<%= addresses_form.text_field :street %>
</li>
<% end %>
</ul>
<div class="actions">
<%= f.submit "Create Person and Addresses" %>
</div>
<%= form_for(@person) do |f| %> <% if @person.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@person.errors.count, "error") %> prohibited this person from being saved: </h2> <ul> <% @person.errors.full_messages.each do |msg| %> <li><%= msg %> </li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :name %>
<%= f.text_field :name %> </div> Addresses: <ul> <%= f.fields_for :addresses do |addresses_form| %> <li> <%= addresses_form.label :kind %> <%= addresses_form.text_field :kind %> <%= addresses_form.label :street %> <%= addresses_form.text_field :street %> </li> <% end %> </ul> <div class="actions"> <%= f.submit "Create Person and Addresses" %> </div> <% end %>
Now if you want to create a new Person, then you can also create two Addresses at the same time:
If you look at the generated source code below and the created Address elements, you can see that the hash key that is used for identifying an Address is an integer, and starts from 0; the second Address is identified with a hash key of 1. I have used Chrome's Developer Tools for viewing those elements below. No user entered data is shown in this view, because the form hasn't been submitted yet.
Now if you submit the data, you have created two Addresses for Person, who in this case is Bart. Whenever you want to create a Person, then those two Address fields are shown each time. Therefore, the people/new page is a bit static at the moment, but let's change that by allowing "Adding Fields on the Fly" functionality.
"Adding Fields on the Fly"
As shown previously the key value for the Address hash is created incrementally
by using integer numbers such as 0 and 1. This means that the addresses that are created
are unique. In our sample we aren't going to increment integers for hash keys
(although I think it could work);
instead let's follow the Rails Guide:
"When generating new sets of fields you must ensure the key of the associated array is unique
- the current JavaScript date (milliseconds after the epoch) is a common choice."
So let's use JavaScript
Date
object in place of added integer values.
Now we are going to make JavaScript code that creates the Address
elements shown before and uses JavaScript Date
object in place of 0 and 1 values for
keys. Also those elements can only be displayed and created on
the form after clicking an "Add address" button like shown below:
After two "Add address" button clicks and some user entered information we get the following view:
To get the functionality that was shown in the two screenshots above,
and to make JavaScript changes that were mentioned earlier,
we need to add some JavaScript to our project.
Let's add a JavaScript function called addAddressField
to app/assets/javascripts folder in a file called people.js
.
Main parts of the function code is highlighted below. You
can also view the full code by clicking the link View Full Code.
# file: app/assets/javascripts/people.js
function addAddressField() {
//create Date object
var date = new Date();
//get number of milliseconds since midnight Jan 1, 1970
//and use it for address key
var mSec = date.getTime();
//Replace 0 with milliseconds
idAttributKind =
"person_addresses_attributes_0_kind".replace("0", mSec);
nameAttributKind =
"person[addresses_attributes][0][kind]".replace("0", mSec);
//create <li> tag
var li = document.createElement("li");
....some code left out....
//create input for Kind, set it's type, id and name attribute,
//and append it to <li> element
var inputKind = document.createElement("INPUT");
inputKind.setAttribute("type", "text");
inputKind.setAttribute("id", idAttributKind);
inputKind.setAttribute("name", nameAttributKind);
li.appendChild(inputKind);
....some code left out....
//add created <li> element with its child elements
//(label and input) to myList (<ul>) element
document.getElementById("myList").appendChild(li);
//show address header
$("#addressHeader").show();
}
View Full Code
function addAddressField() { //create Date object var date = new Date(); //get number of milliseconds since midnight Jan 1, 1970 //and use it for address key var mSec = date.getTime(); //Replace 0 with milliseconds idAttributKind = "person_addresses_attributes_0_kind".replace("0", mSec); nameAttributKind = "person[addresses_attributes][0][kind]".replace("0", mSec); idAttributStreet = "person_addresses_attributes_0_street".replace("0", mSec); nameAttributStreet = "person[addresses_attributes][0][street]".replace("0", mSec); //create <li> tag var li = document.createElement("li"); //create label for Kind, set it's for attribute, //and append it to <li> element var labelKind = document.createElement("label"); labelKind.setAttribute("for", idAttributKind); var kindLabelText = document.createTextNode("Kind"); labelKind.appendChild(kindLabelText); li.appendChild(labelKind); //create input for Kind, set it's type, id and name attribute, //and append it to <li> element var inputKind = document.createElement("INPUT"); inputKind.setAttribute("type", "text"); inputKind.setAttribute("id", idAttributKind); inputKind.setAttribute("name", nameAttributKind); li.appendChild(inputKind); //create label for Street, set it's for attribute, //and append it to <li> element var labelStreet = document.createElement("label"); labelStreet.setAttribute("for", idAttributStreet); var streetLabelText = document.createTextNode("Street"); labelStreet.appendChild(streetLabelText); li.appendChild(labelStreet); //create input for Street, set it's type, id and name attribute, //and append it to <li> element var inputStreet = document.createElement("INPUT"); inputStreet.setAttribute("type", "text"); inputStreet.setAttribute("id", idAttributStreet); inputStreet.setAttribute("name", nameAttributStreet); li.appendChild(inputStreet); //add created <li> element with its child elements //(label and input) to myList (<ul>) element document.getElementById("myList").appendChild(li); //show address header $("#addressHeader").show(); }
The above JavaScript creates new <li> element, two labels, and two input elements for Address entries, which are then appended on to <ul> element of the form. Also an address header is made visible by using jQuery.
For id
attribute values you don't have
to use that long:
person_addresses_attributes_sth_kind
format;
it can be something more shorter, for instance a plain integer. However,
the format of the name
attribute value has to look like to original one:
person[addresses_attributes][sth][kind]
; otherwise the data won't be
saved in the controller. Like said in the
Ruby Rails Tutorial book,
by Michael Hartl: "These name values allow Rails
to construct an initialization hash (via the params variable)
for creating users using the values entered by the user."
In our case Person and Addresses are being created based on the values
entered by the user.
Finally, let's make some changes
to the _form
partial,
so that it uses addAddressField
function.
Now we can remove the fields_for
helper and the code
within fields_for
helper method,
and add submit_tag
and Address header to get the functionality we need.
The changes are small and are highlighted below.
# file: app/views/people/_form.html.erb
<% submit_tag "Add address", :type => "button", :id => "addAddress", :onclick => 'addAddressField()'%>
<br />
<div id="addressHeader" style="display:none">
Address information:
</div>
<br />
<ul id="myList">
</ul>
<div class="actions">
<%= f.submit "Create Person and Addresses" %>
</div>
<%= form_for(@person) do |f| %> <% if @person.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@person.errors.count, "error") %> prohibited this person from being saved: </h2> <ul> <% @person.errors.full_messages.each do |msg| %> <li><%= msg %> </li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :name %>
<%= f.text_field :name %> </div> <%= submit_tag "Add address", :type => 'button', :id => 'addAddress', :onclick => 'addAddressField()' %> <br> <div id="addressHeader" style="display:none">Address information: </div> <br> <ul id="myList"> </ul> <div class="actions"> <%= f.submit "Create Person and Addresses" %> <div> <% end %>
Now if you look at the generated source code again and the created Address elements, you can
see that the hash key that is used for identifying an Address
are the milliseconds from the JavaScript Date
object.
Finally we can create People and their Addresses by "Adding Fields on the Fly".