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".

20 kommenttia:

  1. It's a very useful functionality. How can I write the javascript part in coffee-script?

    VastaaPoista
  2. Thanks :-) Well about your question: I haven't used coffee-script that much, but there are some JavaScript -> CoffeeScript converters around that you can use for this purpose, for example: http://priest.meteor.com/ I just tested it and seems to work alright.

    VastaaPoista
  3. Is there a gem that does the "adding fields on the fly" part?

    VastaaPoista
  4. Very good question Enrique, I'm not sure :-) if I find one, I'll definitely provide the gem details for you.

    VastaaPoista
    Vastaukset
    1. AldaimSolutions as an offshore e-Commerce website development company provides a wide range of online business solutions in web and mobile space.Popular rails gemsRails is basically a web application framework, which is consist of everything needs to create database baked web application. It helps the developers to create websites and applications by providing structures for all codes written by them. Moreover, common repetitive tasks are simplified with the help of this technology.
      Websites made with PHP and Shopify
      Best Ecommerces Company Audacity24
      Websites made with PHP and Shopify
      Best ruby APIS
      React native and React Js
      Node Js and React Js

      Poista
  5. Ok, i think the "add field on the fly" is not precise sentence for target the action. I think it should be "create field ont he fly".
    Why ? because i'm the problem to just "add fields on the fly", and not create it. What ? Yes... imagine that in your table Adresses (so handle with model Adress in Rails MVC CoC design), then inside this table, there is allready some adress... so you no have to create them, but just add one you should choose first for add to the Person...
    This could be much much more complicate with Rails CoC way of see the world... then now, imagine that a Personn could have many adress, but also at an adress it could be possible to have many persons (that is a real fact happen for all famillies and no isolates people all around the world). then you have a nested relations like has_and_belongs_to many :adress
    Ok... so you have a "middle" table without model named: "Adresses_Persons" who just contain foreign keys for link Adress.id to Person.id... so from this real and current situation, how to dynamically ("on the fly") just for exemple open a dialog-box (jquery easy way) with maybe a rails template inside for show (not form now... we show something) Adresses list with a checkbox on each row for selection and submit this new selected list of allready existing adress to the current Person ? that is, for me and with Rails specifically: a big big pain.
    Then, you not "add things", you "create things"... with Rails, create is easy by form... add from show is not easy (or i miss something and i'm not alone in this situation).

    VastaaPoista
  6. @Jérôme Lanteri, thank you for your excellent comment & question. + please forgive me for giving such a late reply for your questions. Well, I’ll try to give some kind of answer, although it’s not a definitive answer, just a suggestion how things could work in certain situations. You mentioned: “open a dialog-box (jquery easy way) with maybe a rails template inside for show (not form now... we show something) Adresses list with a checkbox on each row for selection”. -I think this is very difficult to achieve unless the Person in question belongs to group called family, and at least one of the family members is already in the system database. Of course if there was some kind third party API service that could be used for listing potential addresses for Person, for example phone number that was entered before, - then that could be used to look up potential Address data. Anyway, I get your point which is in a sense ideal from a database design perspective, because if there are more than one identical address row in the database, then it leads to redundant data which doesn’t go hand in hand with database normalization and first normal form (https://en.wikipedia.org/wiki/First_normal_form). Of course now this isn’t the case as there’s person_id as a foreign in the db, but otherwise there could be duplicate rows in the Address table. Like you suggested, a middle table "Adresses_Persons" could work in this case (although I haven’t used it in the blog). However, if there are many Persons that have the same address (=one Address row in the database) and many "Adresses_Persons" rows, then what needs to be done if only one Person moves in that family to a new Address? -Then are we going to create one new entry to the Address table and update one "Adresses_Persons" row for that Person? -Alternative are we just going to update just one Address field that that is shared by all Persons in that family through "Adresses_Persons" table? After giving some thought to the issue, I don’t think it’s quite that straightforward, although I guess some kind of redundancy can be avoided. I think the Postcode is often used with only numerical foreign key in the Address table because some redundancy can be avoided that way. Well, I wish I could provide you with better answer :-) Also I tried to Google for some useful answers for this issue but I discovered that there are many different ways of doing this issue, so I guess there’s not just one right way of doing this but several :-)

    VastaaPoista
  7. Nice blog keep updating your blog and i am waiting for your next update also keep update the blogger.
    Ruby on Rails Online Course Hyderabad

    VastaaPoista
  8. Really Good blog post.provided a helpful information.I hope that you will post more updates like thisRuby on Rails Online Training Bangalore

    VastaaPoista
  9. Kirjoittaja on poistanut tämän kommentin.

    VastaaPoista
  10. AldaimSolutions as an offshore e-Commerce website development company provides a wide range of online business solutions in web and mobile space.Popular rails gemsRails is basically a web application framework, which is consist of everything needs to create database baked web application. It helps the developers to create websites and applications by providing structures for all codes written by them. Moreover, common repetitive tasks are simplified with the help of this technology.
    Websites made with PHP and Shopify
    Best Ecommerces Company Audacity24
    Websites made with PHP and Shopify
    Best ruby APIS
    React native and React Js
    Node Js and React Js

    VastaaPoista
  11. I am regular reader, how are you everybody?
    This post posted at this site is really good on ruby on rails online training

    VastaaPoista
  12. Kirjoittaja on poistanut tämän kommentin.

    VastaaPoista