Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Applied Rails: Variable Rows For Child Records

DZone's Guide to

Applied Rails: Variable Rows For Child Records

In this post, you'll learn how to easily manipulate Ruby on Rails, using both HTML and JavaScript, to make variable rows for child elements.

· Web Dev Zone
Free Resource

Add user login and MFA to your next project in minutes. Create a free Okta developer account, drop in one of our SDKs to your application and get back to building.

I got a requirement in my Rails application to give the user the ability to input a variable number of records with multiple fields. The fields were of text type as well as select combo boxes. These rows had to be saved to the database as child table rows.

As is my inclination, I sought out to code it in raw HTML/JavaScript. Of course, I did Google for readily available solutions, and found some, but ruled them out for different reasons. The first step was to write an HTML file to achieve the desired functionality.



The first column in the table has a checkbox, the second column displays the row number, and the remaining columns have other attributes. Below the table, there are two buttons, one to add a row and another to delete a checked row. Each invokes a JavaScript function when clicked.

The function to add a row, addScreenRow, takes the table id as an argument. It takes the current row count of the table and increments by one to display for the new record. It inserts a new row with a call to insertRow and adds child elements to the cells.

Similarly, the function to delete a row, deleteScreenRow, takes a table id as an argument. First, it gets the current row count of the table. It then loops through the rows, and if the checkbox is checked, it calls deleteRow on that row.

The following HTML file illustrates the code and usage of these JavaScript functions.

<html>
<head>
    <title> Dynamically add / delete rows in HTML page </title>

    <script language="javascript">
        var globalRowCount = 2;
        function addScreenRow(tableID)
        {
	        var table     = document.getElementById(tableID);
	        var rowCount  = table.rows.length;
	        var row       = table.insertRow(rowCount);
          
	        var cell1     = row.insertCell(0);
	        var element1  = document.createElement("input");
	        element1.type = "checkbox";
	        cell1.appendChild(element1);
          
	        var cell2       = row.insertCell(1);
	        cell2.innerHTML = globalRowCount; //rowCount + 1;
          
	        var cell3       = row.insertCell(2);
	        var element2    = document.createElement("input");
	        element2.type   = "text";
	        cell3.appendChild(element2);
            
	        globalRowCount++;
        
        }
        function delScreenRow(tableID)
        {
            try {
                var table    = document.getElementById(tableID);
                var rowCount = table.rows.length;
        
                for (var i=0; i<rowCount; i++) {
                    var row    = table.rows[i];
                    var chkbox = row.cells[0].childNodes[0];
                    if (null != chkbox && true == chkbox.checked) {
                        table.deleteRow(i);
                        rowCount--;
                        i--;
                    }
                }
            }
            catch(e) {
                alert(e);
            }
        }
      
    </script>
</head>

<body>
    <table id="dataTable" border="1">
        <tr>
            <td><input type="checkbox" name="chk"/></td>
            <td> 1 </td>
            <td> <input type="text" /> </td>
         </tr>
    </table>

    <input type="button" value="Add Row"    onclick="addScreenRow('dataTable')" />
    <input type="button" value="Delete Row" onclick="delScreenRow('dataTable')" />

</body>
</html>


We now tackle the latter part of the requirement. Our dynamic rows need to have combo boxes, which as defined in Wikipedia are: "a combination of a drop-down list or list box and a single-line editable textbox, allowing the user to either type a value directly or select a value from the list." To go with the spirit of my approach, I did not use library widgets.

I found a very nice solution on stackoverflow.com question 264640, in the answer given by Max. It is simplicity exemplified, where we get the combo box feature with a few lines of CSS and one line of JavaScript. The code is also available on fiddle.

Plugging this into my HTML was quite easy. In the JavaScript, instead of just a text element, we need to add a div with a text element as well as a select element into the cell. That's it.

