Reputation: 211
I'm trying to build a small expense tracking app. Using the nested_form gem to add line items. There is an Expense model which accepts nested attributes. Items belong to expenses.
class Expense < ActiveRecord::Base
belongs_to :organization
belongs_to :department
has_many :expense_types
has_many :items
accepts_nested_attributes_for :items
end
The items model:
class Item < ActiveRecord::Base
belongs_to :expense
end
The controller create action action looks like:
class ExpensesController < ApplicationController
def new
@expense = Expense.new
end
def create
@expense = Expense.new(expense_params)
if @expense.save
flash[:notice] = "Expense Report Submitted"
redirect_to @expense
else
render 'new'
end
end
private
def expense_params
params.require(:expense).permit(:department_id, :expense_type_id, :notes, items_attributes: [:id, :description, :amount, :issue_date, :_destroy])
end
end
The new expense form looks like:
<%= nested_form_for (@expense) do |f| %>
<% if @expense.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@expense.errors.count, "error") %> prohibited
this expense from being saved:</h2>
<ul>
<% @expense.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class"row">
<div class="col-md-8">
<div class="form-group">
<%= f.label :department_id %><br>
<%= f.collection_select(:department_id, Department.all, :id, :department_name, prompt: true, class: "dropdown-menu") %>
</div>
<div class="form-group">
<%= f.label :expense_type_id %><br>
<%= f.collection_select(:expense_type_id, ExpenseType.all, :id, :expense_name, prompt: true, class: "form-control") %>
</div>
<%= f.fields_for :items do |i| %>
<div class="form-group">
<%= i.label :description%>
<%= i.text_field :description, class: "form-control" %>
</div>
<div class="form-group">
<%= i.label :amount%>
<%= i.text_field :amount, class: "form-control" %>
</div>
<div class="form-group">
<%= i.label :issue_date%>
<%= i.date_select :issue_date, class: "form-control" %>
</div>
<%= i.link_to_remove "Remove", class: "btn btn-default" %>
<% end %>
<div><p><%= f.link_to_add "Add Expense", :items, class: "btn btn-default" %></p></div>
<div class="form-group">
<%= f.label :notes %>
<%= f.text_area :notes, class: "form-control" %>
</div>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
I was able to save expenses before adding the nested attributes. After doing that, whenever I hit the submit button, I get the ActiveRecord::UnknownAttributeError in ExpensesController#create error. It's weird to see unknown attribute: expense_id. Did I miss something here?
Upvotes: 2
Views: 5018
Reputation: 76784
Nested Form
To the best of my knowledge, the nested_form
gem basically just creates a fields_for
instance for your form. If this is the case (it more or less has to be), then you've got several issues you need to consider
By the way, looking at the nested_form
documentation, it seems it was primarily designed for Rails 3. When it mentioned about attr_accessible
, it now means to focus on strong_params
(if you're using Rails 4 of course)
--
Build
As mentioned by Mandeep
, you need to ensure you're building the correct ActiveRecord objects for your form. form_for
works by taking an ActiveRecord object & populating the various attributes etc with its own attributes.
This is how Rails creates the illusion of persistence with this functionality -- it takes the ActiveRecord & populates the same data recursively
In order to get fields_for
to work (which is the basis of nested_attributes
, you need to build the associative ActiveRecord object in the action which renders the form_for
(in your case new
):
#app/controllers/expenses_controller.rb
Class ExpensesController < ApplicationController
def new
@expense = Expense.new
@expense.items.build #-> required for fields_for to work
end
end
Remember, the nested_form
gem is not magic - it is essentially just a javascript plugin to replicate the fields_for
elements rendered in your form already, and then append them to the DOM.
It essentially uses the child_index: Time.now.to_i
"trick" to surmount the incremental id
isue
--
Attributes
Secondly, you need to appreciate the error you're receiving
expense_id
could be an attribute in the items_attributes
objects. I've not seen this with pure Rails, but perhaps the nested_form
gem appends a particular attribute to the objects or something
Either way, I believe the problem will be how to associate your items
objects with your parent Expense
object. To do this, I would do the following:
- Check your params hash (see where
expense_id
is being passed)- Update your
strong_params
to allow theexpense_id
I KNOW THE PROBLEM
DATABASE - you likely don't have the
expense_id
column in theitems
table
Fix
You need to create a migration to put the expense_id
foreign_key into your items
table
To do this, you should open your CMD and perform the following:
$ rails generate migration AddExpenseIDToItems
Then you can change the migration to have the following line:
add_column :expense_id, :items
Then you just need to do:
$ rake db:migrate
This should resolve your issue
Upvotes: 3
Reputation: 9173
Your controllers new action should be like this
def new
@expense = Expense.new
@item = @expense.items.build
end
Also if you are using rails 4 then you don't need nested_form_for gem. Checkout nested forms
. In your form you can simply use
<%= form_for @expense do |f| %>
// expense fields
<%= f.fields_for @item do |e| %>
// item fields
<% end %>
<%= f.submit %>
<% end %>
Upvotes: 2