I've been working on a rails form and think I've messed something up, but cant put my finger on what.
To give an overview. I have created a 1 page site with a contact form at the bottom. I have looked around for similar questions and they seem to have the contact form on another page with another controller, so can't find an answer for me. I know I've missed something, but can't figure out what.
I get an error which states
First argument in form cannot contain nil or be empty
My main_controller.rb is as follows
class MainController < ApplicationController
def new
#contact = Contact.new
end
def create
#contact = Contact.new(contact_params)
#contact.request = request
if #contact.deliver
redirect_to thank_you_path
else
flash.now[:error] = 'Cannot send message.'
render :index
end
end
private
def contact_params
params.require(:contact).permit(:name, :email, :phone, :message)
end
end
contact.rb is
class Contact < MailForm::Base
attribute :name, :validate => true
attribute :email, :validate => /\A([\w\.%\+\-]+)#([\w\-]+\.)+([\w]{2,})\z/i
attribute :phone, :validate => true
attribute :message, :validate => true
def headers
{
subject: "Quote Form",
to: "email-addy#gmail.com",
from: %("#{name}" <#{email}>)
}
end
end
index.html.erb
<%= bootstrap_form_for(#contact, layout: :horizontal, label_col: "col-sm-2", control_col: "col-sm-10") do |f| %>
form template here
<% end %>
routes/rb
Rails.application.routes.draw do
resources :main, only: [:new, :create]
root 'main#index'
get '/thank-you' => 'main#thank-you'
end
You are creating a instance of Contacts in new method and calling that instance with form_for on index.html.erb that's why you get #contact nil, just move this code #contact = Contact.new to index method/action where you actually calling the form for with th Contact instance.
Related
I have a ContactsController that looks like:
class ContactsController < ApplicationController
def create
#contact = Contact.new(contact_params)
if captcha?
ContactMailer.potential_lead(#contact).deliver if #contact.save
else
#contact.errors.add(:base, 'Verify your humanity')
end
respond_with #contact
end
def thanks
#contact = 'success'
render file: '/layouts/contact'
end
private
def contact_params
params.require(:contact).permit(:name, :message, :interest, :email, :phone)
end
end
There are no issues with the ContactsController, views, mailers, etc. All works fine. However I need to add a standalone page and I'm planning on using the ContactsController to handle the form. So I created a new view in Layouts titled widget.
The idea being that when someone fills out the contact form on the widget page there's a hidden field that handles the interest and just uses the contact mailer, etc.
However I'm running into some issues. On my view I have:
=content_for :body do
.container
.row
.col
=render partial: '/shared/widget_form'
Then in my partial:
=tb_form_for #contact, remote: true, data: {errors: :inline, success: thanks_path } do |f|
.row
.col
= tb_form_errors(f.object, :base)
.row
.col
=f.text_field :name, class: 'form-control', placeholder: 'Name', required: true
Then I updated my routes to have
resources :contacts, only: [:create]
match 'widget' -> 'contacts#create', via: :post
However I end up with
First argument in form cannot contain nil or be empty
I thought maybe it's an issue with doing post, although I want to post to the Contacts > Create, but I changed that to get and end up with:
param is missing or the value is empty: contact
So I thought I'm probably using match wrong so I'll just do:
get 'widget', to: 'contacts#create', as: :widget
Then I updated my form to have:
=tb_form_for #contact, url: widget_path, remote: true, data: {errors: :inline, success: thanks_path}
And I'm still getting
param is missing or the value is empty: contact
What am I missing? I'd just like to reuse the method on a different page.
EDIT:
If I use:
=tb_form_for Contact.new
It works....but this feels like a cheat. Any way around this?
How does rails render forms?
In short:
For example user sends a get request to an contacts#index
Inside #index action you something like #contact = Contact.new
By default index.haml view will be rendered for #index action if another view is not specified.
In this case index.haml view will be able to access #contact variable.
In your case there is method = content_for :body do that renders partial: '/shared/widget_form'
I believe there is yield :body somewhere in another view or layout.
So to make it work you need to initialize #contact variable in any controller that may render view/layout with yield :body
I have a very straight-forward task to fulfil --- just to be able to write comments under posts and if the comments fail validation display error messages on the page.
My comment model uses a gem called Acts_as_commentable_with_threading, which creates a comment model after I installed.
On my post page, the logic goes like this:
Posts#show => display post and a form to enter comments => after the comment is entered, redisplay the Post#show page which has the new comment if it passes validation, otherwise display the error messages above the form.
However with my current code I can't display error messages if the comment validation fails. I think it is because when I redisplay the page it builds a new comment so the old one was erased. But I don't know how to make it work.
My codes are like this:
Comment.rb:
class Comment < ActiveRecord::Base
include Humanizer
require_human_on :create
acts_as_nested_set :scope => [:commentable_id, :commentable_type]
validates :body, :presence => true
validates :first_name, :presence => true
validates :last_name, :presence => true
# NOTE: install the acts_as_votable plugin if you
# want user to vote on the quality of comments.
#acts_as_votable
belongs_to :commentable, :polymorphic => true
# NOTE: Comments belong to a user
belongs_to :user
# Helper class method that allows you to build a comment
# by passing a commentable object, a user (could be nil), and comment text
# example in readme
def self.build_from(obj, user_id, comment, first_name, last_name)
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
end
end
PostController.rb:
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = Comment.build_from(#post, nil, "", "", "")
end
end
CommentsController:
class CommentsController < ApplicationController
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
private
def comment_params
params.require(:comment).permit(:user, :first_name, :last_name, :body, :commentable_id, :commentable_type, :comment_id,
:humanizer_answer, :humanizer_question_id)
end
def commentable_type
comment_params[:commentable_type]
end
def commentable_id
comment_params[:commentable_id]
end
def comment_id
comment_params[:comment_id]
end
def body
comment_params[:body]
end
def make_child_comment
return "" if comment_id.blank?
parent_comment = Comment.find comment_id
#comment.move_to_child_of(parent_comment)
end
def build_comment(comment_params)
if current_user.nil?
user_id = nil
first_name = comment_params[:first_name]
last_name = comment_params[:last_name]
else
user_id = current_user.id
first_name = current_user.first_name
last_name = current_user.last_name
end
commentable = commentable_type.constantize.find(commentable_id)
Comment.build_from(commentable, user_id, comment_params[:body],
first_name, last_name)
end
end
comments/form: (this is on the Posts#show page)
<%= form_for #new_comment do |f| %>
<% if #new_comment.errors.any? %>
<div id="errors">
<h2><%= pluralize(#new_comment.errors.count, "error") %> encountered, please check your input.</h2>
<ul>
<% #new_comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<% end %>
I would instead use nested routes to create a more restful and less tangled setup:
concerns :commentable do
resources :comments, only: [:create]
end
resources :posts, concerns: :commentable
This will give you a route POST /posts/1/comments to create a comment.
In your controller the first thing you want to do is figure out what the parent of the comment is:
class CommentsController < ApplicationController
before_action :set_commentable
private
def set_commentable
if params[:post_id]
#commentable = Post.find(params[:post_id])
end
end
end
This means that we no longer need to pass the commentable as form parameters. Its also eliminates this unsafe construct:
commentable = commentable_type.constantize.find(commentable_id)
Where a malicous user could potentially pass any class name as commentable_type and you would let them find it in the DB... Never trust user input to the point where you use it to execute any kind of code!
With that we can start building our create action:
class CommentsController < ApplicationController
before_action :set_commentable
def create
#comment = #commentable.comments.new(comment_params) do |comment|
if current_user
comment.user = current_user
comment.first_name = current_user.first_name
comment.last_name = current_user.last_name
end
end
if #comment.save
respond_to do |format|
format.json { head :created, location: #comment }
format.html { redirect_to #commentable, success: 'Comment created' }
end
else
respond_to do |format|
format.html { render :new }
format.json { render json: #comment.errors, status: 422 }
end
end
end
private
# ...
def comment_params
params.require(:comment).permit(:first_name, :last_name, :body, :humanizer_answer, :humanizer_question_id)
end
end
In Rails when the user submits a form you do not redirect the user back to the form - instead you re-render the form and send it as a response.
While you could have your CommentsController render the show view of whatever the commentable is it will be quite brittle and may not even provide a good user experience since the user will see the top of the post they where commenting. Instead we would render app/views/comments/new.html.erb which should just contain the form.
Also pay attention to how we are responding. You should generally avoid using redirect_to :back since it relies on the client sending the HTTP_REFERRER header with the request. Many clients do not send this!
Instead use redirect_to #commentable or whatever resource you are creating.
In your original code you have totally mixed up JSON and HTML responses.
When responding with JSON you do not redirect or send flash messages.
If a JSON POST request is successful you would either:
Respond with HTTP 201 - CREATED and a location header which contains the url to the newly created resource. This is preferred when using SPA's like Ember or Angular.
Respond with HTTP 200 - OK and the resource as JSON in the response body. This is often done in legacy API's.
If it fails do to validations you should respond with 422 - Unprocessable Entity - usually the errors are rendered as JSON in the response body as well.
Added.
You can scrap your Comment.build_from method as well which does you no good at all and is very idiosyncratic Ruby.
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = #post.comments.new
end
end
Don't use line contiuation (\) syntax like that - use parens.
Don't:
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
Do:
new(
foo: a,
bar: b
)
Added 2
When using form_for with nested resources you pass it like this:
<%= form_for([commentable, comment]) do |f| %>
<% end %>
This will create the correct url for the action attribute and bind the form to the comment object. This uses locals to make it resuable so you would render the partial like so:
I'm assuming your form_for submits a POST request which triggers the HTML format in CommentsController#create:
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
So, if #comment.save fails, and this is an HTML request, the #create method renders create.html. I think you want to render Posts#show instead.
Keep in mind that if validations fail on an object (Either by calling save/create, or validate/valid?), the #comment object will be populated with errors. In other words calling #comment.errors returns the relevant errors if validation fails. This is how your form is able to display the errors in #new_comment.errors.
For consistency, you'll need to rename #new_comment as #comment in the posts#show action, otherwise you'll get a NoMethodError on Nil::NilClass.
TL;DR: You're not rendering your form again with your failed #comment object if creation of that comment fails. Rename to #comment in posts, and render controller: :posts, action: :show if #comment.save fails from CommentsController#create
I have figured out the answer myself with the help of others here.
The reason is that I messed up with the JSON format and html format (typical noobie error)
To be able to display the errors using the code I need to change two places ( and change #comment to #new_comment as per #Anthony's advice).
1.
routes.rb:
resources :comments, defaults: { format: 'html' } # I set it as 'json' before
2.
CommentsController.rb:
def create
#new_comment = build_comment(comment_params)
respond_to do |format|
if #new_comment.save
make_child_comment
format.html { redirect_to(:back, :notice => 'Comment was successfully added.') }
else
commentable = commentable_type.constantize.find(commentable_id)
format.html { render template: 'posts/show', locals: {:#post => commentable} }
format.json { render json: #new_comment.errors }
end
end
end
i have this controller
class StoresController < ApplicationController
before_filter :authenticate_business!, :except => [:index, :show]
def index
##stores = Store.paginate(:page => params[:page])#, :per_page => 8)
if params[:query].present?
#stores = Store.search(params[:query], page: params[:page])
else
#stores = Store.all.page params[:page]
end
end
def show
#store = Store.friendly.find(params[:id])
if request.path != store_path(#store)
redirect_to #store, status: :moved_permanently
end
end
def new
#store = Store.new
end
def create
#store = Store.new(store_params)
#store.business_id = current_business.id
if #store.save
redirect_to #store
else
render 'new'
end
end
def edit
#store = Store.friendly.find(params[:id])
end
def update
#store = Store.friendly.find(params[:id])
if #store.update(store_params)
redirect_to #store
else
render 'edit'
end
end
def destroy
#store = Store.friendly.find(params[:id])
#store.destroy
redirect_to stores_url
end
private
def store_params
params.require(:store).permit(:name, :description, :address, :telephone, :email, :website)
end
end
and a view with a form to create a new store.
<%= form_for #store do |f| %>
.......
code
......
<% end %>
The problem is that when i submit the form, it gives me this error "param is missing or the value is empty: store", pointing at line "params.require(:store).permit(:name, :description, :address, :telephone, :email, :website)"
Any idea to solve this problem?
Thank you.
I had this same issue and it was caused by a route issue, as discussed in the comments, causing the form not to post any data.
I think what you need is to make sure 'get' requests to the 'new' route access your 'new' method, while 'post' requests to the 'new' route access your 'create' method. Something like:
get 'stores/new' => 'stores#new'
post 'stores/new' => 'stores#create'
I'm a new rails developer who has a basic scaffolded crud application that I modified a bit.
I'm getting this error:
undefined method description for #<ActiveRecord::Relation:0x00000102df26d8>
when I visit john/recipes/46. Here's my view:
<h1 itemprop="name"><%= #recipe.name %></h1>
<ul>
<li><%= link_to 'Edit', edit_recipe_path(#recipe) %></li>
</ul>
<p itemprop="description"><%= #recipe.description %></p>
here's my routes:
match "/:username" => "recipes#index"
scope ':username' do
resources :recipes
end
here's my show index:
def show
#user = User.find_by_username params[:username]
#recipe = Recipe.where(:user_recipe_id => params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #recipe }
end
end
and my model:
before_save :set_next_user_recipe_id
belongs_to :users
validates :user_recipe_id, :uniqueness => {:scope => :user_id}
def to_param
self.user_recipe_id.to_s
end
def set_next_user_recipe_id
self.user_recipe_id ||= get_new_user_recipe_id
end
def get_new_user_recipe_id
user = self.user
max = user.recipes.maximum('user_recipe_id') || 0
max + 1
end
attr_accessible :description, :duration, :author, :url, :name, :yield, :ingredients_attributes, :user_recipe_id, :directions_attributes, :tag_list, :image
The reason I'm doing a Recipe.where(:user_recipe_id => params[:id]) instead of Recipe.where(:id => params[:id]) is because I'm trying to get so instead of john/recipes/46 showing the 46th recipe in the database, instead to show the 46th recipe that belongs to John.
Thanks for all help!
You're only trying to look for one recipe, but your query is searching for multiples. When you use a plain where(...) without ending it with .first, Rails interprets it as "show me all (multiple) Recipes with this user id" instead of "show me the (one) recipe with this id".
So you need to either put .first at the end of your query:
#recipe = Recipe.where(:user_recipe_id => params[:id]).first
or use an ActiveRecord finder that only returns one record:
#recipe = Recipe.find_by_user_recipe_id(params[:id])
How can you pass an error messages coming from a model --> controller to view?
= form_tag :controller => "article", :action => "create" do
/ how to retrieve error messages here?
%p
= label_tag :article, "Article"
= text_field_tag :article
= submit_tag "Submit Article"
I have this model:
class Article < ActiveRecord::Base
attr_accessible :article
validates :article, :presence => true
end
In my controller:
def create
#article = Article.new(params[:article])
if ! #article.save
# how to set errors messages?
end
end
I'm using Rails 3.0.9
The errors messages are stored in your model. You can access through the errors methods, like you can see in http://api.rubyonrails.org/classes/ActiveModel/Errors.html.
An easy way to expose the error message is including the follow line in your view:
%span= #article.errors[:article].first
But, I belive you have to change your controller to be like that:
def new
#article = Artile.new
end
def create
#article = Artile.new params[:article]
if !#article.save
render :action => :new
end
end
In the new action you don't need to try save the article, because the creation action already do that job. The new action exists, (basically) to call the new view and to provide support for validations messages.
The newmethod shouldn't save anything. create method should.
def create
#article = Article.new(params[:article])
if ! #article.save
redirect_to root_path, :error => "ops! something went wrong.."
end
end