I'm trying to get a little red notifications circle to update dynamically in my Rails chat application using Action Cable. The problem is that sometimes when a message is sent, it seems to trigger the receive function more than once. I'm pretty sure I have the subscriptions defined correctly, but something is going wrong.
Here I make sure to create the subscriptions using the chat_id parameter. The same parameter is used in the submitNewMessage function
assets/javascript/channels/chat.js
$(document).on('turbolinks:load', function() {
submitNewMessage();
$.ajax({
type: "GET",
dataType: "json",
url: "/chats",
success: function(data){
$.each(data, function(i, chat){
App['chat' + chat.id] = App.cable.subscriptions.create({
channel: "ChatChannel",
chat_id: chat.id},
{ received: function(data){
# Code to update notifications
}}
});
}
}
});
function submitNewMessage(){
$('#message_text').keydown(function(event) {
if (event.keyCode == 13) {
var text = event.target.value
App['chat' + chat_id].send({text: text})
$('#message_text').val(" ")
return false;
}
});
}
});
And in the subscribed method I also use the chat_id params
channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{param['chat_id']}_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def receive(payload)
Message.create(user_id: current_user.id, chat_id: params["chat_id"], text: payload["text"])
end
end
How could the received function in chat.js be triggered more than once when a new comment is triggered?
Turns out that the Ajax request was creating a double of each request, when I visited other pages. I fixed it by adding
if (App['chat' + chat.id] == undefined){...
before creating the subscriptions. This way it only creates a subscription if it doesn't already exist.
Related
I am experiecing some issues with AJAX updating the page. The actual data in the database is updated but this is not always reflecting in real time on the web page.
For example, I have the following event:
$("#add_note").click(function(e) {
//e.preventDefault();
$("#add_note_form").validate({
rules: {
contact_note: {
required: true
}
},
submitHandler: function(form) {
contact.modal_update({
'obj' : $('#add_note_form'),
'uri' : '/contact/add_note/'
});
}
});
});
This function when a new note is created calls a callback to validate the form fields first and then if successful calls a callback inside a seperate class to conduct the update. See the modal_update class below:
// Update modal
this.modal_update = function(data)
{//
// Declare a few variables for the data object we've received
obj = data.obj // The form element to serialize
uri = data.uri;
// Get the form ID from the data-target attribute
id = obj.attr('data-target');
// URL to send to
url = this.site_url + uri + id;
// The form object
this.post_data(obj.serialize(),url);
// Hide Modal
obj.closest('.modal').modal('hide');
// Refresh
this.refresh();
}
This then figures out the correct route to ajax and calls a ajax call back inside the same class:
// AJAX post
this.post_data = function(obj,uri)
{
$.ajax({
data: obj,
dataType: 'json',
type: 'post',
url: uri,
headers: { "cache-control": "no-cache" },
cache: false,
success: function (response) {
if (response.success == true)
{
$("#alert_success .msg").html(response.message);
$("#alert_success").fadeIn(200).delay(2000).fadeOut(200);
}
else
{
$("#alert_error .msg").html(response.error);
$("#alert_error").fadeIn(200).delay(2000).fadeOut(200);
console.log(response.error);
}
}
});
}
I am then running another class callback to "refresh" the data in all the elements on the page:
this.refresh = function()
{
// Refresh the ajax requests
this.get_contact_data();
this.get_notes();
this.get_contact_log();
this.get_contact_tasks();
}
This class re loads the functions which run on page load to get the inial data into the tables/fields on the page. See "get_notes" below:
// Get notes
this.get_notes = function()
{
// Get all notes and populate table
var log_uri = this.site_url + "/contact/get_notes/" + this.contact_id;
this.get_data(log_uri,function(data) {
notes = $("#contact_notes ul");
notes.empty("");
// Populate the contact fields, assuming there is a result to play with
if (data != false) {
//alert(JSON.stringify(data));
$("#notes-tab .count").html("(" + data.length + ")");
$.each( data, function( key, value ) {
notes.append("<li class='list-group-item' modal-id='editNoteModal' data-target='" + value.ID + "'><div class='row'><div class='col-lg-3'><i class='fa fa-sticky-note mr-3'></i>" + value.timestamp + "</div><div class='col-lg-7'>" + value.note + "</div><div class='col-lg-2'><a href='#' class='edit mr-3'><i class='fa fa-edit mr-1'></i>Edit</a><a href='#' class='delete'><i class='fa fa-times mr-1'></i>Remove</a></div></div></li>");
});
console.log('Notes loaded');
} else {
notes.append("<li>There are currently no notes for this contact</li>");
}
});
}
Now the problem:
For some reason this does not update consistently in real time. The data is updated fine on the server side but on the client side the update/refresh does not always update. I might add a note and get a correct update response but the refresh method seems to be receiving the old data and always be one note behind. So the next time I add a note, the one I added before then appears and so forth.
Another problem I am experiencing is the methods seem to stack on each event so if I add one note (or one of the other methods) I will see the console say "notes loaded" but on the second note it says "notes loaded" twice, then on the 3rd note added 3 times and so forth.
I am sure there must be something fatal flaw in the design of my code here but I am not experienced enough with javascript/jquery to notice what direction I am going wrong so I can fix it.
I thought that this was an issue with ajax caching and not refreshing the result so I have adjusted the ajax request as cache none and also to send no cache headers. I am running in wamp.
In your case, your refresh code will always run before your data got updated. Because ajax is asynchronous so the code behind and below ajax will always execute nearly the time your ajax running.
At the time you run your post_data function to call the API, the refresh function got run too. So it's done before your data got updated.
You should run refresh function inside ajax callback. For example:
this.post_data = function(obj,uri, callback)
{
$.ajax({
data: obj,
dataType: 'json',
type: 'post',
url: uri,
headers: { "cache-control": "no-cache" },
cache: false,
success: function (response) {
if (response.success == true)
{
$("#alert_success .msg").html(response.message);
$("#alert_success").fadeIn(200).delay(2000).fadeOut(200);
}
else
{
$("#alert_error .msg").html(response.error);
$("#alert_error").fadeIn(200).delay(2000).fadeOut(200);
console.log(response.error);
}
callback();
}
});
}
And in modal_update, you pass refresh function to post_data as a callback:
this.modal_update = function(data)
{//
// Declare a few variables for the data object we've received
obj = data.obj // The form element to serialize
uri = data.uri;
// Get the form ID from the data-target attribute
id = obj.attr('data-target');
// URL to send to
url = this.site_url + uri + id;
// The form object
this.post_data(obj.serialize(),url, this.refresh);
// Hide Modal
obj.closest('.modal').modal('hide');
}
You should read more about asynchronous ajax. You can use other tricky solution is setTimeout to run this.refresh but I do not recommend that because you not sure when the update is done.
I have delete action in my controller, this is the code in pickups_controller.rb
def delete
#pickup = Pickup.find(params[:id])
if !#pickup.nil?
#pickup.destroy
render json: { success_message: "Success!, Pickup is deleted." }, status: :ok
end
end
I call the delete action using javascript json by pressing a button using assets/javascripts/pickups.js
document.addEventListener("DOMContentLoaded", function(event) {
var xhttp = new XMLHttpRequest();
// delete the pickup you choose
$('.removepickup.btn.btn-primary').on('click', function() {
var pickup_div = $(this).parents('.removepickupparent');
var pickup_id = pickup_div.attr('id');
var x = "../deletepickup?id=" + pickup_id;
$.ajax({
type: "POST",
url: x,
success: function(data) {
var success = data.success_message;
$(".successr"+ pickup_id).text(success).show(0).delay(1000).hide(0);
setTimeout(function () {
location.reload();
}, 1000);
},
error: function (xhr, ajaxOptions, thrownError){
if(xhr.status==404) {
$(".errorl"+ pickup_id).text("Fail!, pickup Is Already Deleted Before").show(0).delay(1000).hide(0);
setTimeout(function () {
location.reload();
}, 2000);
}
}
});
});
// when pressing on this button, it redirects you to create pickup page
$('.addpickup.btn.btn-primary').on('click', function() {
var success = "Redirecting to add pickup Page"
$(".successp").text(success).show(0).delay(2000).hide(0);
setTimeout(function () {
$(location).attr('href', '../createpickup');
}, 2000);
});
});
the function is working great, but when adding 4 lines extra code inside the delete action, it doesn't work, here's the code after adding 4 lines of extra code inside my delete action, and the action is not working.
def delete
#pickup = Pickup.find(params[:id])
if !#pickup.nil?
# the start of the extra code
#trip = Trip.find(#pickup.trip_id)
if !#trip.nil?
#trip.seatsno = #trip.seatsno + 1
#trip.save
end
# the end of the extra code
#pickup.destroy
render json: { success_message: "Success!, Pickup is deleted." }, status: :ok
end
end
any solutions please? .. knowing that I'm still beginner in Ruby on Rails
Note:
I used byebug, and when reaching the first line in the etra code I got this error in the local server terminal
"request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?"
Use the find_by instead of the find method. The find' method raises the exception if a particular record is not found, whilefind_by` returns nil.
Usage:
find_by(id: params[:id])
This answer is more of a refactor suggestion than the actual answer, but it may fix your problem as well.
You can refactor your action to this:
def delete
#pickup = Pickup.find(params[:id])
# no need to test #pickup.nil? here because `find` method raise
# an ActiveRecord::RecordNotFound error if the record is not found
# which should be caught by ApplicationController to render a 404
if #pickup.destroy
#pickup.trip.update_attributes(seatsno: #pickup.trip.seatsno + 1)
render json: { success_message: "Success!, Pickup is deleted." }, status: :ok
else
render json: { error_message: "Error, Pickup could not be deleted." }, status: 409
end
end
Even better, move the concern of incrementing seatsno to the Pickup model:
# app/models/pickup.rb
after_destroy :increment_trip_seatsno
def increment_trip_seatsno
self.trip.update_attributes(seatsno: self.trip.seatsno + 1)
end
And remove the concern from the Controller. This way, every time a Pickup record is destroyed via Rails (console or other places in your app), the trip will be updated accordingly.
I have an application that displays a YouTube video and has a rate button to allow a user to like or unlike the video. On the click event 3 functions are called chained together through the success function of the ajax. The flow is this: ytvRate() -> getRating() -> showRating()
When I log the actions and results the response from getRating() does not have the value that I sent in ytvRate(). If I wait a while and refresh the page, the result of getRating() comes back correct. I call getRating() inside the success function of the ajax in ytvRate(). Doesn't that mean the function should not be called until a success response is received?
Here is an example of my logs:
rating sent: like
call get rating
this is my rating: none
call show rating
As you can see, the rating returned from the API is not correct - it should be the rating I just sent. Upon refresh the same call does return the correct rating... so, is there a delay or something to the data api updating the correct information? How can I get the correct rating on the same button click that sends the request?
Here are the functions (showRating does not seem relevant to the problem. It works fine as long as it gets the correct rating - which it is not.)
function ytvRate(id, rating, event){
event.preventDefault()
var apiKey = 'This is a valid key';
var client_id = 'This is a valid client id';
var redirect_uri = 'This is a redirect uri';
var scope = 'https://www.googleapis.com/auth/youtube';
var rateUrl = 'https://www.googleapis.com/youtube/v3/videos/rate?id='+id+'&key='+apiKey+'&rating='+rating;
if(getHash().access_token){
var token = getHash().access_token;
$.ajax({
type: "POST",
url: rateUrl,
beforeSend: function (request){
request.setRequestHeader('Authorization', 'Bearer ' + token);
},
success: function(data){
console.log('rating sent: '+rating);
getRating(id);
},
error: function(e) {
console.log(e);
}
});
} else{
window.location = 'https://accounts.google.com/o/oauth2/v2/auth?client_id='+client_id+'&redirect_uri='+redirect_uri+'&scope='+scope+'&response_type=token&prompt=consent&include_granted_scopes=false';
}
return false;
}
function getRating(id){
var getRatingUrl = 'https://www.googleapis.com/youtube/v3/videos/getRating?id='+id;
console.log('call get rating');
if(getHash().access_token){
var token = getHash().access_token;
$.ajax({
type: "GET",
url: getRatingUrl,
beforeSend: function (request){
request.setRequestHeader('Authorization', 'Bearer ' + token);
},
success: function(data){
var rating = data.items[0].rating;
console.log("this is my rating: " + rating);
showRating(rating, id);
}
});
}
}
function showRating(response, id){
console.log('call show rating');
numLikes(id);
if(response == 'like'){
document.getElementById("notliked").className = "hide";
document.getElementById("liked").className = "";
document.getElementById("like-btn").style.color = "#87CEFA";
document.getElementById("like-btn").setAttribute("onclick", "ytvRate('"+id+"', 'none', event)");
} else{
document.getElementById("notliked").className = "";
document.getElementById("liked").className = "hide";
document.getElementById("like-btn").style.color = "inherit";
document.getElementById("like-btn").setAttribute("onclick", "ytvRate('"+id+"', 'like', event)");
}
}
Edit:
Interestingly, if I call the youtube/v3/videos resource in a new method instead of youtube/v3/videos/getRating and access the statistics.likeCount, the number is instantly updated. Why can I not receive the user rating with the same efficiency?
After the discussion in the comments I suggest you to take a different approach. When ytvRate success you don't need to fetch getRating as you already know what is the rating set by the user.
The rate method is like a setter in regular programming language - if it successed (didn't throw an exception or returned an error) you can assume the current value is the one you set without fetching it again. This might be wrong assumption in multithreaded/distributed enviroments but might be ok in your case.
function ytvRate(id, rating, event){
...
success: function(data){
console.log('rating sent: '+rating);
showRating(rating, id);
}
...
}
I have an AJAX call:
$('#enrollment_members').change(function(){
var memberNumber = $(this);
$.ajax({type: 'GET',
url: $(this).href,
type: "get",
data: { members: memberNumber.val() },
error: function(){ alert("There was a problem, please try again.") }
});
return false;
console.log(data);
});
through which I send params[:members] into a new method.
I wanna do something like this:
def new
#enrollment = Enrollment.new
params[:members] ? params[:members].to_i.times { #enrollment.atendees.build } : #enrollment.atendees.build
respond_to do |format|
format.js
end
end
I need this value in order to know how many fields_for to build.
But this being in the new action, how can I update the content of the new view after inputting a value in the members input field?
From that ternary, #enrollment.atendees contains 4 objects.
My new.js.erb :
$("#contact-wrap").html("<%= j render(:partial => 'enrollments/form') %>");
The xhr response contains 4 fields_for forms.
Is the object #enrollment_members the input value you are trying to pass to the controller?
if so, try this:
$('#enrollment_members').change(function(){
var memberNumber = $(this);
$.ajax({type: 'GET',
url: $(this).href,
type: "get",
data: { members: memberNumber.serialize() }, //CHANGE
error: function(){
alert("There was a problem, please try again.")
}
});
return false;
Hmm are you really sure you need a custom made solution for this ? The behavior of dynamically adding/removing children in fields_for is already adressed by several gems like nested_form (no longer maintained) or cocoon (more promising)
I'd suggest to actually reuse their library even if you need to tweak it your way. Because doing an AJAX request is completely pointless. Your AJAX is a GET request that will always do the same thing, unless you have a custom atendees.build which will do weird things for successive calls (like incrementing a global counter somewhere).
Those gems I mentionned above will actually save the HTML fields to generate in a "template" (stored locally in HTML or JS), and just call this template when you want to add a new field.
I got it working by modifying the ajax call:
$('#enrollment_members').change(function(){
var memberNumber = $(this);
$.ajax({type: 'GET',
url: $(this).href,
type: "get",
dataType : "html",
data: { members: memberNumber.val() },
success: function( data ) {
var result = $('<div />').append(data).find('#contact-wrap').html();
$('#contact-wrap').html(result);
$('#atendees-wrap').show();
},
error: function( xhr, status ) {
alert( "Sorry, there was a problem!" );
}
});
return false;
});
I'm newbie with Rails
My purpose is insert song_id and title which received from Javascript via AJAX POST into Database (MySQL)
In my javascript file
var song_id = "23f4";
var title = "test";
$( document ).ready( function() {
jQuery.ajax({
url: 'create',
data: "song_id=" + song_id + "&title=" + title,
type: "POST",
success: function(data) {
alert("Successful");
},
failure: function() {
alert("Unsuccessful");
}
});
} );
In my editor_controller.rb
class EditorController < ApplicationController
def new
#song = Song.new
end
def create
logger.debug("#{params[:song_id]}")
logger.debug("#{params[:title]}")
#song = Song.new(song_params)
if #song.save
redirect_to root_path
else
flash[:notice_song_failed] = true
redirect_to root_path
end
end
private
def song_params
params.require(:song).permit(:song_id, :title)
end
The problem is when I running the Rails app with this code, the Console notices me that
ActionController::ParameterMissing at /editor/create
param is missing or the value is empty: song
I'm trying to use
private
def song_params
params.require(:song).permit(params[:song_id], params[:title])
end
but it doesn't work and notices me the same, moreover in the terminal log told me below
Started POST "/editor/create" for ::1 at 2015-04-01 01:07:23 +0700
Processing by EditorController#create as /
Parameters: {"song_id"=>"23f4", "title"=>"test"}
23f4
test
Completed 400 Bad Request in 1ms
Do I missed something in my code, Thanks for Advance.
You are not sending a song parameter at all. It looks like you need to update the data line in the jQuery.ajax call to include the song parameter like so:
data: {song: {song_id: song_id, title: title}}
This:
params.require(:song).permit(params[:song_id], params[:title])
is saying "require the 'song' parameter, and allow 'song_id' and 'title' through. If you don't pass a song parameter, you'll get a bad request.
You can either:
Change that line of code to remove the require on 'song'
or
Like #infused says, change your ajax call to send a song JSON object.