Rails: form_for in application wide footer - ruby-on-rails-5

I started learning Rails for about a month now and I am working on a fairly simple project to improve my skills. It's a blog where editors can add articles and users can subscribe to a newsletter by adding their email. The homepage is an index view and their is a footer that shows up across all pages.
Here is my problem: I would like to include the form_for the newsletter on the footer that exists inside the application layout. That form has a specific model: Subscriber, which stores users emails.
What I've done so far is include the following on the Articles controller:
def index
#articles = Article.order(created_at: :desc).all
#subscriber = Subscriber.new
end
def create
#subscriber = Subscriber.new(subscriber_params)
if #subscriber.save
redirect_to '/home'
else
render 'new'
end
end
def subscriber_params
params.require(:subscriber).permit(:email)
end
In application.html.erb :
<%= form_for(#subscriber) do |f| %>
<%= f.email_field :email, :placeholder => "email address" %>
<%= f.submit 'Sign up', :id => "signup" %>
<% end %>
The form is displayed correctly on the index page only and it doesn't save to the database (without error).
I have tried using a before filter on the application controller as well as rendering a partial without any success.
Edit
Subscribers controller code:
def new
#subscriber = Subscriber.new
end
def create
#subscriber = Subscriber.new(subscriber_params)
if #subscriber.save
redirect_to '/home'
else
render 'new'
end
end
private
def subscriber_params
params.require(:subscriber).permit(:email)
end
Application controller:
before_action :create_new_subscriber
def create_new_subscriber
#subscriber = Subscriber.new
end

Related

Nested routes and dot in url in Rails

I'm building a simple todo app for some practice. I have projects which has_many tasks and tasks belongs_to projects.
So that I can display url/projects/1/tasks I'm nesting the route:
routes.rb
resources :projects do
resources :tasks
end
In my project show view I have the following form:
Add a task:
<%= form_for [#project, #task] do |f| %>
<%= f.label :Task_name %>
<%= f.text_field :name, :placeholder => "Task Name" %>
<%= f.submit 'Create Task' %>
<% end %>
In my tasks controller I'm doing the following:
def create
#project = Project.find(params[:project_id])
#task = #project.tasks.new(params[:task])
if #task.save
redirect_to projects_path(#project)
else
redirect_to projects_path(#project)
end
end
It seems on task.save when I redirect and pass in the project instance variable it redirects me to http://todoapp.dev/projects.5 (5 being the id of the project) instead of http://todoapp.dev/projects/5.
Could the problem be in my controller with the redirect_to method or possibly the nested route?
I have a basic understanding of Rails routing but could use some advice.
Looks like in the redirect_to in my controller I was using the pluralized version of the path. projects_path(#project) instead of project_path(#project).
def create
#project = Project.find(params[:project_id])
#task = #project.tasks.new(params[:task])
if #task.save
redirect_to project_path(#project)
else
redirect_to project_path(#project)
end
end
This ended up working.

Rails 310 Redirect Loop

I'm going back to write a basic app with projects that have tasks. In my show view of a project I want to list the tasks and also include a form. When I wire this all up I get 310 Redirect loop. It's been a while since I've written anything from scratch so would appreciate some help looking at my code.
controller code:
def show
#project = Project.find(params[:id])
#task = #project.tasks.new(params[:task])
if #task.save
redirect_to #project, :notice => "Task added"
else
render action: :show
end
end
view code:
<%= #project.project_name %>
<%= form_for(#task) do |m| %>
<%= m.label :Task %>
<%= m.text_field :task_name %>
<%= m.button :submit %>
<% end %>
<% #project.tasks.each do |t| %>
<%= t.task_name %>
<% end %>
project.rb
has_many :tasks
task.rb
belongs_to :project
You are redirecting to #project, which is interpreted as meaning, redirect to the show page for #product. But you are calling redirect from the show page, hence the redirect loop:
request routed to show page
find project
task instantiated
task saved
redirect to show (loop back to 2)
Normally you don't create records in show, you do it in create. Any reason you're doing it this way?

Adding ajax to Rails 3 form_for

I'm learning to program and got a form running in my Rails 3 app. Now I'm attempting to add ajax to the form so the page doesn't reload after submitting.
I've followed the numerous tutorials but can't quite seem to figure out how to bring it together. The form adds new Objects to the Profile through the following model:
class Profile < ActiveRecord::Base
has_many :objects
end
class Object < ActiveRecord::Base
belongs_to :profile
end
My form in views/profiles/_object_form.html.erb:
<%= form_for(#object, :remote => true) do |f| %>
<% end %>
Where the form and its created objects are rendered in my views/profiles/_about.html.erb:
<div id="newObjects">
<%= render :partial => 'object_form' %>
</div>
<div id="objectList">
<%= render :partial => 'object', :collection => #profile.objects, :locals => {:object_count => #profile.objects.length) %>
</div>
In my objects_controller.rb I have the following create action:
def create
#object = Object.new(params[:object].merge(:author_id => current_user.id))
respond_to do |format|
if #object.save!
format.html {redirect_to profile_path(#object.profile) }
format.js { render }
else
format.html { redirect_to #profile, :alert => 'Unable to add object' }
end
end
end
In views/objects/create.js.erb:
$('#objectList').append("<%= escape_javascript(render #profile.object)) %>");
So I have a form calling an action in another controller to which I want to add ajax. What happens at the moment is that I need to reload the profile to show the newly created object. What am I doing wrong?
CLARIFICATION: Other than the create action in the ObjectsController, I only reference #object once elsewhere. That's in the ProfilesController's show action:
def show
#profile = Profile.find(params[:id])
#superlative = #profile.superlatives.new`
end
Not sure if this is a full code snippet for your create action, but looks like you are trying to call render on an instance variable that doesn't exist... #profile is never set in the create method in the ObjectController...
Perhaps you meant to type $('#objectList').append("<%= escape_javascript(render #object)) %>");
Also noticed that in your existing code you're making a call to render #profile.object, but the Profile class has a has_many relationship with your Object class, so if that was the right code, then you should type render #profile.objects (plural, not singular).
But I would think you would likely want the code I mentioned above, since you are appending onto the list of objects, not rendering the list again?

Issues getting a profile to update and show newly submitted form item

Following up on a previous question, I have a few issues to resolve before I have a comment form showing and submitting securely on my profile. I'm a beginner to programming so thinking across multiple controllers seems to have me lost.
What I'm doing is posting comments in a form, then listing them.
Background: The _comment_form and _comment reside as partials in the Profile about. (My next task is toggling from about to other Profile information, but that's another question altogether.)
Using the help provided in my last question I feel like I'm almost there but am getting an error.
CreateComments migration:
t.integer :profile_id
t.integer :author_id
t.string :body
My Comment model:
class Comment < ActiveRecord::Base
belongs_to :profile
belongs_to :author, :class_name =>"User", :foreign_key => "author_id"
end
CommentsController:
def create
#comment = Comment.new(params[:comment].merge(:author_id => current_user.id))
#comment.save!
redirect_to profile_path(#comment.profile)
end
ProfilesController:
def create
#profile = Profile.new(params[:profile])
if #profile.save
redirect_to profile_path(#profile), :notice => 'User successfully added.'
else
render :action => 'new'
end
end
def show
#user = User.find(params[:id])
#profile = #user.profile
#comment = #profile.comments.new
end
Comment partials inside Profile partial:
<div id="commentEntry">
<%= render :partial => 'comment', :collection => #profile.comments %>
</div>
<div id="newitem">
<%= render :partial => 'comment_form' %>
</div>
Routes.rb:
resources :users do
resources :profiles
end
resources :comments
_comment_form.html.erb:
<%= form_for #comment do |f| %>
<%= f.text_field :body %>
<%= f.submit 'Add new' %>
<% end %>
_comment.html.erb:
<li class="comment" title="<%= #comment.author.profile.first_name %> <%= #comment.author.profile.last_name %>">
<%= #comment.body %>
</li>
So, Issue #1: Wrapping the _comment.html.erb in a loop <% for #comment in #user.profile.comments %> shows the profile but when I try and submit a new comment I get "Unknown action The action 'update' could not be found for CommentsController". If I take away the loop, the profile doesn't show and I get "NoMethodError in Profiles#show undefined method `profile' for nil:NilClass". Can anyone help me out and explain what I'm doing wrong?
Issue #2: I created a sample comment in rails console and when I get the profile to show, the input field for comment :body repopulates with the comment's body. Any ideas on what could be going on?
Short explanation of your problem:
The #comment you're getting in your _comment_form partial is one that's already saved in your database, hence the call to the update action and the body that's already filled.
You're creating the new comment just fine with #comment = #profile.comments.new in your show action, but it gets overridden somewhere else.
You're mentioning that you wrapped the _comment render in a loop with <% for #comment in #user.profile.comments %>, the problem is most likely there.
Fix:
The only thing you should have to change is the _comment partial to (without the for loop that you added):
<li class="comment" title="<%= comment.author.profile.first_name %> <%= comment.author.profile.last_name %>">
<%= comment.body %>
</li>
When you do the render :partial => 'comment', :collection => #profile.comments, rails is smart enough to loop over #profile.comments and give the comment (not #comment) variable to the partial.
How to avoid this the next time:
I'll give you two rules of thumb to avoid getting in this situation:
Try to name your variables more precisely. #new_comment would have been a better name for the variable to store the new comment. #comment is a bit ambigous as you've got a boatload of those in your view.
Avoid creating and modifying instance variables (# variables) in your views, try to do this only in your controller. I'll admit your particular case was a bit harder to detect because of the <% for #comment in #user.profile.comments %>. The view got its name for a good reason, it's only supposed to let you view the data you've defined in your controller.
Hope this helps.

Rails 3 - Displaying submit errors on polymorphic comment model

Fairly new to Rails 3 and have been Googling every which way to no avail to solve the following problem, with most tutorials stopping short of handling errors.
I have created a Rails 3 project with multiple content types/models, such as Articles, Blogs, etc. Each content type has comments, all stored in a single Comments table as a nested resource and with polymorphic associations. There is only one action for comments, the 'create' action, because there is no need for the show, etc as it belongs to the parent content type and should simply redisplay that page on submit.
Now I have most of this working and comments submit and post just fine, but the last remaining issue is displaying errors when the user doesn't fill out a required field. If the fields aren't filled out, it should return to the parent page and display validation errors like Rails typically does with an MVC.
The create action of my Comments controller looks like this, and this is what I first tried...
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
respond_to do |format|
if #comment.save
format.html { redirect_to(#commentable, :notice => 'Comment was successfully created.') }
else
format.html { redirect_to #commentable }
format.xml { render :xml => #commentable.errors, :status => :unprocessable_entity }
end
end
end
When you fill nothing out and submit the comments form, the page does redirect back to it's appropriate parent, but no flash or nothing is displayed. Now I figured out why, from what I understand, the flash won't persist on a redirect_to, only on a render. Now here's where the trouble lies.
There is only the 'create' action in the comment controller, so I needed to point the render towards 'blogs/show' (NOTE: I know this isn't polymorphic, but once I get this working I'll worry about that then). I tried this in the "else" block of the above code...
else
format.html { render 'blogs/show' }
format.xml { render :xml => #commentable.errors, :status => :unprocessable_entity }
end
Anyway, when I try to submit an invalid comment on a blog, I get an error message saying "Showing [...]/app/views/blogs/show.html.erb where line #1 raised: undefined method `title' for nil:NilClass."
Looking at the URL, I think I know why...instead of directing to /blogs/the-title-of-my-article (I'm using friendly_id), it's going to /blogs/the-title-of-my-article/comments. I figure that extra "comments" is throwing the query off and returning it nil.
So how can I get the page to render without throwing that extra 'comments' on there? Or is there a better way to go about this issue?
Not sure if it matters or helps, but the route.rb for comments / blogs looks like this...
resources :blogs, :only => [:show] do
resources :comments, :only => [:create]
end
I've been plugging away at this over the last few weeks and I think I've finally pulled it off, errors/proper direction on render, filled out fields remain filled in and all. I did consider AJAX, however I would prefer to do it with graceful degradation if at all possible.
In addition, I admit I had to go about this a very hacky-sack way, including pulling in a way to pluralize the parent model to render the appropriate content type's show action, and at this stage I need the code to simply work, not necessarily look pretty doing it.
I KNOW it can be refactored way better, and I hope to do so as I get better with Rails. Or, anyone else who thinks they can improve this is welcomed to have at it. Anyway, here is all my code, just wanted to share back and hope this helps someone in the same scenario.
comments_controller.rb
class CommentsController < ApplicationController
# this include will bring all the Text Helper methods into your Controller
include ActionView::Helpers::TextHelper
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
respond_to do |format|
if #comment.save
format.html { redirect_to(#commentable, :notice => 'Comment was successfully created.') }
else
# Transform class of commentable into pluralized content type
content_type = find_commentable.class.to_s.downcase.pluralize
# Choose appropriate instance variable based on #commentable, rendered page won't work without it
if content_type == 'blogs'
#blog = #commentable
elsif content_type == 'articles'
#article = #commentable
end
format.html { render "#{content_type}/show" }
format.xml { render :xml => #commentable.errors, :status => :unprocessable_entity }
end
end
end
private
# Gets the ID/type of parent model, see Comment#create in controller
def find_commentable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
end
end
articles_controller.rb
class ArticlesController < ApplicationController
def show
#article = Article.where(:status => 1).find_by_cached_slug(params[:id])
#comment = Comment.new
# On another content type like blogs_controller.rb, replace with appropriate instance variable
#content = #article
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #article }
end
end
end
show.html.erb for articles (change appropriate variables for blog or whatever)
<h1><%= #article.title %></h1>
<%= #article.body.html_safe %>
<%= render :partial => 'shared/comments', :locals => { :commentable => #article } %>
shared/_comments.html.erb (I'm leaving out the displaying of posted comments here for simplification, just showing the form to submit them)
<%= form_for([commentable, #comment]) do |f| %>
<h3>Post a new comment</h3>
<%= render :partial => 'shared/errors', :locals => { :content => #comment } %>
<div class="field">
<%= f.label :name, :value => params[:name] %>
<%= f.text_field :name, :class => 'textfield' %>
</div>
<div class="field">
<%= f.label :mail, :value => params[:mail] %>
<%= f.text_field :mail, :class => 'textfield' %>
</div>
<div class="field">
<%= f.text_area :body, :rows => 10, :class => 'textarea full', :value => params[:body] %>
</div>
<%= f.submit :class => 'button blue' %>
<% end %>
shared/_errors.html.erb (I refactored this as a partial to reuse for articles, blogs, comments, etc, but this is just a standard error code)
<% if content.errors.any? %>
<div class="flash error">
<p><strong><%= pluralize(content.errors.count, "error") %> prohibited this page from being saved:</strong></p>
<ul>
<% content.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
I slightly refactored #Shannon answer to make it more dynamic. In my 'find_parent' method I'm grabbing the url path and fetching the controller name. In the 'create' method I'm creating an 'instance_variable_set' which creates a dynamic variable for either Articles (#article) or Blogs (#blog) or what ever it may be.
Hopefully you'll like what I've done? Please let me know if you have any doubts or if something can be improved?
def create
#comment = #commentable.comments.new(params[:comment])
if #comment.save
redirect_to #commentable, notice: "Comment created."
else
content_type = find_parent
instance_variable_set "##{content_type.singularize}".to_sym, #commentable
#comments = #commentable.comments
render "#{content_type}/show"
end
end
def find_parent
resource = request.path.split('/')[1]
return resource.downcase
end
You're getting an error because the blogs/show view likely refers to the #blog object, which isn't present when you render it in the comments controller.
You should go back to using the redirect_to rather than render. It wasn't displaying a flash when you made an invalid comment because you weren't telling it to set a flash if the comment wasn't saved. A flash will persist till the next request.