I'm making a recipe-manager (who isn't as their first app?) in Rails, and here is my layout:
Ingredient belongs to Recipe
Recipe has many Ingredients
What is the best way to make a form that reflects this relationship? I was thinking an input that, when one is filled, creates another, so there is always 'one more' at the end of the form for ingredients.
Once I have the UI made, what would the structure of the model and controller look like? Right now I have the scaffolded controller create method:
def create
#recipe = Recipe.new(params[:recipe])
respond_to do |format|
if #recipe.save
format.html { redirect_to(recipes_path, :notice => 'You made a new recipe!') }
format.xml { render :xml => #recipe, :status => :created, :location => #recipe }
else
format.html { render :action => "new" }
format.xml { render :xml => #recipe.errors, :status => :unprocessable_entity }
end
end
end
Should the params[:recipe] just be a more deeply nested object/hash/dictionary, that contains an array of ingredients or something?
Thanks for any guidance here.
You should use accepts_nested_attributes here.
Some links:
API:
http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods/accepts_nested_attributes_for
Screencasts:
http://railscasts.com/episodes/196-nested-model-form-part-1
http://railscasts.com/episodes/197-nested-model-form-part-2
So your model will look like this
class Recipie < ActiveRecord::Base
has_many :ingredients
accepts_nested_attributes_for :ingridients, :allow_destroy => true
end
Views:
<%= form_for #recipe do |f| %>
... # reciepe fields
<%= f.fields_for :ingridients do |i| %>
... # your ingridients forms
<% end %>
...
<% end %>
And controller
def create
#recipe = Recipe.new(params[:recipe])
#recipe.save # some save processing
end
Just add ingredients by comma delimited.
This can be a text_field_tag because you will need to parse it and save each word spaced by a comma with a before save.
class Recipie < ActiveRecord::Base
has_many :ingredients
before_save :add_ingredients
attr_accessor :ingredients_to_parse #this will be the text_field_tag
def add_ingredients
#create an array of ingredients from #ingredients_to_parse
#then loop through that array i.e. you have your ingredients_array
ingredients_array.each do
Ingredient.create(:recipe => self, :other_params => 'stuff')
end
#there are a lot of ways, I just used create to show you how to add it
end
end
So then in your form just have that text_field_tag
<%= form_for(#recipe) do |f| %>
<% f.text_field :name %>
<% text_field_tag :ingredients_to_parse %>
<%= f.submit %>
<% end %>
Then you can add Javascript so that each time a comma is added in that text_field_tag you can just use some js to to so fancy stuff.
This way it will work when servers are slow, js is not working well, etc. It's always a good idea to get the HTML version going first too.
Good luck, let me know if you have questions/problems.
Related
How do I save multiple records with nested attributes using single text_area? Each line in the text box or separated by a comma should be a separate record.
How would the controller look?
_form.html.erb
<%= simple_form_for #project do |f| %>
<%= f.simple_fields_for :products do |g| %>
<%= render 'product_fields', :f => g %>
<% end%>
<%= link_to_add_association 'add item', f, :products %>
<% end %>
_product_fields.html.erb
<%= f.text_field :category, placeholder: "Category" %>
<%= f.text_area :item, placeholder: "List your products (separated by each line or comma)" %>
project_controller.rb
def create
#project = Project.new(project_params)
respond_to do |format|
format.js
if #project.save
format.html { redirect_to #project, notice: 'Project was successfully created.' }
format.json { render :show, status: :created, location: #project }
else
format.html { render :new }
format.json { render json: #project.errors, status: :unprocessable_entity }
end
end
end
def project_params
params.require(:project).permit(
:user_id,
products_attributes: [:id, :item, :hyperlink, :_destroy, :category]).merge(user_id: current_user.id)
end
I would like to go into my project form, and then there's a large text_area where I can add a list of products, and each product (separated by "enter" or a "comma") will be a record.
EDIT ----
Adding Models:
class Project < ActiveRecord::Base
has_many :products, dependent: :destroy
accepts_nested_attributes_for :products, :reject_if => :all_blank, allow_destroy: true
end
class Product < ActiveRecord::Base
belongs_to :project
end
Normally you save your params without much manipulation. If you want to turn your text_area param into multiple records just chop it up and process it in the controller.
Let's say you use a new line to delineate products so your text area looks like:
product1
product2
product3
project_params[:product_list] = "product1\nproduct2\nproduct3"
prod_arr = project_params[:product_list].split("\n")
prod_arr.each do |product|
#you now have your product name in the local variable product
#you can now save each one separately. You will probably
#need to take common items of the params hash and insert the
#current product, then save.
end
You can choose to split on just about any character. But pick one that makes sense and then also apply some sort of checking on the string you are splitting. Notice the split("\n") is using double quotes, that is needed to tell Ruby that you are talking about the newline character. If you used split('\n') it won't work.
I would also look at wrapping it in a transaction if you want to make sure they all save.
I'm a beginner in RoR and am having issues on working with some of my models.
Basically I have a habtm relation between a product-ticket-reservation.
A product habtm reservations through tickets and vice-versa.
I also have a Supplier, which has_many :products and has_many :reservations.
What I want to do is after the user selects a supplier and sees it's products, he may then select the products he wants from that supplier.
In that reservations.new I got a form but since after the "submit" action I have to insert data in 2 models, I'm having issues with it.
When I create a reservation, it is supposed to create a reservation entry and a ticket entry at the same time, the ticket entry will have the reservation_id and the product_id as foreign keys.
My Reservations' view:
<%= form_for(#reservation) do |f| %>
Reservation Info
<div id="reservation_top"></div>
<div id="reservation">
<%= f.label :name %><br />
<%= f.text_field :name %>
<%= f.label :surname %><br />
<%= f.text_field :surname %>
(...)
<%= f.hidden_field :supplier_id, :value => #reservation.supplier_id %> #to get the supplier ID
Products:
<%= f.fields_for :tickets do |t| %>
<%= t.select("product_id",options_from_collection_for_select(#products, :id, :name))%>
#I also have another t.select and although this isn't my primary concern, I wanted this t.select option's to change according to what is selected on the previous t.select("product_id"). Something like a postback. How is it done in RoR? I've searched and only found observe_field, but I didn't understand it very much, can you point me in the right direction? thanks
<%end%>
<%= f.label :comments %>
<%= f.text_area :comments %>
<%= f.submit%>
<%end%>
Now i think the problem is in my controller, but I can't understand what to put there, I currently have:
def new
#supplier=Supplier.find(params[:supplier_id])
#reservation = Reservation.new(:supplier_id => params[:supplier_id])
#ticket = Ticket.new(:reservation_id => params[#reservation.id])
#products = Supplier.find(params[:supplier_id]).products
#ticket = #reservation.tickets.build
respond_to do |format|
format.html
format.json { render :json => #reservation }
end
end
def create
#reservation = Reservation.new(params[:reservation])
respond_to do |format|
if #reservation.save
#reservation.tickets << #ticket
format.html { redirect_to #reservation, :notice => 'Reservation Successful' }
else
format.html { render :action => "new" }
format.json { render :json => #reservation.errors, :status => :unprocessable_entity }
end
end
I'm now getting a
Called id for nil, which would mistakenly be 4
Is it because it is trying to create a ticket and it doesn't have the reservation_id?
I've never handled habtm associations before. Any tips?
Thanks in advance,
Regards
Take a look at the POST params for your create action in your log. That will show you exactly what data you have to work with from params when it comes time to save your data.
In
def create
#reservation = Reservation.new(params[:reservation])
respond_to do |format|
if #reservation.save
#reservation.tickets << #ticket
what is #ticket at that point? (There's your nil I believe)
I think it might also be interesting to see what your #reservation and #ticket look like in your new method right before generating the response... log a .inspect of each of those objects to make sure you have what you think you have.
And in a more complicated save like you have, I'd wrap it all in a transaction.
I have a model "Issue" and a nested Model "Relationship"
In the issue.rb I have mentioned:
has_many :relationships, :dependent => :destroy
accepts_nested_attributes_for :relationships, :allow_destroy => true
In relationship.rb I have mentioned:
belongs_to :issue
Following Ryan Bates Railcast#196 I have the following in my issues_controller:
relationship = #issue.relationships.build
However, I am encountering an error "unknown attribute: relationship"
Am I doing something incorrectly here? I do see the Relationships Attributes being passed to the server in the log however, this error does not let the create to be successful.
My expertise with rails is beginners level so kindly excuse me if I am asking a question which maybe deemed trivial.
Thanks for the help.
EDIT: The relevant Controller code:
#relationship = #issue.relationships.build
##relationship = Relationship.new(params[:relationship])
if #relationship.issue_id = ''
#relationship.issue_id = #issueid
end
if #relationship.cause_id = ''
#relationship.cause_id = #issueid
end
#relationship.save
redirect_to(:back, :notice => 'New Relationship was created')
What I see on the trace:
ActiveRecord::UnknownAttributeError in IssuesController#create
unknown attribute: relationship
Among the Issue parameters, I see the Relationship params being passed as expected:
"relationship"=>{"issue_id"=>"100",
"cause_id"=>""}
ANOTHER UPDATE
Posting the form_for code:
- form_for Issue.new do |f|
.field
= f.text_field :description, :class=>"formfield", :id=>"frm_descr"
.field
= f.hidden_field :wiki_url, :class=>"formfield", :id=>"frm_wiki_url"
.field
= f.hidden_field :short_url, :class=>"formfield", :id=>"frm_img_url"
.field
= f.hidden_field :title, :class=>"formfield", :id=>"frm_title"
= f.fields_for :relationship do |builder|
= builder.text_field :issue_id, :class=>"form_field", :id=>"frm_rel_issue_id", :value=>#issue.id
= builder.text_field :cause_id, :class=>"form_field", :id=>"frm_rel_cause_id"
.actions
= f.submit 'Create', :class=>"save_button", :name=>"save_issue_rel_button", :id=>"val_collector"
Change this line
= f.fields_for :relationship do |builder|
to this:
= f.fields_for :relationships do |builder|
Your issue has_many relationships - plural. That will give you the correct relationships_attributes parameters.
Here is the working skeleton code:
I created a new project and tried the combination of the other answers, and finally made it to work.
Here is my solution, after that are the things to watch out for. I am using different models so bear with me:
My models are: discussion has_many posts.
Discussion has no attributes.
Posts has content:text and discussion_id:integer.
Working Code
(model) discussion.rb
has_many :posts
accepts_nested_attributes_for :posts
(model) post.rb
belongs_to :discussion
routes.rb
resources :discussions do
resources :posts
end
(discussion view) _form.html.erb
<%= form_for(#discussion) do |f| %>
<%= f.fields_for :posts, #post do |p| %>
<%= p.text_area :content %>
<% end %>
<%= f.submit %>
<% end %>
(controller) discussions_controller.rb
def new
#discussion = Discussion.new
#post = #discussion.posts.build
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #discussion }
end
end
def create
#discussion = Discussion.new(params[:discussion])
respond_to do |format|
if #discussion.save
format.html { redirect_to(#discussion, :notice => 'Discussion was successfully created.') }
format.xml { render :xml => #discussion, :status => :created, :location => #discussion }
else
format.html { render :action => "new" }
format.xml { render :xml => #discussion.errors, :status => :unprocessable_entity }
end
end
end
Possible things that can go wrong
First, Thilo was right, I get unknown attribute: post if I do
# WRONG!
f.fields_for :post
Second, I have to have the #post instance variable in new action otherwise the post.context textarea will not show up.
# REQUIRED!
#post = #discussion.posts.build
Third, If I use the f.fields_for #post, the create action will complain unknown attribute: post too.
# WRONG!
f.fields_for #post do |p|
Use this instead:
# RIGHT!
f.fields_for :posts, #post do |p|
The End
So yeah, I wish we get to see more documentations on this (can't see any useful ones). For example I see some use of form_for [#discussion, #post] but I can never get it to work.
By using accepts_nested_attributes, you have created a setter method relationship_attributes=.
There are a couple of things I noticed that need to change.
You don't need to set
#relationship = #issue.relationships.build
Your form should be the following (you have f.fields_for :relationship)
= form_for #issue do |f|
# your issue fields here
= f.fields_for :relationships do |r|
# your relationship fields here
The beauty here is that you won't have to set any ids or anything.
I assume you are constructing the relationship in your controller and then trying to use it in the view. In order for this to be visible, you must make it an instance variable. All you need to do is throw an # symbol in from of the name of relationship, as you have done with #issue.
#relationship = #issue.relationships.build
Edit: due to further information provided by the OP after the original question was asked, this answer is now clearly not applicable.
this is my form code:
<%= simple_form_for setup_video(#video) do |f| %>
<% f.fields_for :comment_titles do |t| %>
<%= t.input :title, :label => "Comment Title:" %>
<%= t.button :submit, :value => 'Add', :id => 'add_comment_title' %>
<div class='hint'>Let your listeners know what comments you want by adding a guiding title for them. Pose a question, ask for feedback, or anything else!</div>
<% end %>
<% end %>
I have has_many :comment_titles and accepts_nested_attributes_for :comment_titles, :comments in my model. when I create a new comment_title in the form, the old one is replaced. I want an additional one to be built. How can I do this?
Here are the video controller actions:
def new
#video = Video.new
respond_to do |format|
format.js do
render_to_facebox(:partial => 'add_video')
end
end
end
def create
#video = current_user.videos.new(params[:video])
respond_to do |format|
if #video.save
format.html { redirect_to(#video) }
else
format.html { render :action => "new" }
end
end
end
I think this is actually what is needed:
def update
#video = current_user.videos.find(params[:id])
respond_to do |format|
if #video.update_attributes(params[:video])
format.html { redirect_to(#video) }
format.js
else
format.html { render :action => "edit" }
end
end
end
The edit action here will provide a form which will allow you to edit the existing record as well as its nested attributes. This is why it's replacing the existing object.
If you only want people to add new comment titles then I would recommend building a new object in your edit action like this:
def edit
video = current_user.videos.find(params[:id])
video.comment_titles.build
end
Then this will be available as an additional row in your fields_for call. To only make this show new objects:
<% f.fields_for :comment_titles do |t| %>
<% if t.object.new_record? %>
# stuff goes here
<% end %>
<% end %>
However this restricts people to being able to only add new items in an edit action, which may seen counter-intuitive to some users.
I have a has_many and belongs_to association set up between two models: Project and Task.
I'd like to be able to create a form which enables me to create a new Task and assign an existing Project as a parent. For example, this form might have a pulldown for selecting from a list of existing projects.
There are only a finite set of projects available in this application, so I've created Project records via a seeds.rb file. I do not need to make a form for creating new Projects.
I believe I've achieved a solution by using a collection_select form helper tag in the new Task form. I'm pretty happy with how this works now, but just curious if there are other approaches to this problem.
#models/project.rb
class Project < ActiveRecord::Base
has_many :tasks, :dependent => :destroy
end
#models/task.rb
class Task < ActiveRecord::Base
belongs_to :project
end
#controllers/tasks_controller.rb
class TasksController < ApplicationController
def new
#task = Task.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #task }
end
end
def create
#task = Task.new(params[:task])
respond_to do |format|
if #task.save
format.html { redirect_to(#task, :notice => 'Task was successfully created.') }
format.xml { render :xml => #task, :status => :created, :location => #task }
else
format.html { render :action => "new" }
format.xml { render :xml => #task.errors, :status => :unprocessable_entity }
end
end
end
end
#views/new.html.erb
<h1>New task</h1>
<%= form_for(#task) do |f| %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="select">
<%= collection_select(:task, :project_id, Project.all, :id, :name) %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
<%= link_to 'Back', tasks_path %>
I just reviewed your code and this looks fantastic to me. One small tweak:
<%= f.collection_select(:project_id, Project.all, :id, :name) %>
This is just slightly cleaner in that you're still using the |f| block variable
Since you mentioned other approaches, I would definitely mention and actually recommend, you use formtastic. The associations are handled automatically and keeps your code clean and also gives you some great customization options.