perjantai 6. kesäkuuta 2014

Adding "Fields on the Fly" with Ruby on Rails Nested Forms

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
View Full Code
            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>
View Full Code
<%= 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:



Click on the image to enlarge

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.



Click on the image to enlarge

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:



Click on the image to enlarge


After two "Add address" button clicks and some user entered information we get the following view:



Click on the image to enlarge

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>
View Full Code

 <%= 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.



Click on the image to enlarge

Finally we can create People and their Addresses by "Adding Fields on the Fly".