I have a rails app!
I'd like to create a form for a product model, where users can choose a product category first and then can fill the form out.
This would be easy, but I'd like to show them different attributes based on the chosen category. Something like if they choose book category, then they will have fields like title, author, published_at, but if they choose shoes category then they can fill out the size, color and type fields.
I saw afew tuts about dynamic forms, but as far as I understand it, I don't need that since the form fields will be predefined and users won't be able to add extra fields.
What is the good approach in this case? Should I create more different models like (shoes,books, etc.) or something else?
Should I create more different models
No, I don't think that's necessary.
What you'd be best doing is using ajax to populate the form on category change. This would require some configuration, but will make it the most efficient and secure:
#config/routes.rb
resources :products do
put :new, on: :new #-> url.com/products/new
end
#app/controllers/products_controller.rb
class ProductsController < ApplicationController
def new
if request.get?
#product = Product.new
#categories = Category.all
elsif request.put?
#category = params[:product][:category_id]
#attributes = ...
end
respond_to do |format|
format.js
format.html
end
end
end
#app/views/products/new.html.erb
<%= form_for #product do |f| %>
<%= f.collection_select :category_id, #categories, :id, :name, {}, { data: { remote: true, url: new_product_path, method: :put }} %>
<div class="attributes"></div>
<%= f.submit %>
<% end %>
#app/views/products/new.js.erb
$attributes = $(...); // need a way to create form elements from #attributes
$("form#new_product .attributes").html( $attributes );
Something important to note is that Rails select & check elements allow you to use the data-remote attribute to send an ajax call to your controller on change.
Not much documentation about it, playing around with the above code should get it to work.
Related
In my rails app I'm trying to use a form partial to display the same quiz on the new and edit views. I can see the new view page, but when I hit <%= f.submit "Submit Answers" %> I get an error saying No route matches [POST] "/flex_quiz/new".
Here is the form for line in my partial:
<%= form_for #flex_quiz, url: url do |f| %>
And here's how the locals stand in my new view:
<%= render partial: "quiz", locals: { url: new_flex_quiz_path, method: :post } %>
And my edit view:
<%= render "quiz", url: edit_flex_quiz_path(#flex_quiz), method: :put %>
Here are the route paths:
Prefix Verb URI Pattern Controller#Action
...
flex_quiz_index GET /flex_quiz(.:format) flex_quiz#index
POST /flex_quiz(.:format) flex_quiz#create
new_flex_quiz GET /flex_quiz/new(.:format) flex_quiz#new
edit_flex_quiz GET /flex_quiz/:id/edit(.:format) flex_quiz#edit
flex_quiz GET /flex_quiz/:id(.:format) flex_quiz#show
PATCH /flex_quiz/:id(.:format) flex_quiz#update
PUT /flex_quiz/:id(.:format) flex_quiz#update
DELETE /flex_quiz/:id(.:format) flex_quiz#destroy
Can anyone suggest how to fix this? I have looked at several similar posts (like this and this) but since I'm using partials the solution here is going to have to be a bit different.
EDIT
Here are my definitions in my flex_quiz_controller:
class FlexQuizController < ApplicationController
before_action :require_sign_in
def show
#flex_quiz = FlexQuiz.find(params[:id])
end
def new
#flex_quiz = current_user.build_flex_quiz
end
def create
#flex_quiz = FlexQuiz.new
#flex_quiz.flex01 = params[:flex_quiz][:flex01]
#flex_quiz.flex02 = params[:flex_quiz][:flex02]
#flex_quiz.flex03 = params[:flex_quiz][:flex03]
#flex_quiz.flex04 = params[:flex_quiz][:flex04]
#flex_quiz.flex05 = params[:flex_quiz][:flex05]
#flex_quiz.flex06 = params[:flex_quiz][:flex06]
#flex_quiz.flex07 = params[:flex_quiz][:flex07]
#flex_quiz.flex08 = params[:flex_quiz][:flex08]
#flex_quiz.flex09 = params[:flex_quiz][:flex09]
#flex_quiz.flex10 = params[:flex_quiz][:flex10]
#flex_quiz.user = current_user
if #flex_quiz.save
flash[:notice] = "Quiz results saved successfully."
redirect_to user_path(current_user)
else
flash[:alert] = "Sorry, your quiz results failed to save."
redirect_to welcome_index_path
end
end
def edit
#flex_quiz = FlexQuiz.find(params[:id])
end
def update
#flex_quiz = FlexQuiz.find(params[:id])
#flex_quiz.assign_attributes(flex_quiz_params)
if #flex_quiz.save
flash[:notice] = "Post was updated successfully."
redirect_to user_path(current_user)
else
flash.now[:alert] = "There was an error saving the post. Please try again."
redirect_to welcome_index_path
end
end
private
def flex_quiz_params
params.require(:flex_quiz).permit(:flex01, :flex02, :flex03, :flex04, :flex05, :flex06, :flex07, :flex08, :flex09, :flex10)
end
end
If you want to create new flex_quiz objects, then you're going to want to POST to flex_quiz_index_path.
Notice in your route paths, if you look at new_flex_quiz, the HTTP verb is a GET.
It may be slightly unintuitive, but the new action is actually a GET request.
The action in which the object is supposed to be created in is the create action.
So to solve your problem this should do the trick:
<%= render partial: "quiz", locals: { url: flex_quiz_index_path, method: :post } %>
EDIT:
Instead of defining locals, you can simply define your forms in form_for as such:
You will also have to define #flex_quiz in your controller actions as well (in your case new and edit) form_for will automatically infer the appropriate URL.
From documentation:
However, further simplification is possible if the record passed to
form_for is a resource, i.e. it corresponds to a set of RESTful
routes, e.g. defined using the resources method in config/routes.rb.
In this case Rails will simply infer the appropriate URL from the
record itself.
You'll also need to change the naming from singular form to plural.
The rails to do resuable forms is:
app/views/flex_quiz/_form.html.erb:
<%= form_for(#flex_quiz) do |f| %>
# ...
<% end %>
app/views/flex_quiz/new.erb:
<h1>Create a new quiz</h1>
<%= render 'form' %>
app/views/flex_quiz/edit.erb:
<h1>Edit a quiz</h1>
<%= render 'form' %>
While using locals can often be a good idea its not needed here. Note we just pass the resource and not a URL to form_for - that is convention over configuration in action and is what makes Rails awesome.
Rails figures out all by itself what URL to use for the action attribute and what method to use based on if the resource has been saved.
However for this to work you to actually follow the conventions. Make sure you are using the proper plural forms (the plural of quiz is quizzes):
resources :flex_quizzes
class FlexQuizzesController < ApplicationController
end
Unfortunately when it comes to rest of your setup you need to revisit the drawing board. Its not very realistic to think that you can do this with a single model. You would usually have several models with relations:
class Quiz
has_many :questions
end
class Question
belongs_to :quiz
has_many :answers
end
class Answer
belongs_to :question
end
class UserQuiz
belongs_to :user
belongs_to :quiz
end
class UserAnswer
belongs_to :question
belongs_to :answer
end
You would use one or several controllers to let admins create the quizes and a separate controller to let users answer the quiz. Its a quite common domain so you should be able to find plenty of examples.
I'm trying to make simple app. I input my first name and last name to simple <%= form_for #data do |f| %> rails form and after submitting it, app should render simple text like this. My first name is <%= data.first_name %> and my last name is <%= data.last_name %>. I don't know why but my app is saying this error:
undefined local variable or method `data' for
It's probably saying it because no params are passed to view.
Here is my code.
routes.rb
resources :data, only: [:new, :create, :index]
data_controller.rb
class DataController < ApplicationController
def new
#data = Data.new
end
def index
end
def create
#data = Data.new(data_params)
if #data.valid?
redirect_to #data
else
render :new
end
end
private
def data_params
params.require(:data).permit(:first_name, :second_name)
end
end
/views/data/new.html.erb
<%= form_for #data do |f| %>
<%= f.label :first_name %>
<%= f.text_field :first_name %>
<%= f.label :second_name %>
<%= f.text_field :second_name %>
<%= f.submit 'Continue', class: 'button' %>
<% end %>
/views/data/index.html.erb
<h2>Coolest app ever :D</h2>
<p>My first name is: <%= data.first_name %>.</p>
<p>And my second name is: <%= data.second_name %>.</p>
/models/data.rb
class Data
include ActiveModel::Model
attr_accessor :first_name, :second_name
validates :first_name, :second_name, presence: true
end
Please help to find out why params are not passing to next page. Thanks anyways :D
Your view should look like this:
<h2>Coolest app ever :D</h2>
<p>My first name is: <%= #data.first_name %>.</p>
<p>And my second name is: <%= #data.second_name %>.</p>
Also, I would suggest that calling a model something generic like Data is not a very Rails-y approach. Generally, domain models correspond to real-world things like User and Article, which are easy to understand and relate to. It'll get confusing quite fast if you use need to make another model and want to call it Data2 or something :)
Edit:
Since you specified that you do not wish to use the database, I would recommend passing in the object params through the redirect:
redirect_to(data_path(data: #data))
and in your controller's index method:
def index
#data = Data.new(params[:data])
end
Now your view should render properly, since you're passing the in-memory #data object attributes as params within the redirect. You then recreate this object in the index page or wherever you wish to redirect to.
To expand on Matt's answer, the reason you're getting NilClass errors is because:
You're redirecting to a data#show action when no show action has been enabled within your routes file. Since you've set your views up for the index, I'm assuming you want to redirect there when the #data object has been verified as valid:
redirect_to data_path
However I would recommend you follow Rails conventions and specify the data#show route within your routes.rb:
resources :data, only: [:index, :new, :create, :show]
and in your data_controller.rb:
def show
#data = Data.find(params[:id])
end
Another problem is that you're not actually saving the #data object upon creating it. The new method populates the attributes, and valid? runs all the validations within the specified context of your defined model and returns true if no errors are found, false otherwise. You want to do something like:
def create
#data = Data.new(data_params)
if #data.save
redirect_to data_path
else
render :new
end
end
Using save attempts to save the record to the database, and runs a validation check anyways - if validation fails the save command will return false, the record will not be saved, and the new template will be re-rendered. If it is saved properly, the controller will redirect to the index page, where you can call upon the particular data object you want and display it within your view.
I have this structure in my view:
<%= simple_form_for #user ... %>
...
...
<%= render '/hobbies/form', :user => #user %>
...
...
<% end %>
The render part loads data from a partial - there's a form. The form-fields are successfully loaded (like inputs, submit input, radio buttons etc), but when I take a look at the generated HTML, in the rendered form is missing <%= form_for... and the closing tag.
Which means when I click to the submit button from the rendered form, this rendered form is not sent out, but instead of it is sent out the "big" for - simple_form_for.
How to make the rendered form sendable?
Thank you
Simply, you can't have two separate nested forms.
You can have nested associated forms using accepts_nested_attributes_for - this is dependent on the structure of your models in the backend, which I'll detail below.
--
Forms
The most important thing you need to know is that Rails just builds HTML forms - which means they have to abide by all the constraints & rules these forms stipulate:
Each form has its own action attribute - which is how your browser knows where to send the data. If you have multiple / nested forms, you'll basically have two of these "action" attributes; preventing HTML from sending the form correctly
--
Nested
If you want to use some Rails conventions, you'll be better using the accepts_nested_attributes_for method. This resides in the model, and in order to use it effectively, you need to ensure you have your ActiveRecord associations set up correctly:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :hobbies
accepts_nested_attributes_for :hobbies
end
#app/models/hobby.rb
Class Hobbies < ActiveRecord::Base
belongs_to :user
end
The importance of the association is crucial - Rails will pass "nested" model data through to each other. The nested model data is only there if the models are associated correctly with ActiveRecord (as above)
You can then pass the data through your controller / models as follows:
#app/controllers/users_controller.rb
Class UsersController < ApplicationController
def new
#user = User.new
#user.hobbies.build # -> essential for nested attributes
end
def create
#user = User.new(user_params)
#user.save #-> notice how this saves the #user model only?
end
private
def user_params
params.require(:user).permit(:user, :attributes, hobbies_attributes: [:hobbies, :attributes])
end
end
This will allow you to include the hobbies part of the form as follows:
<%= form_for #user do |f| %>
<%= f.fields_for :hobbies do |h| %>
<%= h.text_field :your_attributes %>
<% end %>
<% end %>
Nested forms is not valid html. To achieve this you have to either un-nest your forms or use javascript. If you opt for the javascript approach, you simple catch the form submit event, prevent the default, and manually submit the form you want.
EDIT:
If you opt for the javascript approach, consider that you might not need two forms at all. Instead you can just add a click handler for the button on your 'sub action' which makes an ajax request.
Alternatively, if javascript is not an option, and rearranging the html is not an option, you could handle this on the server by submitting the data from both forms and processing each differently depending on which submit input was clicked.
You can't have a form inside a form. If you have your associations correctly setup somthing like
class User < ActiveRecord::Base
has_many :hobbies
accepts_nested_attributes_for :hobbies
end
class Hobby < ActiveRecord::Base
belongs_to :user
end
then you can use fields_for in your form like
<%= form_for #user do |u| %>
// user fields
<%= u.fields_for :hobbies do |h| %>
// hobbies fields
<% end %>
<%= f.submit %>
<% end %>
This will give you params like
users_attributes: { name: "xyz", hobbies_attributes: { attribute: "value"} }
For details checkout accept_nested_attributes_for
I have Categories and Products. A product has a relation
belongs_to :category
In the categories show page I have a button to add a new product. This button goes to a page where I create the new product, but I need to give the category to the new product.
How can I pass the id from the category page where I was to the new Product? So, if I am in the category Electronic I click 'Add product' and this product automaticaly is associated with Eletronic category.
Hope you can understand what I want.
Thanks
You need to pass the category_id in your link, e.g. new_product_path(category_id: #category.id).
You will also need to have a field in your product form to save the category's ID, e.g <%= f.hidden_field :category_id, params[:category_id] %>
First, I would decide whether each product is contained within a category, or whether it's simply associated with a category. Hints it is contained would be:
You expect each product to have exactly one 'parent' category.
You expect each product will always appear in the context of its parent category.
If and only if you believe this to be the case, I would be tempted to nest the product resource within the category.
# routes.rb
resources :categories do
resources :products
end
# products_controller.rb (SIMPLIFIED!)
class ProductController < ApplicationController
before_filter :get_category
def new
#product = #category.products.build
end
def create
#product = #category.products.build(params[:product])
if #product.save
redirect_to #product
else
render template: "new"
end
end
def get_category
#category = Category.find(params[:category_id])
end
end
If you do this, rails will ensure your product is associated with the right category. The magic happens in #category.products.build, which automatically sets the category_id based on the relationship.
If you'd rather keep categories and products as simple associations, I'd just use a query parameter as per Eric Andres answer, although I'd be tempted to handle it in a slightly different way:
# link:
new_product_path(category_id: #category.id) # So far, so similar.
# products_controller.rb
class ProductsController < ApplicationController
def new
#product = Product.new
#product.category_id = params[:category_id].to_i if params[:category_id]
end
end
# new.erb
<%= f.hidden_field :category_id %>
This is mostly just a stylistic difference. Eric's answer will work too - I just prefer to set the value on the model itself rather than have the view worry about parameters etc.
I have this form in my application.html.erb.
<%= form_tag(:action=>"index", :controller=>"posts") %>
<p>
// code here
</p>
I dont understand why is this getting directed to posts->create instead of posts->index?
Thanks.
Basically, Rails observes and obeys "RESTful" web service architecture. With REST and Rails, there are seven different ways to interact with a server regarding a resource. With your current code, specifying the form's action as index doesn't make sense: Rails' form helpers can either POST, PUT or DELETE.
If you wanted to create a post, then redirect to the index, you can do so in the applicable controller action:
class PostsController < ApplicationController
...
def create
#post = Post.new
respond_to do |format|
if #post.save
format.html { redirect_to(:action => 'index') }
end
end
While your form would look like:
<% form_for #post do |f| %>
# put whatever fields necessary to create the post here
<% end %>
You seem to be a little mixed up with respect to the uses for each action. Here's a quick summary of typical RESTful usage:
Index -> view a list of items
New/Edit -> form where items are added or edited
Create/update -> controller action where items are created/updated
The reason your routes file is not taking you to index is because index is not an action where posts are typically created or updated. The best way is to go RESTful. Unless you have a very unusual situation, the best way to set your system up is probably a little like this:
# routes.rb
resources :posts
# application.html.erb (or better: posts/_form.html.erb).
<% form_for #post do |f| %>
<% end %>
# posts controller, whichever action you want to use
def new
#post = Post.new
end
By putting the form in a partial called form you can access it in new, edit, or wherever else you need to manipulate a post in your system.