Wednesday, 31 March 2010

Rails - Day 15 - Rails Forms Part II

Rails  - Day 1 - The RubyGem Love Story
Rails -  Day 10 - Testing Rails Controllers
Rails -  Day 13 -ActiveRecord Relationships Part II
Rails - Day 15 - Rails Forms Part II
In the last post we introduced the rails form builder and showed how easy it is to render a simple form using a simple model object.

I want to go into much more detail with a pseudo real example because most of the posts about rails forms and ActiveRecord that I have come across illustrate exactly what I did in the last post and show how to use the form builder and a simple model object.  Nearly all these examples show a simple model with a one to one mapping on a single table.  This is not very real world.

In this post I want to show how to use a form builder with a slightly more complex model.

In the past few posts, we have been building up a simple application that will record the expenses a small business owner might incur. 

Below is the view that is rendered from the new action of the ExpensesControlller where a user will add a new expense:

In the last post we were able to create an Expense object that captured the Paid by Director, Expense date, External reference and Posting Description attributes.

We covered ActiveRecord relationships in this post and defined the relationships between the expense object and its child objects.

The Expense model is a complex object with two child objects defined by the the relationships below. 

We are now going to use the form builder to both set the and retrieve attributes of the child objects through an HTML form.

Our first step is to fill the Expense Type dropdown that you can see in the screenshot at the start of the post with any records that exist in the expense_types table.

The form builder contains a select method which works well for belongs_to relationships.  The first argument of the select method is the foreign key of the belongs_to relationship which in our case is expense_type_id.  

We could create a select method similar to the following which has the ActiveRecord call ExpenseType.all to the database defined in the view and I have seen this done quite a lot:

We could do that but this feels dirty and ugly.  Such a call to the database has no place in our view and instead we add the following code to the new action of the ExpensesController:

The collect method of the array object will iterate over the array and provide a block that allows us to return a new array with different elements.  I think I am right in saying that the map and collect methods are the same.  A new array is created for each element in the original array that contains the id and name needed to render the option elements of the dropdown.

We then update our select method to use the instance variable @expense_types instead of the ActiveRecord call:

This makes me feel much better.  The ExpenseType.all call is now in the controller which of course makes it a lot more testable and tidy than adding it directly to the form builder select method.  We can also mock the call to ExpenseType.all which I prefer for testing purposes over direct database calls.

The select method will take care of the Expense belongs_to ExpenseType relationship.  

There is also a has_one relationship that defines the association between Expense and ExpensePayment as we discussed in this post.  

The ExpensePayment model object will take care of this part of the UI:

We also stated in this post that the VAT and Gross attributes are actually derived fields from the Net attribute.

The calculation for VAT is ((Net / 100) * 17.5) 
The calculation for Gross is (Net + VAT).

So the only attribute we are concerned with recording from the user is the Net attribute.  We should use JQuery and Ajax to update the derived fields Vat and Gross as the user inputs their values.

Nested Attributes

Nested attributes allow you to save attributes on associated records through the parent which is exactly the behaviour we want for our Expense has_one ExpensePayment relationship.  By default nested attribute updating is turned off but you can enable it using the accepts_nested_attributes_for class method.  When you enable nested attributes an attribute writer (property set in .NET speak) is defined on the model.  The attribute writer is named after the association, which means that in our example, a new method will be dynamically added to the Expense model:expense_expense_payment=

Enabling nested attributes on a one to one association allows us to create both the Expense and Expense_payment in one go from a params hash that would be passed in from a rails form builder generated form.

In order to test this, we create the following failing test:

You can see from the above that the hash contains a child hash named :expense_payment_attributes that will represent the child ExpensePayment model object.  To make the above test pass, we update the Expense model to the following:

The changes have incorporated the following:

  1. To make the expense_payment attributes available through nested forms we need to add the :expense_payment_attributes convention to the attr_accessible method.  We mentioned  attr_accessible in the last pos tand this method restricts what methods can be accessed via a web form.  This is often overlooked and is a potential security hole.  Shame on you if your models do not include this.
  2. We define the relationship as nested by using the accepts_nested_attributes_for method.
  3. We have also updated the has_one statement to has_one :expense_payment, :dependent => :destroy which tells ActiveRecord to cascade the delete of the child object when the Expense is deleted.

Nested Model Form

In the view that will render the new action, we simply use the fields_for method to expose the fields of the nested model:

It is worth mentioning that we are using the text_field_tag method instead of the text_field method for the readonly attributes vat and gross.

This will generate the following HTML:

You can see that the p.text_field :net expression has generated an input text element with a name attribute of expense[expense_payment][net] which models the Expense has_one ExpenseType relationship and is what rails uses to build the params hash of the relationship.

If we inspect the params hash that is passed to the create action of the ExpenseController by adding the code raise params.to_yaml to the action, we get the following output:

You can see that the expense_payment_attributes hash is nested within the parent expense hash.  This means that we can create an expense object from the form fields in our create action:

We are going to mention validation in the next post but if we now fill out the required fields and submit the form, the create action completes successfully.

In order to check that the ExpensePayment has made it into the database, we can spark up the rails console with the command ruby script/console and enter the expression ExpressionPayment.all.

We can see from the above that our relationship is successfully created.

I wanted to write this post because in my opinion, a lot of the posts on rails forms do not go into this level of detail for defining relationships.

I personally do not like having to add the accepts_nested_attributes_for method and would like the framework pick this up automatically.  If anyone who knows rails better than me can suggest a better way than this or maybe argue that this way is a good way then please leave a comment.

In the next post, I am going to touch on how to validate your rails model.

No comments:

Post a Comment