<html>
<head>
    <title> Dynamically add / delete rows in HTML page </title>

    <style>
        .dropdown {
            position: relative;
            width: 200px;
        }
        .dropdown select
        {
            width: 100%;
        }
        .dropdown > * {
            box-sizing: border-box;
            height: 1.5em;
        }
        .dropdown select {
        }
        .dropdown input {
            position: absolute;
            width: calc(100% - 20px);
        }
    </style>
    
    <script language="javascript">
        var options = ["", 'Option 1', 'Option 2', 'Option 3']
        function updateSelectValue() {
            this.previousElementSibling.value = this.value;
            this.previousElementSibling.focus();
        }
       
      var globalRowCount = 2;
        function addScreenRow(tableID)
        {
            var table     = document.getElementById(tableID);
            var rowCount  = table.rows.length;
            var row       = table.insertRow(rowCount);
            
            var cell1     = row.insertCell(0);
            var element1  = document.createElement("input");
            element1.type = "checkbox";
            cell1.appendChild(element1);
            
            var cell2       = row.insertCell(1);
            cell2.innerHTML = globalRowCount; //rowCount + 1;
           
            var cell3       = row.insertCell(2);
            var element2    = document.createElement("input");
            element2.type   = "text";
            cell3.appendChild(element2);
           
            var cell4       = row.insertCell(3);
            var div         = document.createElement("div");
            div.classList.add("dropdown");
            
            var tf          = document.createElement("input");
            tf.classList.add("input");
            div.appendChild(tf);
            
            var sel = document.createElement("select");
            sel.classList.add("select");
           
            var opt;
            for (var i=0; i<options.length; i++) {
                opt      = document.createElement("option");
                opt.text = options[i];
                sel.appendChild(opt);
            }
            
            sel.addEventListener("change", updateSelectValue);
            div.appendChild(sel);
            
            cell4.appendChild(div);
            
            globalRowCount++;
        }
      
        function delScreenRow(tableID)
        {
            try {
                var table    = document.getElementById(tableID);
                var rowCount = table.rows.length;
               
                for (var i=0; i<rowCount; i++) {
                    var row    = table.rows[i];
                    var chkbox = row.cells[0].childNodes[0];
                    if (null != chkbox && true == chkbox.checked) {
                        table.deleteRow(i);
                        rowCount--;
                        i--;
                    }
                }
            }
            catch(e) {
                alert(e);
            }
        }
      
    </script>
</head>

<body>
    <table id="dataTable" border="1">
        <tr>
            <td><input type="checkbox" name="chk"/></td>
            <td> 1 </td>
            <td> <input type="text" /> </td>
            <td> 
                <div class="dropdown">
                    <input type="text" name="field" id="field" />
                    <select  onchange="this.previousElementSibling.value=this.value; this.previousElementSibling.focus()">
                        <option></option>
                        <option>Option 1</option>
                        <option>Option 2</option>
                        <option>Option 3</option>
                    </select>
                </div>
            </td>
        </tr>
    </table>

    <input type="button" value="Add Row"    onclick="addScreenRow('dataTable')" />
    <input type="button" value="Delete Row" onclick="delScreenRow('dataTable')" />

</body>
</html>


Now on to the main task of providing this feature in a Rails app. Let's consider an order entry screen. Our models are Order and LineItem.

An Order has many LineItems and a LineItem has item, quantity, and gift wrap type as attributes. The user enters an item as text, quantity as a number, and for the gift wrap type can either select one of the dropdown values provided or enter a string as its value. The one to many relationship between Order and LineItem is expressed with 'has_many and belongs_to' keywords in the model classes and with the foreign key in the database table.

Code for achieving this is straightforward Rails:

order.rb?

class Order < ActiveRecord::Base
    has_many :line_items
    accepts_nested_attributes_for :line_items, allow_destroy: true
end


line_item.rb?

class LineItem < ActiveRecord::Base
    belongs_to :order
end


migration file?

def change
  add_reference   :line_items, :order, index: true
  add_foreign_key :line_items, :orders
end


We make one key change from the basic HTML form that we saw above. The variables for each field have to be given the name of the database column. Rails will take care of doing the insert.

The child records are nested attributes in the view and are sent as hashes inside arrays of hashes. So the naming has to be done as shown below, inside backticks for JavaScript to evaluate the global row count variable?

element4.name = `order[line_items_attributes][${globalLineItemsRowCount-1}][quantity]`;


For the update operation, we will go with a different form, the ' _edit_form.' We render it as a partial in the edit view file. This form is same as, _form, with a couple of changes.

We use a variable to display the row number in the second column of the HTML table. We initialize its value to 1 and increment for each row. In the JavaScript part, we initialize the global row count to one more than the number of child records.

?

var globalLineItemsRowCount = <%= @order.line_items.length+1 %>;
<% counter = 1 %>
<%= f.fields_for :line_items do |l| %>
    <tr>
        <td><input type="checkbox" name="chk"/></td>
        <td> <%= counter %> 
        <% counter += 1 %>
.............


The next thing to take care of is the delete operations for the child rows. This occurs when the user deletes the rows from the edit screen. We do something extra for this in the update action.

For deleted rows, the form parameters for the child rows will have only one member which is the id of the child record. So we check if a particular attribute parameter is of size one and has an 'id' value. If yes, we delete the child record by calling destroy.?

if order_params[:line_items_attributes] != nil
   order_params[:line_items_attributes].each do |la|
      if la[1].length == 1 && la[1]['id'] != nil
          @order.line_items.destroy(la[1]['id'])
      end
   end
end


I have made a small project and put it on GitHub. It's called mahrasa, short for Mahboob Rails Sample. Check out the code from https://github.com/mh-github/mahrasa, to see more details that I could not accommodate in this article, and play around with it.

Launch your application faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
ruby on rails ,web dev ,html ,javascript

Published at DZone with permission of Mahboob Hussain, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}