Wednesday, 15 February 2017

Applied Rails : Variable Rows For Child Records

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 comboboxes. 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, found some but ruled them out for different reasons. First step was to write a HTML file to achieve the desired functionality.


The first column in the table has a check box, 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 add child elements to the cells.

Similarly, the function to delete a row, deleteScreenRow takes a table id as 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. We now tackle the latter part of the requirement. Our dynamic rows need to have comboboxes, which as given 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 combobox 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. 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. User enters 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, _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 a '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, play with it and let me know your feedback.

No comments:

Post a Comment