I am getting an uncaught reference error: XHR is not defined in my coffeescript below.
jQuery ->
# Create a comment
$(".comment-form")
.on "ajax:beforeSend", (evt, xhr, settings) ->
$(this).find('textarea')
.addClass('uneditable-input')
.attr('disabled', 'disabled');
.on "ajax:success", (evt, data, status, xhr) ->
$(this).find('textarea')
.removeClass('uneditable-input')
.removeAttr('disabled', 'disabled')
.val('');
$(xhr.responseText).hide().insertAfter($(this)).show('slow')
# Delete a comment
$(document)
.on "ajax:beforeSend", ".comment", ->
$(this).fadeTo('fast', 0.5)
.on "ajax:success", ".comment", ->
$(this).hide('fast')
.on "ajax:error", ".comment", ->
$(this).fadeTo('fast', 1)
I have been unable to figure out the issue and I've pretty weak in javascript.
What I'm trying to do is add a comment to a users page then show it via AJAX. The comment saves without any problem as I can see it if I manually refresh the page. However neither the create or delete actions work in the Coffeescript.
Since neither the create or delete AJAX calls seem to work, I am assuming it's in the way the script is called. I'll include the relevant controller code here as well.
class CommentsController < ApplicationController
before_action :set_comment, only: [:show, :destroy]
def create
#comment_hash = comment_params
#obj = #comment_hash[:commentable_type].constantize.find(#comment_hash[:commentable_id])
# Not implemented: check to see whether the user has permission to create a comment on this object
#comment = Comment.build_from(#obj, current_user, #comment_hash[:body])
#comment.user = current_user
if #comment.save
render partial: "comments/comment", locals: { comment: #comment }, layout: false, status: :created
else
p #comment.errors
render js: "alert('error saving comment');"
end
end
def destroy
if #comment.destroy
render json: #comment, status: :ok
else
render js: "alert('error deleting comment');"
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_comment
#comment = Comment.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def comment_params
params.require(:comment).permit( :commentable_id, :commentable_type, :body, :user_id)
end
end
Also my partial for the comment:
<div class='comment'>
<hr>
<%=link_to "×", comment_path(comment), method: :delete, remote: true, confirm: "Are you sure you want to remove this comment?", disable_with: "×", class: 'close' %>
<small><%=comment.updated_at.to_s(:short) %></small>
<p><%= comment.body %></p>
And the form itself to add new comments:
<div class='comment-form'>
<%= simple_form_for comment, remote: true do |f| %>
<%= f.input :body, input_html: { rows: "2" }, label: false %>
<%= f.input :commentable_id, as: :hidden, value: comment.commentable_id %>
<%= f.input :commentable_type, as: :hidden, value: comment.commentable_type %>
<%= f.button :submit, 'New Note', class: "button tiny radius", disable_with: "Submitting…" %>
<% end %>
</div>
Any help would be appreciated since I just don't know where to start right now. I'm not sure how I should be defining the XHR.
Incidentally, most of the code for this was from the tutorial here
Related
I have follow and unfollow buttons for users on my application. I don't want to do anything fancy, I just want to not have the page refresh every time follow or unfollow button is clicked.
My controller
relationships_controller.rb
def create
current_user.follow(#user)
respond_to do |format|
format.html { #handle HTML, i.e. full page reload }
format.js # handle ajax request
end
end
def destroy
current_user.unfollow(#user)
respond_to do |format|
format.html
format.js # this one handle the request comes from `remote: true` button
end
end
My view
tweets/index.html.erb
<% if current_user.id != tweet.user.id %>
<% if current_user.following?(tweet.user) %>
<%= button_to "Unfollow", relationships_path(user_id: tweet.user), remote: true, method: :delete, :class => "btn btn-primary" %>
<% else %>
<%= button_to "Follow", relationships_path(user_id: tweet.user), remote: true, :class => "btn btn-primary" %>
<% end %>
<br>
<% end %>
<hr/>
<% end %>
Relationships model
relationship.rb
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
validates :followed_id, presence: true
end
User model
User.rb
has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
has_many :followers, through: :passive_relationships, source: :follower
def follow(user)
active_relationships.create(followed_id: user.id)
end
def unfollow(user)
active_relationships.find_by(followed_id: user.id).destroy
end
def following?(user)
following.include?(user)
end
Routes
routes.rb
resource :relationships, :only => [:create, :destroy]
Application.js
require("#rails/ujs").start()
require("turbolinks").start()
require("#rails/activestorage").start()
require("channels")
require("chartkick")
require("chart.js")
//= require jquery3
//= require popper
//= require bootstrap-sprockets
Inspected button element
<form class="button_to" method="post" action="/relationships?user_id=1" data-remote="true"><input class="btn btn-primary" type="submit" value="Follow"><input type="hidden" name="authenticity_token" value="hfwF8wXBcp/OM2P/pCYBnEBrjw22BDKWbw/dZFwwDsRpiIFq5jBKS/AoTMjkCZRrGum7UyW1kaL3h/4XEM2wIg=="></form>
With this when I click follow nothing now happens. I think I need a new js file in my views but unsure how to implement it.
I have looked at solutions but they are many and varied and seeking to do more than I want to do which is just a simple no refresh.
How is this best achieved? (can provide more code if needed)
EDIT: This code got jQuery working in Rails 6 in my ..webpack/enironment.js file
# app/config/webpack/environment.js
const {environment} = require('#rails/webpacker');
const webpack = require('webpack');
environment.plugins.append('Provide', new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery' # or if its not work specify path `'jquery/src/jquery'` which node_modules path for jquery
}));
module.exports = environment;
For #max
$(document).on('ajax:success', '.follow-btn', function(event){
let $el = $(this);
let method = this.dataset.method;
if (method === 'post') {
$('.follow-btn[href="'+this.href+'"]').each(function(el){ $(el).text('Unfollow'); });
this.dataset.method = 'delete';
} else if (method === 'delete') {
$('.follow-btn[href="'+this.href+'"]').each(function(el){ $(el).text('Follow'); });
this.dataset.method = 'post';
}
});
Instead of going down the js.erb rabbit hole you can just send a JSON request and write a simple event handler.
Lets start by adding a data-type="json" attribute to the buttons so they send a request for JSON instead of javascript:
<% unless current_user == tweet.user %>
<% if current_user.following?(tweet.user) %>
<%= link_to "Unfollow", relationships_path(user_id: tweet.user),
data: { remote: true, type: :json, method: :delete },
class: "follow-btn btn btn-primary"
<% else %>
<%= link_to "Follow", relationships_path(user_id: tweet.user),
data: { remote: true, type: :json, method: :post},
class: "follow-btn btn btn-primary"
%>
<% end %>
<% end %>
And then just write JSON responses for your controller.
def create
current_user.follow(#user)
respond_to do |format|
format.html
format.json { head :created }
end
end
def destroy
current_user.unfollow(#user)
respond_to do |format|
format.html
format.json { head :no_content }
end
end
As you can see its pretty damn simple, when creating a resource you return a 201 - Created and usually a location header or the entity in the body (a JSON payload describing what was created). When you update or destroy a record a 204 - No Content status code is sufficient.
If you test it now and look at the network tab in your browser inspector you will see that an AJAX request is sent but nothing happens in the view.
So lets write an event handler that toggles the button text and method after the request was sent. Since Rails UJS already created the AJAX handler for the button for us we can just hook into its events:
// put this in your application.js or anywhere in your pack
$(document).on('ajax:success', '.follow-btn', function(event){
let $el = $(this);
let method = this.dataset.method;
if (method === 'post') {
$el.text('Unfollow');
this.dataset.method = 'delete';
} else if (method === 'delete') {
$el.text('Follow');
this.dataset.method = 'post';
}
});
Why is this better than a js.erb template?
No server side involvement in updating the UI on the client. No spagetti-code views.
JavaScript is minified, not generated by ERB and easy to debug/reason about.
It can be changed to use optimistic create/delete to give instant feedback
I have implemented follow/unfollow functionality and would like to add AJAX call to it, but I am stuck.
My partial _follow_button.html.erb for follow/unfollow which is rendered on Users->index, looks like:
<% if current_user.id != user.id %>
<% if !current_user.following?(user) %>
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
<div><%= hidden_field_tag :followed_id, user.id %></div>
<span class="follow"><%= f.submit "Follow User", class: "btn btn-primary btn-sm" %></span>
<% end %>
<% else %>
<%= form_for(current_user.active_relationships.find_by(followed_id: user.id),
html: { method: :delete }, remote: true) do |f| %>
<span class="unfollow"><%= f.submit "Unfollow User", class: "btn btn-secondary btn-sm" %></span>
<% end %>
<% end %>
<% end %>
Then my controller for relationships looks like:
class RelationshipsController < ApplicationController
respond_to :js, :json, :html
def create
user = User.find(params[:followed_id])
#follow = current_user.follow(user)
end
def destroy
user = Relationship.find(params[:id]).followed
#unfollow = current_user.unfollow(user)
end
end
My view on user profile looks like:
<div class="col-5" style="margin-left: -5px;">
<%= render '/components/follow_button', :user => User.find_by_username(params[:id]) %>
</div>
My routes.rb have the following routes defined:
resources :users do
member do
get :following, :followers
end
end
resources :relationships, only: [:create, :destroy]
My Views folder structure has subfolders Users and Relationships. Both of them have separate controllers, and I have tried adding simple alert function 'alert("Works");' to the create.js.erb in both of those subfolders to try and match them with the controller, but none don't seem to work. This is my first Rails project, and I do not quite understand what the issue could be. Any suggestions?
Calling the partial follow/unfollow
<% if current_user.id != user.id %>
<%= render partial: 'follow_links', locals: { user: user }
<% end %>
Partial follow_links.
<% show_follow_link = current_user.following?(user) ? 'hidden' : '' %>
<% show_unfollow_link = current_user.following?(user) ? '' : 'hidden' %>
<!-- links to follow/unfollow have data-attributes that include the path to make the ajax post and the user to follow, that is used to find the link to show after the ajax call. You should use the path to the controller that will create or destroy the relationship -->
<%= link_to 'Follow', '#', { class: 'follow-user btn-success #{show_follow_link}', "data-url": follow_user_path(user.id), "data-followee": user.id } %>
<%= link_to 'Unfollow', '#', { class: 'unfollow-user btn-danger #{show_unfollow_link}', "data-url": unfollow_user_path(user.id), "data-followee": user.id } %>
Javascript for the partial. Ajax post to follow/unfollow
$('.follow-user').on("click",function() {
follow_unfollow($(this), "follow")
});
$('.unfollow-user').on("click",function() {
follow_unfollow($(this), "unfollow")
});
function follow_unfollow(target, what_to_do)
url = target.attr('data-url')
followee = target.attr('data-followee')
if (what_to_do == "follow") {
other_button = $('.unfollow-user[data-followee="'+followee+'"]')
} else {
other_button = $('.follow-user[data-followee="'+followee+'"]')
}
$.ajax( {
url: url,
type: 'post',
success: function() {
// Hide this link
target.addClass('hidden');
// Show the other link
other_button.removeClass('hidden');
},
error: function(ret) {
alert(ret.responseJSON.error);
}
});
};
Changes in your controller.
class RelationshipsController < ApplicationController
def create
user = User.find(params[:followed_id])
#follow = current_user.follow(user)
respond_to do |format|
if #follow.valid?
format.html
format.json: { render json: #follow }
return
else
format.html
format.json: { render json: { :error => 'Follow failed', :status_code :not_found } }
end
end
end
def destroy
user = Relationship.find(params[:id]).followed
#unfollow = current_user.unfollow(user)
respond_to do |format|
if #unfollow.valid?
format.html
format.json: { render json: #unfollow }
else
format.html
format.json: { render json: { :error => 'Unfollow failed', :status_code :not_found } }
end
end
end
end
An advice
An advice, also regarding your last question: I would recommend - instead of posting questions about debugging code on StackOverflow - create a good debugging environment for yourself.
Byebug or Binding pry is a good place to start, but before you can use those properly you need to understand the code you are using. I would recommend reading Working with Javascript in depth! - it really helped me getting the hang of it and understanding the dataflow of Rails and ajax.
This would, i think, break the unbreakable Stackoverflow-loop, that i myself were tied to for a long time:
loop do
puts "Try code"
sleep 1000
puts "Arrhh! an error!"
sleep 1000
puts "Posting on Stackoverflow"
sleep 1000
puts "Waiting for answer"
sleep 1000
end
I hope you figure it out!
I am following section 4 (Server Side Concerns) to set up ajax on a page. I've copied the tutorial text completely (replacing the model names with my own) and it creates and saves my "Participants" record, but does not automatically refresh the ajax partial.
This is the error I get...which looks like it's referrring to my create.js.erb
ActionView::Template::Error ('nil' is not an ActiveModel-compatible object. It must implement :to_partial_path.):
1: $("<%= escape_javascript(render #participant) %>").appendTo("#participants");
2: // $('#participants').html("<%= j (render #participants) %>");
app/views/participants/create.js.erb:2:in `_app_views_participants_create_js_erb___1675277149181037111_70181034249880'
Here's my code
class ParticipantsController < ApplicationController
def new
#participant = Participant.new
#participants = #participants.recently_updated
end
def create
#participant = Participant.new(participant_params)
respond_to do |format|
if #participant.save
format.html { redirect_to #participant, notice: 'Helper Invited!' }
format.js {}
format.json { render json: #participant, status: :created, location: #participant }
else
format.html { render action: "new" }
format.json { render json: #participant.errors, status: :unprocessable_entity }
end
end
end
_form.html.erb
<ul id="participants">
<%= render #participants %>
</ul>
<%= form_for(#participant, remote: true) do |f| %>
<%= f.label :email %><br>
<%= f.email_field :email %>
<%= f.submit 'SUBMIT' %>
<script>
$(document).ready(function() {
return $("#new_participant").on("ajax:success", function(e, data, status, xhr) {
return $("#new_participant").append(xhr.responseText);
}).on("ajax:error", function(e, xhr, status, error) {
return $("#new_participant").append("<p>Oops. Please Try again.</p>");
});
});
</script>
<script>
$(function() {
return $("a[data-remote]").on("ajax:success", function(e, data, status, xhr) {
return alert("The helper has been removed and notified.");
});
});
</script>
_participant.html.erb
<li >
<%= participant.email %> <%= link_to participant, remote: true, method: :delete, data: { confirm: 'Are you sure?' } do %>REMOVE<% end %>
</li>
create.js.erb
$("<%= escape_javascript(render #participant) %>").appendTo("#participants");
destroy.js.erb
$('#participants').html("<%= j (render #participants) %>");
It's on line 2 of your create.js.erb file, it's the missing #participants not the #participant.
You've commented the line out in JS, but the ERB is still going to be processed by Rails, so it's still trying to do the render #participants
Update
For future... it's the last line of that error that's the key:
app/views/participants/create.js.erb:2
See the 2 at the end, that's telling you which line the error happened on, and so that's where you need to focus when looking for the problem.
I've having trouble with the following coffeescript:
jQuery ->
# Create a comment
$(".comment-form")
.on "ajax:beforeSend", (evt, xhr, settings) ->
$(this).find('textarea')
.addClass('uneditable-input')
.attr('disabled', 'disabled');
.on "ajax:success", (evt, data, status, xhr) ->
$(this).find('textarea')
.removeClass('uneditable-input')
.removeAttr('disabled', 'disabled')
.val('');
$(xhr.responseText).hide().insertAfter($(this)).show('slow')
# Delete a comment
$(document)
.on "ajax:beforeSend", ".comment", ->
$(this).fadeTo('fast', 0.5)
.on "ajax:success", ".comment", ->
$(this).hide('fast')
.on "ajax:error", ".comment", ->
$(this).fadeTo('fast', 1)
Basically, I have a user form that I add comments to. When a new comment is added it should change class and disable the textarea before sending the data off to the database. Then it should reset the class, clear the textarea and enable the textarea again. Then finally it should add the new comment after the textarea.
The first part of the code works and the class is added to the textarea and it is set to disabled but the rest of the script never happens. Of course the comment is actually saved to the database and a refresh of the page will show the comment.
I've gone over this a ton of times and can't figure out what is going wrong. I did have an earlier issue with the indenting being wrong with the script but that has been fixed.
My CommentController code is as below:
class CommentsController < ApplicationController
before_action :set_comment, only: [:show, :destroy]
def create
#comment_hash = comment_params
#obj = #comment_hash[:commentable_type].constantize.find(#comment_hash[:commentable_id])
# Not implemented: check to see whether the user has permission to create a comment on this object
#comment = Comment.build_from(#obj, current_user, #comment_hash[:body])
#comment.user = current_user
if #comment.save
render partial: "comments/comment", locals: { comment: #comment }, layout: false, status: :created
else
p #comment.errors
render js: "alert('error saving comment');"
end
end
def destroy
if #comment.destroy
render json: #comment, status: :ok
else
render js: "alert('error deleting comment');"
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_comment
#comment = Comment.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def comment_params
params.require(:comment).permit( :commentable_id, :commentable_type, :body, :user_id)
end
end
Here's my form for creating comments:
<div class='comment-form'>
<%= simple_form_for comment, remote: true do |f| %>
<%= f.input :body, input_html: { rows: "2" }, label: false %>
<%= f.input :commentable_id, as: :hidden, value: comment.commentable_id %>
<%= f.input :commentable_type, as: :hidden, value: comment.commentable_type %>
<%= f.button :submit, 'Save Note', class: "button tiny radius", disable_with: "Submitting…" %>
<% end %>
</div>
And here's my code for displaying comments:
<div class='comment'>
<hr>
<%=link_to "×", comment_path(comment), method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove this comment?',disable_with: 'x' }, class: 'close' %>
<small><%=comment.updated_at.to_s(:short) %></small>
<p><%= comment.body %></p>
By the way, the delete action partially works. It deletes the comment from the database and it hides all of the comments. A page refresh shows the comments that have not been deleted.
Any help would be greatly appreciated since I don't know where to go with this not at all. In my mind it should work so I must be missing something simple.
It's an issue with your indentation. The line .on "ajax:success" must be indented at the same level as the other .on. And the code that follows must be indented accordingly as well:
jQuery ->
# Create a comment
$(".comment-form")
.on "ajax:beforeSend", (evt, xhr, settings) ->
$(this).find('textarea')
.addClass('uneditable-input')
.attr('disabled', 'disabled');
.on "ajax:success", (evt, data, status, xhr) -> #<-- this line!
$(this).find('textarea')
.removeClass('uneditable-input')
.removeAttr('disabled', 'disabled')
.val('');
$(xhr.responseText).hide().insertAfter($(this)).show('slow')
What would be the best way to update a record and display a flash success notice without leaving the page?
Here is part of my code but it redirects back to root path:
View
<%= form_for #user, url: record_testimonial_path(#user) do |f| %>
<%= f.text_area :testimonial %>
<%= f.submit "Submit"%>
<% end %>
Controller
def record_testimonial
#user.update_attribute(:testimonial, params[:user][:testimonial])
flash[:success] = "Thank you!"
redirect_to root_path
end
You can redirect back:
redirect_to :back, flash: { success: t('flash.thank_you') }
Or you have to do it via remote:
<%= form_for #user, url: record_testimonial_path(#user), remote: true do |f| %>
<%= f.text_area :testimonial %>
<%= f.submit "Submit"%>
<% end %>
And in the controller:
def record_testimonial
if #user.update_attribute(:testimonial, params[:user][:testimonial])
respond_to do |format|
format.js { render 'record_testimonial', layout: false }
end
else
render nothing: true # This will silently fail. Probably not intended.
end
end
And then the record_testimonial.js.erb:
$('#some_id').html('<%= j render "some_partial", user: #user %>');
These are the most common and I would say sensible ways. If you use ajax don't forget to manually display the flash message, should be 1 line of JS.
Best way to avoid a page refresh completely would be to set the form to use :remote => true like this
<%= form_for #user, url: record_testimonial_path(#user), :remote => true do |f| %>
and then respond accordingly in the controller:
def record_testimonial
respond_to do |format|
if #user.update_attribute(:testimonial, params[:user][:testimonial])
render :success
else
render :error
end
end
end
You would need a corresponding template for success and error here, with which to handle the displaying of a success or error message.
For further info, look here.